其他分享
首页 > 其他分享> > 一文详解 WebSocket 网络协议

一文详解 WebSocket 网络协议

作者:互联网

WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议。WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据(从而解决Http 1.1协议实现中客户端只能通过轮询方式获取服务端推送数据造成的资源消耗和消息延时等问题)。
WebSocket 协议诞生于2008年6月,并在2011年12月成为 RFC6455
https://www.rfc-editor.org/rfc/rfc6455.txt 国际标准,诞生之初被应用于HTML5相关规范中(移动互联网时代,也多应用于服务端向客户端推送消息的场景)。
WebSocket协议定义中客户端和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。而且WebSocket 握手阶段采用的是HTTP 协议;完成协议握手后,后续的数据通信采用WebSocket数据格式通信。

Http对比WebSocket

这篇文章,我们按照如下顺序,学习一下 WebSocket 全向数据传输协议:

一、诞生

早期的很多网站为具备数据推送能力,所在用的技术基本都是HTTP轮询
轮询是由由客户端每隔一段时间(如每隔5s)向服务器发出HTTP请求,服务端接收到请求后向客户端返回最新的数据。

客户端的轮询方式一般为短轮询长轮询

Http短轮序、长轮序

以上两种轮询方式也带来了很明显的缺点

因此,工程师们一直在思考,有没有更好的方法,可以减少资源的消耗,同时提高客户端的用户体验 !,在以上情况下 WebSocket 孕育而生。

二、简介

WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议。WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据
WebSocket 协议诞生于2008年6月,并在2011年12月成为 RFC6455
https://www.rfc-editor.org/rfc/rfc6455.txt 国际标准,诞生之初被应用于HTML5相关规范中(移动互联网时代,也多应用于服务端向客户端推送消息的场景)。
WebSocket协议定义中客户端和服务端只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

2.1 协议特点

2.2 协议标识

WebSocket 的协议标识符是ws(如果Over SSL,则为wss):

ws://example.com:80/some/path
wss://example.com:443/some/path

三、使用举例

对于客户端 WebSocket 的使用方面:OkHttp框架为我们提供了较好的使用封装

/**
 * WebSocketAgent
 *
 * @author https://blog.csdn.net/xiaxl
 */
public class WebSocketAgent {
    //
    private static final String TAG = "WebSocketAgent";
    // OkHttpClient
    private OkHttpClient mOkHttpClient;
    // WebSocket
    private WebSocket mWebSocket;
    private WebSocketCallback mWebSocketCallback;
    public WebSocketAgent() {
        mOkHttpClient = new OkHttpClient.Builder()
                .writeTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .connectTimeout(30, TimeUnit.SECONDS)
                // 每隔 30s 客户端主动发送ping保活消息
                .pingInterval(30, TimeUnit.SECONDS)
                .build();
        Log.d(TAG, "newWebSocket connection");
    }
    public void connect() {
        //建立连接
        Request request = new Request.Builder()
                .url(AgentConstant.WEBSOCKET_APP_URL)
                .build();
        mWebSocket = mOkHttpClient.newWebSocket(request, new OkHttpWebSocketListener());
    }
    public void sendMessage(String message) {
        Log.d(TAG, "sendMessage text: " + message);
        if (mWebSocket != null) {
            mWebSocket.send(message);
        } else {
            Log.e(TAG, "mWebSocket is null, please call connect first.", null);
        }
    }
    public void sendMessage(byte... data) {
        Log.d(TAG, "sendMessage byte: " + data);
        if (mWebSocket != null) {
            ByteString bs = ByteString.of(data);
            mWebSocket.send(bs);
        } else {
            Log.e(TAG, "mWebSocket is null, please call connect first.", null);
        }
    }
    public void close(int code, String reason) {
        if (mWebSocket != null) {
            mWebSocket.close(code, reason);
        } else {
            Log.e(TAG, "mWebSocket is null, please call connect first.", null);
        }
    }
    public void setWebSocketCallback(WebSocketCallback callback) {
        mWebSocketCallback = callback;
    }
    public class OkHttpWebSocketListener extends WebSocketListener {
        @Override
        public void onOpen(WebSocket webSocket, Response response) {
        	// 注意:这里回到的是异步线程
            if (response.code() == 101) {
                //  连接成功
            }
        }
        // 当收到文本(类型{@code 0x1})消息时调用
        @Override
        public void onMessage(WebSocket webSocket, String text) {
        	// 注意:这里回到的是异步线程
            if (mWebSocketCallback != null) {
                mWebSocketCallback.onWebSocketMessage(text);
            }
        }
        // 当收到二进制(类型为{@code 0x2})消息时调用。
        @Override
        public void onMessage(WebSocket webSocket, ByteString bytes) {
        	// 注意:这里回到的是异步线程
        }
        // 当远程对等体指示不再有传入的消息将被传输时调用。
        @Override
        public void onClosing(WebSocket webSocket, int code, String reason) {
        	// 注意:这里回到的是异步线程
            if (mWebSocketCallback != null) {
                mWebSocketCallback.onWebSocketClosing(code, reason);
            }
        }
        // 当两个对等方都表示不再传输消息并且连接已成功释放时调用。 没有进一步的电话给这位听众。
        @Override
        public void onClosed(WebSocket webSocket, int code, String reason) {
        	// 注意:这里回到的是异步线程
            if (mWebSocketCallback != null) {
                mWebSocketCallback.onWebSocketClosed(code, reason);
            }
        }
        // 由于从网络读取或向网络写入错误而关闭Web套接字时调用。
        @Override
        public void onFailure(WebSocket webSocket, Throwable t, Response response) {
        	// 注意:这里回到的是异步线程
            if (mWebSocketCallback != null) {
                mWebSocketCallback.onWebSocketError(t, response);
            }
            // TODO 切换到主线程中后,考虑一下断开连接后的重试逻辑
        }
    }
    public abstract static class WebSocketCallback {
        public void onWebsocketConnected() {}
        public void onWebSocketMessage(String text) {}
        public void onWebSocketClosing(int code, String reason) {}
        public void onWebSocketClosed(int code, String reason) {}
        public void onWebSocketError(Throwable t, Response response) {}
    }
}

