WebSocket 通讯过程与实现

什么是 WebSocket ?

WebSocket 是一种标准协议,用于在客户端和服务端之间进行双向数据传输。但它跟 HTTP 没什么关系,它是基于 TCP 的一种独立实现。javascript

之前客户端想知道服务端的处理进度,要不停地使用 Ajax 进行轮询,让浏览器隔个几秒就向服务器发一次请求,这对服务器压力较大。另一种轮询就是采用 long poll 的方式,这就跟打电话差很少,没收到消息就一直不挂电话,也就是说,客户端发起链接后,若是没消息,就一直不返回 Response 给客户端,链接阶段一直是阻塞的。html

而 WebSocket 解决了 HTTP 的这几个难题。当服务器完成协议升级后( HTTP -> WebSocket ),服务端能够主动推送信息给客户端,解决了轮询形成的同步延迟问题。因为 WebSocket 只须要一次 HTTP 握手,服务端就能一直与客户端保持通讯,直到关闭链接,这样就解决了服务器须要反复解析 HTTP 协议,减小了资源的开销。前端

websockets

随着新标准的推动,WebSocket 已经比较成熟了,而且各个主流浏览器对 WebSocket 的支持状况比较好(不兼容低版本 IE,IE 10 如下),有空能够看看。java

浏览器兼容性

使用 WebSocket 的时候,前端使用是比较规范的,js 支持 ws 协议,感受相似于一个轻度封装的 Socket 协议,只是之前须要本身维护 Socket 的链接,如今可以以比较标准的方法来进行。python

WebSocket 通讯过程

下面咱们就结合上图具体来聊一下 WebSocket 的通讯过程。web

创建链接

客户端请求报文 Header

客户端请求报文:算法

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

与传统 HTTP 报文不一样的地方:浏览器

Upgrade: websocket
Connection: Upgrade

这两行表示发起的是 WebSocket 协议。缓存

Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防御,防止恶意或者无心的链接。安全

Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不一样厂商都有本身的协议版本,不过如今已经定下来了。若是服务端不支持该版本,须要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

建立 WebSocket 对象:

var ws = new websocket("ws://127.0.0.1:8001");

ws 表示使用 WebSocket 协议,后面接地址及端口

完整的客户端代码:

<script type="text/javascript">
    var ws;
    var box = document.getElementById('box');

    function startWS() {
        ws = new WebSocket('ws://127.0.0.1:8001');
        ws.onopen = function (msg) {
            console.log('WebSocket opened!');
        };
        ws.onmessage = function (message) {
            console.log('receive message: ' + message.data);
            box.insertAdjacentHTML('beforeend', '<p>' + message.data + '</p>');
        };
        ws.onerror = function (error) {
            console.log('Error: ' + error.name + error.number);
        };
        ws.onclose = function () {
            console.log('WebSocket closed!');
        };
    }

    function sendMessage() {
        console.log('Sending a message...');
        var text = document.getElementById('text');
        ws.send(text.value);
    }

    window.onbeforeunload = function () {
        ws.onclose = function () {};  // 首先关闭 WebSocket
        ws.close()
    };
</script>

服务端响应报文 Header

首先咱们来看看服务端的响应报文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

咱们一行行来解释

  1. 首先,101 状态码表示服务器已经理解了客户端的请求,并将经过 Upgrade 消息头通知客户端采用不一样的协议来完成这个请求;
  2. 而后,Sec-WebSocket-Accept 这个则是通过服务器确认,而且加密事后的 Sec-WebSocket-Key
  3. 最后,Sec-WebSocket-Protocol 则是表示最终使用的协议。

Sec-WebSocket-Accept 的计算方法:

  1. Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  2. 经过 SHA1 计算出摘要,并转成 base64 字符串。

注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但链接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并无实际性的保证。

建立主线程,用于实现接受 WebSocket 创建请求:

def create_socket():
    # 启动 Socket 并监听链接
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.bind(('127.0.0.1', 8001))

        # 操做系统会在服务器 Socket 被关闭或服务器进程终止后立刻释放该服务器的端口,不然操做系统会保留几分钟该端口。
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.listen(5)
    except Exception as e:
        logging.error(e)
        return
    else:
        logging.info('Server running...')

    # 等待访问
    while True:
        conn, addr = sock.accept()  # 此时会进入 waiting 状态

        data = str(conn.recv(1024))
        logging.debug(data)

        header_dict = {}
        header, _ = data.split(r'\r\n\r\n', 1)
        for line in header.split(r'\r\n')[1:]:
            key, val = line.split(': ', 1)
            header_dict[key] = val

        if 'Sec-WebSocket-Key' not in header_dict:
            logging.error('This socket is not websocket, client close.')
            conn.close()
            return

        magic_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
        sec_key = header_dict['Sec-WebSocket-Key'] + magic_key
        key = base64.b64encode(hashlib.sha1(bytes(sec_key, encoding='utf-8')).digest())
        key_str = str(key)[2:30]
        logging.debug(key_str)

        response = 'HTTP/1.1 101 Switching Protocols\r\n' \
                   'Connection: Upgrade\r\n' \
                   'Upgrade: websocket\r\n' \
                   'Sec-WebSocket-Accept: {0}\r\n' \
                   'WebSocket-Protocol: chat\r\n\r\n'.format(key_str)
        conn.send(bytes(response, encoding='utf-8'))

        logging.debug('Send the handshake data')

        WebSocketThread(conn).start()

