websocket 维基百科介绍
WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455「https://tools.ietf.org/html/rfc6455」,后由RFC 7936「https://tools.ietf.org/html/rfc7936」补充规范。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
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提供全双工通信。此外,WebSocket还可以在TCP之上实现消息流。TCP单独处理字节流,没有固有的消息概念。 在WebSocket之前,使用Comet可以实现全双工通信。但是Comet存在TCP握手和HTTP头的开销,因此对于小消息来说效率很低。WebSocket协议旨在解决这些问题。
WebSocket协议规范将ws(WebSocket)和wss(WebSocket Secure)定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。除了方案名称和片段ID(不支持#)之外,其余的URI组件都被定义为此URI的通用语法。
websocket 背景
早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间(如每秒)向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求与回复可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。
比较新的轮询技术是Comet。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在Comet中普遍采用的HTTP长连接也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
Websocket使用ws或wss的统一资源标志符(URI)。其中wss表示使用了TLS的Websocket。如:
ws://example.com/wsapi
wss://secure.example.com/wsapi
Websocket与HTTP和HTTPS使用相同的TCP端口,可以绕过大多数防火墙的限制。默认情况下,Websocket协议使用80端口;运行在TLS之上时,默认使用443端口。
websocket 优点
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
websocket 请求应答示例
一个典型的Websocket握手请求如下:
客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务器回应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
websocket 字段说明
- Connection必须设置Upgrade,表示客户端希望连接升级。
- Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
- Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行Base64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
- Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
- Origin字段是必须的。如果缺少origin字段,WebSocket服务器需要回复HTTP 403 状态码(禁止访问)。
- 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。
websocket 数据协议分析
协议官方文档地址:https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
FIN 位,也是整个片段的第一个字节的最高位,他只能是 0 或者是 1,这个位的作用只有一个,如果它为 1,表示这个片段是整个消息的最后一个片段,如果是 0,表示这个片段之后,还有其它的片段。RSV1,RSV2,RSV3
这三位是保留给扩展使用的,基本不会用到,反正我没用到,所以我们可以把它们当做空气就行,永远设置为 0,就是这么果断。
websocket opcode
opcode 顾名思义就是操作码,占用第一个字节的低四位,所以 opcode 可以代表 16 种不同的值。你是不是想问,opcode 是用来干嘛的?
opcode 是用 来解析当前片段的载荷(携带的数据)的,具体的后面会再次说明。
0x00,表示当前片段是连续片段,这是啥意思呢?还记得上面讨论 FIN 的时候,一条消息被分割成多条片段?如果当前片段不是第一个,那么 opcode 必须设置为 0。
0x01,表示当前片段所携带的数据是文本数据(记得最开始说的文本数据和二进制数据的区别??),如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
0x02,表示当前片段所携带的数据是二进制数据,如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
0x03-0x07,保留给将来使用,也就是说暂时还没用到。
0x08,表示关闭 websocket 连接,这个后面我会再一次讲到,先放着
0x09,发送 Ping 片段,说白了,它主要是用来检测远程端点是否还存活,我想检查我的对象是不是已经死了,但是这个片段可以携带数据,如果端点的一方发送了 Ping,那么接受方,必须返回 Pong 片段,用中国人的话来说,就是礼尚往来嘛。
0xA,发送 Pong,用以回复 Ping,是不是很简单?
0xB-F,保留给将来使用,也就是说暂时还没用到。
websocket MASK
表示当前片段所携带的数据是否经过加密,位置为第二个字节的最高位,总共 1 位,它的值不是你想设置就设置的啊,RFC6455 明确规定,所有从客户端发送给服务器的数据必须加密,所以 mask 的值必须是 1。还有,所有从服务器发往客户端的数据,一定不能加密,所以呢,mask 必须为 0,就是这么简单粗暴。
websocket Payload Length
这部分是用来定义负载数据的长度的,总共 7 位,所以最大值为 127,就这么简单?哼哼,不会的。
websocket 大于125字节 0x7e
payload_length<=125,此时数据的长度就是 payload_length 的大小。
payload_length=126,那么紧接着 payload_length 的 2 个字节,就用来表示数据的大小,所以当数据大小大于 125,小于 65535 的时候,payload_length 设置为 126,后面分析代码的时候,我会再次讲到。
payload_length=127,也就是 payload_length 取最大值,那么紧接着 payload_length 的 8 个字节,就用来表示数据的大小,此可以表示的数据可就相当大了,后面分析代码的时候,我会再次讲到。
websocket Mask key
它的位置紧接着数据长度的后面,大小为 0 或者是 4 个字节。前面分析了 mask 的作用,如果 mask 为 1 的话,数据需要加密,此时 mask key 占用 4 个字节,否则长度为 0,至于 mask key 如何用来解密数据的,后面会再次讲到。
websocket payload data
这里就是我们从客户端接收到的数据,不过它是经过加密的,“我是奥巴马”,之前 payload_length 的长度,就是经过加密之后的数据的长度,而不是原始数据的长度。
websocket 解析数据包
读取第二个字节的低 7 位,也就是之前讨论的 payload_length,0x7f 转换为二进制就是 01111111,当 payload_length 的长度小于 125 的话,数据长度就等于片段长度。当 payload_length 的长度等于 126 的时候,就有些麻烦了,此时第 3 和第 4 个字节组合为一个无符号 16 位整数,还记得我们之前说的,网络字节序吗?高位字节在前,低位字节在后面,所以当我们读的时候,第 3 个字节就是高 8 位,第 4 个字节就是低 8 位,所以我们首先将高 8 位左移 8 位再和低 8 位做或运算。当 payload_length 的长度等于 127 的时候,此时的第 3 到第 10 位组合为一个无符号 64 位整数,所以最高的 8 位需要左移 56 位,后面的依次类推,低 8 位保持不动。
websocket 解析 mask key
要找到 maskey,首先必须找到它在当前片段的偏移,如果 payload_length<=125,那么偏移就是 2,如果 payload_length==126,那么偏移就是 (2+2)=4,如果 payload_length>126,那么偏移就是(2+8)=10,同时 mask key 的大小为 4 个字节,所以找到了偏移和长度,mask key 就可以获取到了。
websocket 解密数据
解密数据的第一步就是要找到加密数据在当前片段中的偏移,很简单,这个值等于 maskkey 的偏移(上面已经求过了)+maskkey 本身的长度 4,那么怎么来解密数据呢?看上面的代码,就可以看出来,解密的过程其实就是遍历加密数据的每一个字符的 ASCII 值和数据(当前遍历的位置对 4 取模,得出的数据必定是 0,1,2,3,将得出的数据找到 maskkey 对应位置的 ASCII 值)进行异或运算求得,这个算法是 RFC6455 规定的,全世界都是这样。
websocket 掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
首先,假设:
original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
websocket Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
通过SHA1计算出摘要,并转成base64字符串。
伪代码如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
验证下前面的返回结果:
const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';
let secWebSocketAccept = crypto.createHash('sha1')
.update(secWebSocketKey + magic)
.digest('base64');
console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
websocket 接保持+心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)
ws.ping('', false, true);
websocket 数据掩码的作用
WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。
那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。
答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
from:https://www.cnblogs.com/chyingp/p/websocket-deep-in.html
websocket test websocket server
ws://echo.websocket.org/
js 使用websocket
在支持WebSocket的浏览器中,在创建socket之后。可以通过onopen,onmessage,onclose即onerror四个事件实现对socket进行响应
一个简单是示例:
var ws = new WebSocket(“ws://localhost:8080”);
ws.onopen = function()
{
console.log(“open”);
ws.send(“hello”);
};
ws.onmessage = function(evt) { console.log(evt.data); };
ws.onclose = function(evt) { console.log(“WebSocketClosed!”); };
ws.onerror = function(evt) { console.log(“WebSocketError!”); };
首先申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。
websocket 0x81 0x7e 0x7f
注意,从客户端发送到服务端的数据都被 异或加密(用一个32位的key)格式化。详情请参见规范的第5节。掩码明确告知我们消息是否经过格式化。从客户端来的消息必须经过格式化,所以你的服务器必须要求这个掩码是1(事实上,规范5.1节规定了如果客户端发送了没有格式化的消息,你的服务器应该断开连接)
当向客户端发送帧时,不要对其进行掩码,也不要设置掩码位。稍后我们将解释屏蔽。注意:即使使用安全套接字,也必须屏蔽消息。RSV1-3可以忽略,它们是用于扩展的。
操作码字段定义了如何解释有效负载数据:0x0表示延续,0x1表示文本(总是用UTF-8编码),0x2表示二进制,以及其他所谓的“控制代码”,稍后将对此进行讨论。在这个版本的WebSockets中,0x3到0x7和0xB到0xF没有任何意义。
FIN位告诉我们这是不是系列的最后一条消息。如果是0,那么服务器将继续侦听消息的更多部分;否则,服务器应该考虑传递的消息。不仅仅是这样。
解码有效载荷长度
要读取有效负载数据,您必须知道何时停止读取。这就是为什么有效载荷长度很重要。不幸的是,这有点复杂。要阅读它,请遵循以下步骤:
读取9-15(包括)位并将其解析为无符号整型。如果长度小于等于125,那么就是长度;你就完成了。如果是126,到第二步。如果是127,到步骤3。
读取下面的16位,并将其解释为无符号整型。你就完成了。
读取接下来的64位,并将其解释为无符号整型(最重要的位必须为0)。
from: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
websocket java代码实现
public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"WebSocket-Location: ws://127.0.0.1:9527\r\n";
ServerSocket serverSocket = new ServerSocket(7000);
while (true) {
Socket socket = serverSocket.accept();
// 开启一个新线程
Thread thread = new Thread(() -> {
// 响应握手信息
try {
// 读取请求头
byte[] bytes = new byte[10000000];
socket.getInputStream().read(bytes);
String requestHeaders = new String(bytes, StandardCharsets.UTF_8);
// 获取请求头中的
String webSocketKey = "";
for (String header : requestHeaders.split("\r\n")) {
if (header.startsWith("Sec-WebSocket-Key")) {
webSocketKey = header.split(":")[1].trim();
}
}
// 将webSocketKey 与 magicKey 拼接用sha1加密之后在进行base64编码
String value = webSocketKey + magicKey;
String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
// 写入返回头 握手结束 成功建立连接
String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
System.out.println("握手成功,成功建立连接");
}
}
}
from: https://segmentfault.com/a/1190000039890327
websocket debugging-websockets-with-curl
$ curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: echo.websocket.org" -H "Origin:http://www.websocket.org" http://echo.websocket.org
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.websocket.org
WebSocket-Location: ws://echo.websocket.org/
Server: Kaazing Gateway
Date: Mon, 11 Jun 2012 16:34:46 GMT
Access-Control-Allow-Origin: http://www.websocket.org
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Referenced from:https://www.thenerdary.net/post/24889968081/debugging-websockets-with-curl
websocket Base Framing Protocol
https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
websocket implementing-web-sockets-with-libcurl
#define concat(a,b) a b
handle = curl_easy_init();
// Add headers
header_list_ptr = curl_slist_append(NULL , "HTTP/1.1 101 WebSocket Protocol Handshake");
header_list_ptr = curl_slist_append(header_list_ptr , "Upgrade: WebSocket");
header_list_ptr = curl_slist_append(header_list_ptr , "Connection: Upgrade");
header_list_ptr = curl_slist_append(header_list_ptr , "Sec-WebSocket-Version: 13");
header_list_ptr = curl_slist_append(header_list_ptr , "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==");
curl_easy_setopt(handle, CURLOPT_URL, concat("http","://echo.websocket.org"));
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, header_list_ptr);
curl_easy_setopt(handle, CURLOPT_OPENSOCKETFUNCTION, my_opensocketfunc);
curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, my_func);
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, my_writefunc);
curl_easy_perform(handle);
Referenced from:https://phpandmore.net/2015/02/17/implementing-web-sockets-with-curl/
websocket 使用curl测试websocket服务
curl --include \
--no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: example.com:80" \
--header "Origin: http://example.com:80" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
http://example.com:80/
Referenced from:https://blog.csdn.net/sd2131512/article/details/74996577
Test a WebSocket using curl.
curl \
--include \
--no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: example.com:80" \
--header "Origin: http://example.com:80" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
http://example.com:80/
Referenced from:https://gist.github.com/htp/fbce19069187ec1cc486b594104f01d0