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