目录
1 什么是websocket
原理
特点
2 websocket的应用场景
3 websocket协议的解析分析
1websocket的协议格式
2 websocket如何验证客户端合法
3 明文和密文如何传输
4 websocket如何断开
4 自定义实现websocket的服务器的代码实现和关键代码展示
全部代码连接
5 一个疑问:既然客户端可以直接调用一个close来断开tcp的连接,为什么websocket还需要留出一个fin位来断开连接
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯
较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
主要应用于服务器主动的像客户端推送数据的情况下,当然不排除其他的情况也可以是用websocket协议来通讯
举个例子
eg:当我们通过(浏览器)网页端开始登陆CSDN账号的时候,有一种登陆的方式是通过微信扫码进行登陆
我们微信扫码后,手机微信会将扫码的到的信息发送到微信服务器,微信服务器会分析出接收到的信息是关于CSDN的信息,然后这个信息请求转发到CSDN的服务器上面,然后CSDN服务器会将一个CSDN登陆成功后的页面信息主动发送到浏览器上面.而CSDN服务器向浏览器主动发送信息D的这个过程就是采用的是websocet协议
握手的协议格式
http协议是无状态的,不支持持久(非持久化)连接的(长连接、轮询连接除外的话)
websocket是一个持久化的协议
http和websocket协议都是基于TCP/IP协议之上,websocket可以说是基于http协议的一个持久化协议。
websocket协议连接需要以http形式发起,三次握手告诉将http协议转换为websocket协议后,之后客户端和服务端就会开启持久化的TCP信道进行信息传输。
信息传输的协议格式
websocket客户端发送的请求消息
首先客户端在websocket的请求下消息中会有一个
websocket服务器收到客户端发送过来的websocket请求后将sec-websocket-key 后面的base64格式的字符串后面加上一个websocket公认的GUID字符串
GUID:
将GUID拼接在sec-websocket-key后面然后 进行sha1(哈希),将hash结果再base64编码生成一串base64编码的结果放入服务器 返回客户端的信当中进行返回,然后客户端将接收到的结果和自己的算的结果进行对比,如果一样则握手成功。
如果传输明文:的mask设置为0,payload data直接存放的是明文就可以了。makeing-key不会有值
如果传输的是密文:mask的标志位则会被设置成1,payload data会是密文,making-key有四个字节的值。用于密文的加密和解密。
加密过程
payload[i] = payload[i] ^masking-key[i%4]
解密过程(就是再异或一遍)
payload[i] = payload[i] ^masking-key[i%4]
直接将FIN位置1就可以,payload可以不用填数据。
websocket协议交互的状态机和opcode内容和部分协议内容
enum {
WS_HANDSHARK = 0,
WS_TRANMISSION = 1,
WS_END = 2,
};
typedef struct _ws_ophdr {
unsigned char opcode:4,
rsv3:1,
rsv2:1,
rsv1:1,
fin:1;
unsigned char pl_len:7,
mask:1;
} ws_ophdr;
typedef struct _ws_head_126 {
unsigned short payload_length;
char mask_key[4];
} ws_head_126;
typedef struct _ws_head_127 {
long long payload_length;
char mask_key[4];
} ws_head_127;
握手的过程实现
int handshark(struct ntyevent *ev) {
//ev->buffer , ev->length
char linebuf[1024] = {0};
int idx = 0;
char sec_data[128] = {0};
char sec_accept[32] = {0};
do {
memset(linebuf, 0, 1024);
idx = readline(ev->buffer, idx, linebuf);
if (strstr(linebuf, "Sec-WebSocket-Key")) {
//linebuf: Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==
strcat(linebuf, GUID);
//linebuf:
//Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
SHA1(linebuf + WEBSOCK_KEY_LENGTH, strlen(linebuf + WEBSOCK_KEY_LENGTH), sec_data); // openssl
base64_encode(sec_data, strlen(sec_data), sec_accept);
memset(ev->buffer, 0, BUFFER_LENGTH);
ev->length = sprintf(ev->buffer, "HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n\r\n", sec_accept);
printf("ws response : %s\n", ev->buffer);
break;
}
} while((ev->buffer[idx] != '\r' || ev->buffer[idx+1] != '\n') && idx != -1 );
return 0;
}
解析收到的数据的代码实现(此处用的是密文)
int transmission(struct ntyevent *ev) {
//ev->buffer; ev->length
ws_ophdr *hdr = (ws_ophdr*)ev->buffer;
printf("length: %d\n", hdr->pl_len);
if (hdr->pl_len < 126) { //
unsigned char *payload = ev->buffer + sizeof(ws_ophdr) + 4; // 6 payload length < 126
if (hdr->mask) { // mask set 1
umask(payload, hdr->pl_len, ev->buffer+2);
}
printf("payload : %s\n", payload);
} else if (hdr->pl_len == 126) {
ws_head_126 *hdr126 = ev->buffer + sizeof(ws_ophdr);
} else {
ws_head_127 *hdr127 = ev->buffer + sizeof(ws_ophdr);
}
}
https://github.com/xiaoyeyihao/xioayeyihao.github.io/blob/master/websocket_server.c
客户端再调用close之前,先发送一个应用层的fin的包给服务器,服务器接收到这个fin的包,把对应的客户端的用户数据,业务数据做清空,然后再调用close时候,服务器调用close会比较顺畅,不会出现存在大量的close_wait的情况存在。