WebSocket的简单应用

WebSocket的简单应用

Scroll Down

使用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 ChromeFirefoxSafariMicrosoft EdgeInternet ExplorerOpera

与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>

如下图所示,点击发送你好,便会收到服务器的消息内容

image-20201012171040741

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) {
    }
}

启动项目后并打开上面网页

image-20201013100749862

image-20201013100826742

可以发现控制台报错200,并且在服务端日志中也能发现打印了名字错误。

修改Html里面的地址信息

var path = "ws://127.0.0.1:28081/chat?name=张三"

跟http链接携带param参数一致

成功结果如下

image-20201013124858958

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)

运行后如下图所示

image-20201014001037294

python的WebSocketApp还支持修改请求头

header  ={"名字":"张三","籍贯":"未知"}
# 远程连接开始
ws = websocket.WebSocketApp(url,
	header=header,
	on_message = on_message,
	on_error = on_error,
    on_close = on_close)

image-20201014002808731

可以发现,请求头中携带了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\])