WebSocket设计指南
这里主要对常用 TCP 和 WebSocket 做对比, 确认两者的网络传输之间的细节从而在开发当中规避一些可能存在的问题.
WebSocket 是基于 HTTP 协议, 而 HTTP 协议也是基于底层 TCP 架构开发,
WebSocket 为了弥补 HTTP 的 无状态 特性采取的 双向通讯 协议.
为什么采用 WebSocket 而不是更加底层且效率更好的 TCP?
主要来源于 Web和HTML5 应用的飞速发展, 传统 C/S端 逐渐被 B/S端 赶上,
而 Web 界面上面开始对长链接有所需求, 从而需要在网页应用能够做到简单的 Socket 网络请求.
特别是对于网页游戏来说, 需要专门的网页长链接协议方便做数据交换, 这里还需要说明下几种协议差别:
UDP: 基于报文(Packed)传输消息, 每个报文都是独立的数据内含源地址|目的地址|端口号以及数据等信息, 不保证消息有序性TCP: 基于流(Stream)传输消息, 传输数据无边界的字节流需要对流进行消息分包, 已经内部确保消息有序性WebSocket: 基于消息(Message)传输消息, 每个消息都有明确的边界无须在此消息分包
这三种协议性能上从高到底比较来说是: UDP > TCP > WebSocket.
虽然 WebSocket 是性能相对来说较低, 但是在日常使用中有其优点:
多平台支持: 基本上支持HTML5的地方都能使用WebSocket支持, 连带网页本身默认自带的支持连接转发来源: 一般大规模项目都会用高防服务器做数据转发,TCP|UDP可能在反向代理转发中丢失客户端来源IP无须消息分包: 基于TCP保证消息有序性, 并且在此基础上基于消息默认保证消息包的大小, 无须去编写分包逻辑简单化:WebSocket的底层已经包装得足够简单, 没有太多复杂的特性从而方便服务端开发
当时 WebSocket 也有其弱势的地方:
性能: 只要和TCP|UDP对比永远都无法绕过的问题, 如果传输实时性要求高可能对于WebSocket就不太满足灵活性:TCP|UDP衍生出大量不同协议(ssh|telent等), 如果要在WebSocket这些协议可能需要再手动实现
所以对于延迟性要求不高的场景, 可以考虑直接 WebSocket
WebSocket 基于消息
WebSocket 的握手流程:
[ 1.握手阶段 ]
[客户端] ---> [申请连接http服务器]
|
| ---> [服务器判断时候支持, 如果支持返回 101 Switching Protocols 状态]
|
[客户端] <--- [101 Switching Protocols]- |
[ 2.升级协议 ]
[客户端] ---> [申请连接http服务器]
|
| ---> [请求头中会包含 "Upgrade: websocket" 和 "Connection: Upgrade" 表明协议升级]
[要求请求头含有 "Sec-WebSocket-Key: xxxxxxxxx" Base64随机字符串用于消息验证]
|
|
[要求服务器响应头则包含 "Sec-WebSocket-Accept: yyyyyy", 其值是根据客户端发送的 "Sec-WebSocket-Key" 得出的哈希值]
|
[客户端] <--- 握手完成允许连接并响应头 `Sec-WebSocket-Accept`
这里就是 WebSocket 的具体握手流程, 因为基于 HTTP 所以在请求过程会涉及到将 HTTP 升级到 WebSocket,
可能最后的消息内容如下:
# 以下为客户端请求内容
GET /game HTTP/1.1
Host: game.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x34xlIxed4x3ZSBvs35xQ==
Sec-WebSocket-Version: 13
# 以下为服务端响应内容
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: sxv34Txa3VsdFfsshZxvR+xOo=
握手之后就是具体的消息传输过程, 而 WebSocket 基于 Message 也就说明消息内部已经分好 数据帧(frame):
[ 头部(Head) ] [ 内容(Payload) ]
[ FIN ][ RSV1 ][ RSV2 ][ RSV3 ][ Opcode ][ Mask ][ PayloadLength ][ MaskingKey ] [ ...... ]
[ 1bit][ 1bit ][ 1bit ][ 1bit ][ 4bit ][ 1bit ][(7/7+16/7+64)bit][ 32bit ] [ ...... ]
FIN: 标识时候为消息最后的分片RSV1~RSV3: 协议预留的占位符Opcode: 数据传输类型(文本为0x1, 二进制为0x2, 关闭连接为0x8, Ping为0x9, Pong为0xA)Mask: 为了防止缓存污染攻击必须让客户端发送的数据追加个掩码字段确保消息安全, 服务端则不需要PayloadLength: 帧内容的长度也就是整个消息包的最终长度, 支持最大2^64-1字节MaskingKey: 掩码密钥, 由上面的标志位Mask决定, 如果使用掩码就是4byte = 32bit的随机数Payload: 客户端传输过来的最终数据
因为 WebSocket 基于 HTTP, 在连接过程之中会有 协议升级(Upgrade) 的请求;
而有些服务端为了性能考虑会将某些静态网页内容做周期缓存,
通过构建包含恶意请求内容来欺骗这次 WebSocket 识别成 HTTP 请求从而被服务端构建成页面缓存,
如果通过其他用户访问来触发展示恶意内容的缓存页面, 这种就是 缓存污染攻击.
可以看到 WebSocket 已经高度集成了消息包常用的 长度声明|完整哈希|内容类型 等所有用到的功能,
并且基于 HTTP 协议能够充分利用其数据消息压缩功能, 这里 Nginx 就可以直接采用消息压缩:
http {
# 常用压缩协议 DEFLATE 和 GZIP, 其他压缩协议可以参照 NGINX 文档处理.
gzip on; # 开启gzip压缩
gzip_proxied any; # 对代理请求也进行压缩
gzip_types *; # 压缩所有类型的数据
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend_server;
proxy_http_version 1.1;
# 升级协议, 反向代理需要配置转发头
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
}
具体的传输结构可以参照 rfc6455 来进行学习.
那么按照上面所说, 对于 WebSocket 我们完全可以不需要和 TCP 对消息搞成 head+body 形式,
而是直接检索出数据即可, 因为内部传输的底层已经把消息包装成 数据帧 了.
其他底层消息需要在每条消息加上 int32 字节位来标识后面消息长度, 从而实现动态分配消息缓冲区填充, WebSocket在底层已完成支持
从我个人角度来看, WebSocket 为服务端低门槛开发提供不可或缺的助力,
高度封装的底层让开发者能够更加集中于业务实现而不是纠结于底层安全和可靠性,
总体来说我十分喜欢 WebSocket 这门技术也希望后续其发展能够越来越好.