四、协议握手

WebSocket 协议握手复用了HTTP协议:

完成协议握手后,后续的数据交换则遵照 WebSocket 的协议进行。

4.1 握手请求

客户端通过 HTTP/1.1 协议 GET 请求发起协议升级请求:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

请求Header头域字段说明:

Wireshark抓包如下:

WebSocket握手请求

4.2 握手响应

服务端通过HTTP Response 中 101状态码 返回响应数据,完成协议握手:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

响应数据说明:

注:每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n

Wireshark抓包如下:

Wireshark握手响应数据

4.3 Sec-WebSocket-Accept计算

Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。

计算公式为:

// 伪代码举例
Base64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )

标准文档中关于计算方式的表述:
Sec-WebSocket-Accept计算

五、数据帧格式

在完成协议握手后,后续客户端与服务端数据交换均需要遵循 WebSocket 协议进行。
这里,我用 Wireshark 抓包了一个服务端推送到客户端的消息数据,可以让大家先对 Wireshark 数据格式有一个大概的认识。

WebSocket服务端推送到客户端的数据

WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。

RFC6455 定义了 WebSocket数据帧的统一格式。如下图所示,单位为比特,从左到右FINRSV等各占据1比特位,opcode 占据4比特。

RFC6455中WebSocket数据格式定义

Opcode 含义
0x0 表示一个延续帧,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片
0x1 表示这是一个文本帧(frame)
0x2 表示这是一个二进制帧(frame)
0x3-7 保留的操作代码,用于后续定义的非控制帧
0x8 表示连接断开
0x9 表示这是一个ping操作
0xA 表示这是一个pong操作
0xB-F 保留的操作代码,用于后续定义的控制帧

WebSocket数据帧举例

WebSocket客户端向服务端发送的数据抓包举例如下:
WebSocket客户端向服务端发送的数据

WebSocket服务端向客户端推送的数据抓包举例如下:
WebSocket服务端向客户端推送的数据

六、关闭连接

上边说道了发送消息时,WebSocket数据帧格式。
当WebSocket不再需要时,客户端或服务端可以选择关闭 WebSocket 连接。

WebSocket客户端关闭连接抓包如下:
WebSocket客户端关闭连接

WebSocket服务端关闭连接抓包如下:
WebSocket服务端关闭连接

可以看到以上抓包数据中,涉及到一个状态码Status code

连接关闭状态码 含义
1000 正常关闭连接
1001 表示某个端“正在离开”,例如服务器关闭或客户端已离开页面
1002 websocket协议错误
1003 正在关闭连接,某个端接受了不支持数据格式
1004~1006 保留字段
1007 正在关闭连接,某个端接受了无效数据格式(文本消息编码不是utf-8)
1008 正在关闭连接,某个端接收到了违反政策的消息
1009 正在关闭连接,传输的数据量过大
1010 表示客户端正在关闭连接,因为它期望服务器协商一个或多个扩展,但服务器没有做出响应
1011 表示服务器正在关闭连接,因为它遇到了意外情况

七、心跳消息

WebSocket 是客户端与服务端的长链接,需要间隔一段发送Ping、Pong心跳,以抵挡运营商的Nat超时,来维持TCP连接不断开。

客户端发送的Ping消息抓包举例如下:
客户端发送的Ping消息

服务端发送的Pong消息抓包举例如下:
服务端发送的Pong消息

八、参考:

RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txt

RFC7936: WebSocket补充
https://www.rfc-editor.org/rfc/rfc7936.txt

维基百科:WebSocket
https://zh.m.wikipedia.org/zh-hans/WebSocket

阮一峰: WebSocket教程
https://www.ruanyifeng.com/blog/2017/05/websocket.html

WebSocket协议:
https://www.cnblogs.com/chyingp/p/websocket-deep-in.html

= THE END =

文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,欢迎关注我的公众号。
欢迎关注我的公众号

标签:协议,WebSocket,网络协议,详解,数据,public,服务端,客户端
来源: https://www.cnblogs.com/xiaxveliang/p/16292433.html