使用WebSocket的起因
最近毕设项目需要一个能让服务端通知客户端的工具,使用Http轮询或者Http长连接都明显不符合我的需求,所以找到了WebSocket作为长连接工具。
WebSocket简介(维基百科)
WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries
(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头[1]从HTTP协议更改为WebSocket协议。
WebSocket协议支持Web浏览器(或其他客户端应用程序)与Web服务器之间的交互,具有较低的开销,便于实现客户端与服务器的实时数据传输。 服务器可以通过标准化的方式来实现,而无需客户端首先请求内容,并允许消息在保持连接打开的同时来回传递。通过这种方式,可以在客户端和服务器之间进行双向持续对话。 通信通过TCP端口80或443完成,这在防火墙阻止非Web网络连接的环境下是有益的。另外,Comet之类的技术以非标准化的方式实现了类似的双向通信。
大多数浏览器都支持该协议,包括Google Chrome、Firefox、Safari、Microsoft Edge、Internet Explorer和Opera。
与HTTP不同,WebSocket提供全双工通信。[2][3]此外,WebSocket还可以在TCP之上实现消息流。TCP单独处理字节流,没有固有的消息概念。 在WebSocket之前,使用Comet可以实现全双工通信。但是Comet存在TCP握手和HTTP头的开销,因此对于小消息来说效率很低。WebSocket协议旨在解决这些问题。
WebSocket协议规范将ws
(WebSocket)和wss
(WebSocket Secure)定义为两个新的统一资源标识符(URI)方案[4],分别对应明文和加密连接。除了方案名称和片段ID(不支持#
)之外,其余的URI组件都被定义为此URI的通用语法。[5]
使用浏览器开发人员工具,开发人员可以检查WebSocket握手以及WebSocket框架。[6]
简而言之,WebSocket是一个非常好用的长连接框架
WebSocket服务端
下面将使用springboot集成的webSocket
Maven配置
首先加入Springboot,版本任选
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
在pom.xml中添加Springboot整合的websocket包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Java代码
实现WebSocketHandler主程序
WebSocketHandler 源码如下
public interface WebSocketHandler {
/**
* 在WebSocket协商成功且WebSocket连接已打开并准备好使用后调用。
*/
void afterConnectionEstablished(WebSocketSession var1) throws Exception;
/**
* 当新的WebSocket消息到达时调用。
*/
void handleMessage(WebSocketSession var1, WebSocketMessage<?> var2) throws Exception;
/**
* 处理来自基础WebSocket消息传输的错误。
*/
void handleTransportError(WebSocketSession var1, Throwable var2) throws Exception;
/**
* 在任一方关闭WebSocket连接之后或发生传输错误之后调用。尽管从技术上来说,会话可能仍处于打开状态,但根据底层实现,不鼓励在此时发送消息,并且很可能不会成功。
*/
void afterConnectionClosed(WebSocketSession var1, CloseStatus var2) throws Exception;
/**
* WebSocketHandler是否处理部分消息。如果此标志设置为true,并且基础WebSocket服务器支持部分消息,则可能会拆分大的WebSocket消息或大小未知的消息,并且可能会通过对handleMessage()方法的多次调用来接收该消息。标志WebSocketMessage.isLast()指示消息是否为部分消息以及是否为最后一部分。
*/
boolean supportsPartialMessages();
}
从上可知WebSocketHandler 需要我们实现五个方法。相比直接实现 WebSocketHandler,更为简单的方法是扩展 AbstractWebSocketHandler,这是 WebSocketHandler 的一个抽象实现。
TextWebSocketHandler 是 AbstractWebSocketHandler 的子类,它会拒绝处理二进制消息。它重载了 handleBinaryMessage() 方法,如果收到二进制消息的时候,将 会关闭 WebSocket 连接。与之类似,
BinaryWebSocketHandler 也是 AbstractWebSocketHandler 的子类,它重载了 handleTextMessage() 方法,如果接收到文本消息的话,将会关闭连接。
所以在本次代码中,直接选择扩展TextWebSocketHandler,并根据需求重载方法,如下所示
@Component
@Log4j2
public class ChatHandler extends TextWebSocketHandler {
/**
* socket 建立成功事件
*
* @param session session 对每个连接的抽象,每个连接都有自己唯一的Session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("连接成功");
}
/**
* 接收到消息
*
* @param session 对每个连接的抽象,每个连接都有自己唯一的Session
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info(payload);
// 对该连接进行回复
session.sendMessage(new TextMessage("服务器收到:" +payload));
}
}
WebSocketSession是每一个连接独有一个session对象,可以通过该对象来发送消息
对于WebSocketSession的具体方法可以查看WebSocketSession的Api
WebSocketConfigurer配置类
实现完了WebSocketHandler以后,还要让Springboot启动WebSockert端口,
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
/**
* WebSocket主程序
*/
@Autowired
private TextWebSocketHandler chatHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 设置chatHandler的地址
registry.addHandler(chatHandler,"chat");
}
}
只要做了这两步,即可启动项目 ,客户端连接ws://127.0.0.1:8080/chat 即可
先创建一个前端代码
<head>
<meta charset="UTF-8">
<script>
var path = "ws://127.0.0.1:28081/chat"
// 实例化socket
var socket = new WebSocket(path);
// 监听socket连接
socket.onopen = function() {
console.log("连接成功")
};
// 监听socket消息
socket.onmessage = function(msg) {
console.log(msg)
};
function sendhello() {
console.log("发送了你好")
socket.send("你好")
};
</script>
</head>
<body>
<button onclick="sendhello()">发送你好</button>
</body>
</html>
如下图所示,点击发送你好,便会收到服务器的消息内容
HandshakeInterceptor拦截器
上面的WebSocket服务端启动后可以发现一个问题,那就是所有人都能连接到websocket,又由于长连接相比短连接而言比较耗费资源,所以这时候需要使用HandshakeInterceptor拦截器,在连接建立之前进行鉴权
HandshakeInterceptor是WebSocket握手请求的拦截器。可用于检查握手请求和响应,以及将属性传递给目标WebSocketHandler。与Springboot的HTTP拦截器HandlerInterceptor类似
@Log4j2
@Component
public class ChatInterceptor implements HandshakeInterceptor {
/**
* 判断的标志位,是进行握手(ture)还是拒绝(false)
*/
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
String query = serverHttpRequest.getURI().getQuery();
if (query != null){
// 对url字符串进行解码成utf8格式
String decode = URLDecoder.decode(query, "utf-8");
// 将字符串转为map格式
Map<String, String> queryMap = UrlStringUtils.queryUrlSting(decode);
// 判断是否名字带有张三
if ("张三".equals(queryMap.get("name"))){
log.info("张三来连接了");
return true;
}
}
log.info("名字错误了");
return false;
}
/**
* 握手完成后调用。响应状态和标头指示握手的结果,即是否成功。
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler 目标WebSocket处理程序
* @param e 握手期间引发的异常;如果没有异常,则返回null
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
启动项目后并打开上面网页
可以发现控制台报错200,并且在服务端日志中也能发现打印了名字错误。
修改Html里面的地址信息
var path = "ws://127.0.0.1:28081/chat?name=张三"
跟http链接携带param参数一致
成功结果如下
WebSocket客户端
有了服务端自然要有客户端,再这里我们讨论python和JavaScript的客户端
Python客户端
在Python中如果想要使用WebSocket,需要导入websocket包
pip install websockets
python代码如下
import websocket
from urllib.parse import quote
import string
def on_message(ws,message):
print("收到消息:"+ message)
def on_error(ws, error):
print("发生错误")
def on_close(ws):
print("关闭连接,3秒后重新连接")
def on_open(ws):
print("握手成功")
ws.send("你好呀")
if __name__ == "__main__":
websocket.enableTrace(True)
# 将URL地址转化为指定格式
url = "ws://127.0.0.1:28081/chat?name=张三"
url = quote(url,safe = string.printable)
# 远程连接开始
ws = websocket.WebSocketApp(url,
on_message = on_message,
on_error = on_error,
on_close = on_close)
ws.on_open = on_open
ws.run_forever(ping_interval=60,ping_timeout=50)
这部分和服务端WebSocketHandler差不多有四个函数,分别是:
on_open()
握手成功后调用的方法。对应afterConnectionEstablished()
on_message()
收到消息后调用的方法,对应handleMessage()
*on_error()
发生错误调用的方法,对应handleTransportError()
on_close()
发生连接关闭以后调用的方法,对应afterConnectionClosed()
ws.run_forever(ping_interval=60,ping_timeout=50)
这条代码直接开始访问websocket服务端
ping_interval
设置心跳间隔时间ping_timeout
,设置发送ping到收到pong的超时时间
值得一提的是在python中WebSocketApp的url地址是没有转换成ISO编码格式的,所以遇到地址带有中文的时候需要将编码格式转为指定格式,这里和爬虫程序一样,可以使用urllib进行转换quote(url,safe = string.printable)
运行后如下图所示
python的WebSocketApp还支持修改请求头
header ={"名字":"张三","籍贯":"未知"}
# 远程连接开始
ws = websocket.WebSocketApp(url,
header=header,
on_message = on_message,
on_error = on_error,
on_close = on_close)
可以发现,请求头中携带了header这个字典
JavaScript客户端
<head>
<meta charset="UTF-8">
<script>
var path = "ws://127.0.0.1:28081/chat"
// 实例化socket
var socket = new WebSocket(path);
// 监听socket连接
socket.onopen = function() {
console.log("连接成功")
};
// 监听socket消息
socket.onmessage = function(msg) {
console.log(msg)
};
function sendhello() {
console.log("发送了你好")
socket.send("你好")
};
</script>
</head>
<body>
<button onclick="sendhello()">发送你好</button>
</body>
</html>
webSocket由于原本就是HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。所以在html中非常好实现,直接new WebSocket(path)
即可创建一个webSocket长连接。
具体方法也跟python客户端一致
onopen()
握手成功后调用的方法。对应afterConnectionEstablished()
onmessage()
收到消息后调用的方法,对应handleMessage()
*onerror()
发生错误调用的方法,对应handleTransportError()
onclose()
发生连接关闭以后调用的方法,对应afterConnectionClosed()
readyState()
返回当前连接状态
需要注意的是,由于浏览器限制,js的webSocket是不允许修改请求头的,但是可以通过protocols来与服务器约定好子协议
new WebSocket(url[, protocols\])