博客园  :: 首页  :: 新随笔  :: 管理

2.5 websocks协议与服务器实现

Posted on 2023-04-07 01:44  wsg_blog  阅读(80)  评论(0编辑  收藏  举报

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-计算过程

  1. 每个websocket协议都会有一个GUID,这个是RFC文档中规定的没办法改的参数
    GUID = 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  2. 将Sec-WebSocket-Key与GUID连接在一起,构成一个字符串
    str = "QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  3. 对str字符串做hash,生成sha串
    sha = SHA1(str);
  4. 对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