Linux C/C++ Server websocket协议与服务器实现

原文 https://www.cnblogs.com/go-ahead-wsg/p/17294745.html

服务器需要主动推送(长连接)给客户端数据,通常使用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

Linux C/C++高性能服务器开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)成长体系教程

项目实战:https://docs.qq.com/doc/DR0pxY3F4UEtzVUpu

整理了一些学习书籍、视频资料(Linux C++ 后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全学习资料、教学视频和学习路线图),有需要的可以自行添加学习交流群:739729163 领取哦!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要在Linux C++实现WebSocket服务器端转发行情数据,可以使用以下步骤: 1. 使用第三方C++库,如libwebsockets或uWebSockets等,来实现WebSocket服务器端。 2. 在服务器端编写代码,以便将行情数据发送到WebSocket客户端。可以使用网络库,如boost::asio或ACE等,来管理网络连接和数据传输。 3. 在WebSocket服务器端中,使用JSON格式将行情数据打包并发送给客户端。可以使用第三方JSON库,如RapidJSON或nlohmann/json等。 4. 为了便于管理和扩展,可以将WebSocket服务器端和行情数据转发逻辑分离到不同的模块中。 下面是一个简单的示例代码,展示如何使用libwebsockets库实现WebSocket服务器端转发行情数据: ```c++ #include <libwebsockets.h> #include <iostream> #include <string> #include <vector> using namespace std; // 行情数据结构体 struct Quote { string symbol; double price; }; // 行情数据源 class QuoteSource { public: void addQuote(const Quote& quote) { quotes.push_back(quote); } const vector<Quote>& getQuotes() const { return quotes; } private: vector<Quote> quotes; }; // WebSocket服务器端 class WebSocketServer { public: WebSocketServer(int port) : port(port), context(NULL), listen_socket(NULL), quote_source(NULL) {} ~WebSocketServer() { if (listen_socket) { libwebsocket_close_and_free_session(context, listen_socket, LWS_CLOSE_STATUS_GOINGAWAY); } if (context) { libwebsocket_context_destroy(context); } } void setQuoteSource(QuoteSource* qs) { quote_source = qs; } bool start() { // 创建WebSocket协议上下文 struct libwebsocket_protocols protocols[] = { { "http-only", callback_http, 0 }, { "quote", callback_quote, sizeof(struct Quote) }, { NULL, NULL, 0 } }; struct libwebsocket_context_creation_info info = { .port = port, .protocols = protocols, .gid = -1, .uid = -1 }; context = libwebsocket_create_context(&info); if (!context) { cerr << "Failed to create WebSocket context" << endl; return false; } // 监听WebSocket连接 listen_socket = libwebsocket_create_server(context, 0, protocols, NULL, NULL, NULL, NULL, -1, -1); if (!listen_socket) { cerr << "Failed to create WebSocket server" << endl; libwebsocket_context_destroy(context); return false; } cout << "WebSocket server started on port " << port << endl; return true; } private: // HTTP回调函数 static int callback_http(struct libwebsocket_context* context, struct libwebsocket* wsi, enum libwebsocket_callback_reasons reason, void* user, void* in, size_t len) { return 0; } // 行情数据回调函数 static int callback_quote(struct libwebsocket_context* context, struct libwebsocket* wsi, enum libwebsocket_callback_reasons reason, void* user, void* in, size_t len) { switch (reason) { case LWS_CALLBACK_ESTABLISHED: // 新客户端连接 cout << "WebSocket client connected" << endl; break; case LWS_CALLBACK_SERVER_WRITEABLE: // 发送行情数据到客户端 if (quote_source) { const vector<Quote>& quotes = quote_source->getQuotes(); for (size_t i = 0; i < quotes.size(); ++i) { libwebsocket_write(wsi, (unsigned char*)&quotes[i], sizeof(Quote), LWS_WRITE_BINARY); } } break; case LWS_CALLBACK_CLOSED: // 客户端断开连接 cout << "WebSocket client disconnected" << endl; break; default: break; } return 0; } private: int port; struct libwebsocket_context* context; struct libwebsocket* listen_socket; QuoteSource* quote_source; }; int main() { // 创建WebSocket服务器 WebSocketServer server(8080); // 创建行情数据源 QuoteSource quote_source; // 添加行情数据 quote_source.addQuote({ "AAPL", 123.45 }); quote_source.addQuote({ "GOOGL", 234.56 }); // 设置行情数据源 server.setQuoteSource(&quote_source); // 启动WebSocket服务器 if (!server.start()) { return 1; } // 运行事件循环 while (true) { libwebsocket_service(server.getContext(), 50); } return 0; } ``` 在上面的示例代码中,我们使用libwebsockets库创建了一个WebSocket服务器,并且在服务器实现了一个行情数据源。在行情数据源中,我们添加了两个行情数据(AAPL和GOOGL),然后将行情数据源设置为WebSocket服务器的属性。在回调函数中,我们使用libwebsocket_write函数将行情数据发送到WebSocket客户端。 当我们运行这个示例程序时,它将启动一个WebSocket服务器,监听8080端口。如果我们使用浏览器或其他WebSocket客户端连接到这个服务器,它将发送行情数据到客户端。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值