Linux C/C++服务器
websocket协议与服务器实现
服务器需要主动推送(长连接)给客户端数据,通常使用websocket协议,比如股票信息实时数据等;websocket服务器为websocket协议+reactor实现
websocket协议与http协议对比,http协议是针对网页设计的协议,为一请求一连接形式适合短连接,而websocket为长连接,握手使用文本字符串,传输为二进制数据,数据包更小更快,可主动推送数据给客户端
websocket handshake建立连接
ws握手,websocket是在tcp协议之上的协议,也就是说双方已经建立了tcp连接,然后再进行websocket建立连接,也就是websocket handshake
收到的websocket握手数据包:
GET / HTTP/1.1 Host: 192.168.232.128:8888 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Upgrade: websocket Origin: null Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7 Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
返回的websocket握手数据包:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: value
Sec-WebSocket-Accept:value-计算过程
- 每个websocket协议都会有一个GUID,这个是RFC文档中规定的没办法改的参数
GUID = 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
- 将Sec-WebSocket-Key与GUID连接在一起,构成一个字符串
str = "QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- 对str字符串做hash,生成sha串
sha = SHA1(str);
- 对sha串进行base64编码
value = base64_encode(sha);
在应用层上,tcp可以保证顺序,先发的先到;通过分隔符(/r/n/r/n)或者具体的长度解决tcp分包与粘包问题,websocket协议通过分隔符(/r/n/r/n)表示读协议结束,那么handshake的过程变成了协议解析的过程
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; }
websocket 协议与解析
websocket协议
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 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); } } void umask(char *payload, int length, char *mask_key) { int i = 0; for (i = 0;i < length;i ++) { payload[i] ^= mask_key[i%4]; } }
websocket 数据收发
websocket数据状态有三种,handshake握手数据、握手完成后正常的数据收发tranmission、断开连接数据end
我们定义一个标识用来区分这三种数据
enum { WS_HANDSHARK = 0, WS_TRANMISSION = 1, WS_END = 2, };
reactor
在connect()->listenfd会触发listen的回调函数accept_cb,并在accept_cb函数中修改此数据标志为WS_HANDSHARK
reactor通过EPOLLIN/EPOLLOUT触发不同的回调函数
回调函数会触发send_cb或recv_cb
数据接收
recv_cb中会处理handshake和tranmission数据,用来接收websocket数据
int recv_cb(int fd, int events, void *arg) { struct ntyreactor *reactor = (struct ntyreactor*)arg; struct ntyevent *ev = ntyreactor_idx(reactor, fd); int len = recv(fd, ev->buffer, BUFFER_LENGTH , 0); // if (len > 0) { ev->length = len; ev->buffer[len] = '\0'; printf("C[%d]: machine: %d\n", fd, ev->status_machine); websocket_request(ev); nty_event_del(reactor->epfd, ev); nty_event_set(ev, fd, send_cb, reactor); nty_event_add(reactor->epfd, EPOLLOUT, ev); } else if (len == 0) { nty_event_del(reactor->epfd, ev); close(ev->fd); //printf("[fd=%d] pos[%ld], closed\n", fd, ev-reactor->events); } else { nty_event_del(reactor->epfd, ev); close(ev->fd); printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno)); } return len; }
websocket_request接口主要用于判断和解析协议,至此数据接收就完成了
int websocket_request(struct ntyevent *ev) { if (ev->status_machine == WS_HANDSHARK) { ev->status_machine = WS_TRANMISSION; handshark(ev); } else if (ev->status_machine == WS_TRANMISSION) { transmission(ev); } else { } printf("websocket_request --> %d\n", ev->status_machine); }
数据发送
数据发送也比较简单,只需改变事件的属性为EPOLLOUT并添加到epoll中,即可将事件的缓冲区数据发送至相应的fd客户端
websocket 退出
当收到的数据FIN标志位为1时,此websocket断开退出
websocket demo
编译
sudo yum install openssl-devel -y #安装openssl gcc websocket.c -o websocket -I /usr/local/ssl/include -L /usr/local/ssl/lib -lssl -lcrypto -Wall
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
2020-04-07 textcnn文本分类简述及代码(包含中文文本分类代码)