进行通讯

服务端解析 WebSocket 报文

Server 端接收到 Client 发来的报文须要进行解析

Client 包格式

Client 包格式

  1. FIN: 占 1bit

    0:不是消息的最后一个分片
    1:是消息的最后一个分片
  2. RSV1, RSV2, RSV3:各占 1bit

    通常状况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位能够非
    0,且值的含义由扩展进行定义。若是出现非零的值,且并无采用 WebSocket 扩展,链接出错。
  3. Opcode: 4bit

    %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;
    %x1:表示这是一个文本帧(text frame);
    %x2:表示这是一个二进制帧(binary frame);
    %x3-7:保留的操做代码,用于后续定义的非控制帧;
    %x8:表示链接断开;
    %x9:表示这是一个心跳请求(ping);
    %xA:表示这是一个心跳响应(pong);
    %xB-F:保留的操做代码,用于后续定义的控制帧。
  4. Mask: 1bit

    表示是否要对数据载荷进行掩码异或操做。
    0:否
    1:是
  5. Payload length: 7bit or (7 + 16)bit or (7 + 64)bit

    表示数据载荷的长度
    0~126:数据的长度等于该值;
    126:后续 2 个字节表明一个 16 位的无符号整数,该无符号整数的值为数据的长度;
    127:后续 8 个字节表明一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。
  6. Masking-key: 0 or 4bytes

    当 Mask 为 1,则携带了 4 字节的 Masking-key;
    当 Mask 为 0,则没有 Masking-key。
    掩码算法:按位作循环异或运算,先对该位的索引取模来得到 Masking-key 中对应的值 x,而后对该位与 x 作异或,从而获得真实的 byte 数据。
    注意:掩码的做用并非为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
  7. Payload Data: 载荷数据

解析 WebSocket 报文代码以下:

def read_msg(data):
    logging.debug(data)

    msg_len = data[1] & 127  # 数据载荷的长度
    if msg_len == 126:
        mask = data[4:8]  # Mask 掩码
        content = data[8:]  # 消息内容
    elif msg_len == 127:
        mask = data[10:14]
        content = data[14:]
    else:
        mask = data[2:6]
        content = data[6:]

    raw_str = ''  # 解码后的内容
    for i, d in enumerate(content):
        raw_str += chr(d ^ mask[i % 4])
    return raw_str

服务端发送 WebSocket 报文

返回时不携带掩码,因此 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。

struct 模块解析

struct.pack(fmt, v1, v2, ...)

按照给定的格式 fmt,把数据封装成字符串 ( 其实是相似于 C 结构体的字节流 )

struct 中支持的格式以下表:

Format C Type Python type Standard size
x pad byte no value
c char bytes of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I unsigned int integer 4
l long integer 4
L unsigned long integer 4
q long long integer 8
Q unsigned long long integer 8
n ssize_t integer
N size_t integer
e -7 float 2
f float float 4
d double float 8
s char[] bytes
p char[] bytes
P void * integer
为了同 C 语言中的结构体交换数据,还要考虑有的 C 或 C++ 编译器使用了字节对齐,一般是以 4 个字节为单位的 32 位系统,故而 struct 根据本地机器字节顺序转换。能够用格式中的第一个字符来改变对齐方式,定义以下:
Character Byte order Size Alignment
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

发送 WebSocket 报文代码以下:

def write_msg(message):
    data = struct.pack('B', 129)  # 写入第一个字节,10000001

    # 写入包长度
    msg_len = len(message)
    if msg_len <= 125:
        data += struct.pack('B', msg_len)
    elif msg_len <= (2 ** 16 - 1):
        data += struct.pack('!BH', 126, msg_len)
    elif msg_len <= (2 ** 64 - 1):
        data += struct.pack('!BQ', 127, msg_len)
    else:
        logging.error('Message is too long!')
        return

    data += bytes(message, encoding='utf-8')  # 写入消息内容
    logging.debug(data)
    return data

总结

没有其余能像 WebSocket 同样实现全双工传输的技术了,迄今为止,大部分开发者仍是使用 Ajax 轮询来实现,但这是个不太优雅的解决办法,WebSocket 虽然用的人很少,多是由于协议刚出来的时候有安全性的问题以及兼容的浏览器比较少,但如今都有解决。若是你有这些需求能够考虑使用 WebSocket:

  1. 多个用户之间进行交互;
  2. 须要频繁地向服务端请求更新数据。

好比弹幕、消息订阅、多玩家游戏、协同编辑、股票基金实时报价、视频会议、在线教育等须要高实时的场景。

参考文章

https://www.zhihu.com/questio...

http://fullstackpython.atjian...

http://www.52im.net/thread-13...

相关文章
相关标签/搜索