网络编程(七):C++实现基于reactor的http&websocket服务器

文章目录

目录

前言

一、http服务器是什么?

HTTP 服务器的基本功能:

HTTP 服务器的工作流程:

HTTP 服务器的类型:

HTTP 请求和响应的示例:

总结:

二、websocket服务器是什么

WebSocket 服务器的主要特点:

1.双向通信:

2.持久化连接:

3.低延迟:

4.节省资源:

WebSocket 协议的握手过程:

1.客户端发起握手请求:

2.服务器接受握手并建立连接:

3.通信过程:

4.关闭连接:

WebSocket 服务器的常见用途:

1.即时通讯应用:

2.在线游戏:

3.实时金融数据:

4.协作工具:

5.物联网(IoT):

总结:

三、源码展示

reactor.c

server.h

server.c

reactor.h

编译指令

四、代码分析

(一)整体架构

(二)核心数据结构

(三)工作流程

1. 初始化阶段

2. 接受连接

3. 处理数据

4. 发送回复

(四)关键协议处理

HTTP协议处理

WebSocket协议处理

(五)重要技术细节

1.epoll使用:

2.高效文件传输:

3.WebSocket帧结构:

总结



前言

本文根据本系列上一篇文章《网络编程(六)》的代码改进而来,为了便于理解,可以先移步上一篇文章。

0voice · GitHub


一、http服务器是什么?

HTTP 服务器 是一种用于处理和响应 HTTP 请求的服务器,它是基于 HTTP 协议(超文本传输协议)工作的。HTTP 服务器接受客户端(通常是浏览器或其他客户端应用程序)发起的请求,并返回所请求的资源或信息。

HTTP 服务器的基本功能:

  1. 接收请求:HTTP 服务器监听来自客户端的 HTTP 请求。这些请求通常是通过浏览器或其他客户端发起的,包含了客户端所请求的资源信息(如网页、图片、文件等)。

  2. 处理请求:服务器根据请求的内容、请求头和请求方法,判断客户端请求的资源是静态的(如 HTML 文件、图片)还是动态的(如数据库查询结果、处理后的页面内容)。服务器还会根据请求的 URL 路径和查询参数来决定如何处理。

  3. 返回响应:服务器根据客户端的请求,生成相应的 HTTP 响应,通常包括:

    • 状态码(例如:200 OK 表示成功,404 Not Found 表示资源未找到,500 Internal Server Error 表示服务器错误)
    • 响应头(如 Content-Type、Date 等信息)
    • 响应体(即实际返回的内容,例如 HTML 页面、JSON 数据、图片等)
  4. 支持静态和动态内容

    • 静态内容:例如存储在服务器上的 HTML 文件、CSS 文件、图片、视频等资源。
    • 动态内容:通过服务器端脚本(如 PHP、Python、Node.js 等)动态生成的页面或数据,通常是与数据库进行交互并返回生成的内容。

HTTP 服务器的工作流程:

  1. 客户端(如浏览器)向服务器发送 HTTP 请求。
  2. 服务器接收并解析请求,查看请求的路径、方法、参数等。
  3. 根据请求的内容,服务器获取资源或调用后端程序生成动态内容。
  4. 服务器返回一个 HTTP 响应,包含相应的状态码、头部信息和响应体。
  5. 客户端(浏览器)接收到响应并显示结果。

HTTP 服务器的类型:

  • 静态服务器:只提供静态资源(如 HTML 文件、CSS、JS 文件、图片等)的服务。
  • 动态服务器:处理客户端请求并生成动态内容。通常与后端语言(如 PHP、Python、Ruby、Java 等)结合使用。
  • 反向代理服务器:将客户端请求转发给其他服务器处理,常用于负载均衡和提高可用性。
  • API 服务器:专门处理 API 请求,通常返回 JSON 或 XML 格式的数据,广泛用于前后端分离的应用。

HTTP 请求和响应的示例:

HTTP 请求示例:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

HTTP 响应示例:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 132

<html>
  <head><title>Example</title></head>
  <body><h1>Hello, world!</h1></body>
</html>

总结:

  • HTTP 服务器 是一个应用程序,负责接受和处理来自客户端的 HTTP 请求,并返回相应的 HTTP 响应。
  • 它的主要功能是提供静态和动态资源,支持客户端的访问请求。
  • 常见的 HTTP 服务器包括 Apache、Nginx、LiteSpeed、Node.js 等。
  • HTTP 服务器是 Web 应用的核心组件,用于实现客户端与服务器之间的通信。

二、websocket服务器是什么

WebSocket 服务器 是一种专门用于处理 WebSocket 协议连接的服务器,它支持全双工、实时、持久化的双向通信。这使得 WebSocket 服务器与传统的基于 HTTP 协议的服务器有所不同,后者通常是请求-响应模式,每次交互都需要重新建立连接。

WebSocket 服务器的主要特点:

1.双向通信:

  • WebSocket 提供了 全双工通信,这意味着客户端和服务器可以在任何时候彼此发送数据,不必等待对方的请求或响应。与 HTTP 协议的请求-响应模型不同,WebSocket 允许服务器主动向客户端发送数据。

2.持久化连接:

  • 在 WebSocket 协议中,一旦建立了连接,客户端和服务器之间的连接将保持打开状态,直到显式关闭。这样,服务器和客户端之间可以维持一个长期有效的连接,避免了 HTTP 请求/响应模式中每次都需要建立连接的开销。

3.低延迟:

  • WebSocket 的持久连接和双向通信特性使得它比 HTTP 更适合处理需要低延迟和实时交互的应用场景,如在线游戏、聊天应用、实时金融数据等。

4.节省资源:

  • 在传统的 HTTP 协议中,每次客户端向服务器发送请求时,都需要建立和关闭一个连接,而 WebSocket 协议只需要在初始握手时建立连接,后续的数据交换通过同一个连接进行。这使得 WebSocket 在高频次消息交换的应用中更为高效。

WebSocket 协议的握手过程:

1.客户端发起握手请求:

  • 客户端向服务器发送一个 HTTP 请求,带有特定的 Sec-WebSocket-Key 头和其他 WebSocket 相关的头部,表示希望与服务器建立 WebSocket 连接。

请求示例:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlbG9vb3B5Y29tcGxleG9u
Sec-WebSocket-Version: 13

2.服务器接受握手并建立连接:

  • 如果服务器支持 WebSocket 协议,它会返回一个 HTTP 101 状态码("Switching Protocols"),并且在响应头中包含 Sec-WebSocket-Accept 头。此时,WebSocket 连接被成功建立,之后的通信就不再使用 HTTP,而是通过 WebSocket 协议进行。

响应示例:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGz7wF3QXAmQK6vKpmkGk0cQ==
 

3.通信过程:

  • 连接建立后,客户端和服务器之间的通信将使用 WebSocket 协议进行,而不再是 HTTP 协议。此时,双方可以随时互相发送消息。

4.关闭连接:

  • WebSocket 连接可以由任何一方(客户端或服务器)发起关闭,通过发送一个控制帧来完成关闭过程。

WebSocket 服务器的常见用途:

1.即时通讯应用:

如在线聊天、即时消息(IM)系统,通过 WebSocket,消息可以实时传输,不需要不断的轮询。

2.在线游戏:

WebSocket 使得多人在线游戏能够实时交换状态信息和动作。

3.实时金融数据:

金融市场中的实时股价、汇率等数据可以通过 WebSocket 推送给客户端。

4.协作工具:

像 Google Docs、GitHub 等在线协作工具使用 WebSocket 进行实时的多用户协作和更新。

5.物联网(IoT):

WebSocket 可用于实时数据交换,适用于物联网设备的通信。

总结:

  • WebSocket 服务器 是一种专门处理 WebSocket 协议连接的服务器,支持持久连接、低延迟、全双工通信。
  • 它通过 WebSocket 握手 与客户端建立连接,在后续的通信中可以实时、双向交换数据,适用于实时应用场景。
  • 常见的使用场景包括 即时通讯、在线游戏、金融数据、物联网 等。

三、源码展示

reactor.c




#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>


#include "server.h"


#define CONNECTION_SIZE			1048576 // 1024 * 1024

#define MAX_PORTS			20

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)





int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);



int epfd = 0;
struct timeval begin;



struct conn conn_list[CONNECTION_SIZE] = {0};
// fd


int set_event(int fd, int event, int flag) {

	if (flag) {  // non-zero add

		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

	} else {  // zero mod

		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
		
	}
	

}


int event_register(int fd, int event) {

	if (fd < 0) return -1;

	conn_list[fd].fd = fd;
	conn_list[fd].r_action.recv_callback = recv_cb;
	conn_list[fd].send_callback = send_cb;

	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].rlength = 0;

	memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].wlength = 0;

	set_event(fd, event, 1);
}


// listenfd(sockfd) --> EPOLLIN --> accept_cb
int accept_cb(int fd) {

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);

	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
	//printf("accept finshed: %d\n", clientfd);
	if (clientfd < 0) {
		printf("accept errno: %d --> %s\n", errno, strerror(errno));
		return -1;
	}
	
	event_register(clientfd, EPOLLIN);  // | EPOLLET

	if ((clientfd % 1000) == 0) {

		struct timeval current;
		gettimeofday(&current, NULL);

		int time_used = TIME_SUB_MS(current, begin);
		memcpy(&begin, &current, sizeof(struct timeval));
		

		printf("accept finshed: %d, time_used: %d\n", clientfd, time_used);

	}

	return 0;
}


int recv_cb(int fd) {

	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH );
	int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
	if (count == 0) { // disconnect
		printf("client disconnect: %d\n", fd);
		close(fd);

		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); // unfinished

		return 0;
	} else if (count < 0) { // 

		printf("count: %d, errno: %d, %s\n", count, errno, strerror(errno));
		close(fd);
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

		return 0;
	}

	
	conn_list[fd].rlength = count;
	//printf("RECV: %s\n", conn_list[fd].rbuffer);

#if 0 // echo

	conn_list[fd].wlength = conn_list[fd].rlength;
	memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);

	printf("[%d]RECV: %s\n", conn_list[fd].rlength, conn_list[fd].rbuffer);

#elif 0

	http_request(&conn_list[fd]);

#else

	ws_request(&conn_list[fd]);
	
#endif


	set_event(fd, EPOLLOUT, 0);

	return count;
}


int send_cb(int fd) {

#if 0

	http_response(&conn_list[fd]);

#else

	ws_response(&conn_list[fd]);

#endif

	int count = 0;

#if 0
	if (conn_list[fd].status == 1) {
		//printf("SEND: %s\n", conn_list[fd].wbuffer);
		count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
		set_event(fd, EPOLLOUT, 0);
	} else if (conn_list[fd].status == 2) {
		set_event(fd, EPOLLOUT, 0);
	} else if (conn_list[fd].status == 0) {

		if (conn_list[fd].wlength != 0) {
			count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
		}
		
		set_event(fd, EPOLLIN, 0);
	}
#else

	if (conn_list[fd].wlength != 0) {
		count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
	}
	
	set_event(fd, EPOLLIN, 0);

#endif
	//set_event(fd, EPOLLOUT, 0);

	return count;
}



int init_server(unsigned short port) {

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
	servaddr.sin_port = htons(port); // 0-1023, 

	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
		printf("bind failed: %s\n", strerror(errno));
	}

	listen(sockfd, 10);
	//printf("listen finshed: %d\n", sockfd); // 3 

	return sockfd;

}

int main() {

	unsigned short port = 2000;


	epfd = epoll_create(1);

	int i = 0;

	for (i = 0;i < MAX_PORTS;i ++) {
		
		int sockfd = init_server(port + i);
		
		conn_list[sockfd].fd = sockfd;
		conn_list[sockfd].r_action.recv_callback = accept_cb;
		
		set_event(sockfd, EPOLLIN, 1);
	}

	gettimeofday(&begin, NULL);

	while (1) { // mainloop

		struct epoll_event events[1024] = {0};
		int nready = epoll_wait(epfd, events, 1024, -1);

		int i = 0;
		for (i = 0;i < nready;i ++) {

			int connfd = events[i].data.fd;

#if 0
			if (events[i].events & EPOLLIN) {
				conn_list[connfd].r_action.recv_callback(connfd);
			} else if (events[i].events & EPOLLOUT) {
				conn_list[connfd].send_callback(connfd);
			}

#else 
			if (events[i].events & EPOLLIN) {
				conn_list[connfd].r_action.recv_callback(connfd);
			} 

			if (events[i].events & EPOLLOUT) {
				conn_list[connfd].send_callback(connfd);
			}
#endif
		}

	}
	

}


server.h

#ifndef __SERVER_H__
#define __SERVER_H__

#define BUFFER_LENGTH		1024


typedef int (*RCALLBACK)(int fd);


struct conn {
	int fd;

	char rbuffer[BUFFER_LENGTH];
	int rlength;

	char wbuffer[BUFFER_LENGTH];
	int wlength;

	RCALLBACK send_callback;

	union {
		RCALLBACK recv_callback;
		RCALLBACK accept_callback;
	} r_action;

	int status;
#if 1 // websocket
	char *payload;
	char mask[4];
#endif
};


int http_request(struct conn *c);
int http_response(struct conn *c);

int ws_request(struct conn *c);
int ws_response(struct conn *c);



#endif



server.c




#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>

#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>


#include "reactor.h"

int http_request(struct conn *c) {

	memset(c->wbuffer, 0, BUFFER_LENGTH);
	c->wlength = 0;

	c->status = 0;

	return 0;
}


int http_response(struct conn *c) {

//Tue Apr 30 06:41:52 AM UTC 2024
#if 0
	c->wlength = sprintf(c->wbuffer, 		
		"HTTP/1.1 200 OK\r\n"		
		"Accept-Ranges: bytes\r\n"		
		"Content-Length: 82\r\n"		
		"Content-Type: text/html\r\n"		
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n"		
		"<html><head><title>0voice.king</title></head><body><h1>King</h1></body></html>\r\n\r\n");
#elif 0

	int filefd = open("index.html", O_RDONLY);

	struct stat stat_buf;
	fstat(filefd, &stat_buf);

	c->wlength = sprintf(c->wbuffer, 		
		"HTTP/1.1 200 OK\r\n"		
		"Accept-Ranges: bytes\r\n"		
		"Content-Length: %ld\r\n"		
		"Content-Type: text/html\r\n"		
		"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",
		stat_buf.st_size);

	//printf("header: %s\n", c->wbuffer);

	int count = read(filefd, c->wbuffer + c->wlength, BUFFER_LENGTH - c->wlength);
	if (count < 0) {
		printf("read: %d, errno: %d, %s\n", filefd, errno, strerror(errno));
	}
	c->wlength += count;

	close(filefd);
	//printf("body: %s\n", c->wbuffer);


#elif 0

	int filefd = open("index.html", O_RDONLY);

	struct stat stat_buf;
	fstat(filefd, &stat_buf);

	if (c->status == 0) {
		c->wlength = sprintf(c->wbuffer, 		
			"HTTP/1.1 200 OK\r\n"		
			"Accept-Ranges: bytes\r\n"		
			"Content-Length: %ld\r\n"		
			"Content-Type: text/html\r\n"		
			"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",
			stat_buf.st_size);
		c->status = 1;
		printf("header: %s\n", c->wbuffer);
	} else {
		int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
		if (ret == -1) {			
			printf("errno: %d\n", errno);		
		}
		c->status = 0;
		printf("sendfile\n");
		c->wlength = 0;
	}
	
	close(filefd);

#else 

	int filefd = open("c1000k.png", O_RDONLY);

	struct stat stat_buf;
	fstat(filefd, &stat_buf);

	if (c->status == 0) {
		c->wlength = sprintf(c->wbuffer, 		
			"HTTP/1.1 200 OK\r\n"		
			"Accept-Ranges: bytes\r\n"		
			"Content-Length: %ld\r\n"		
			"Content-Type: image/png\r\n"		
			"Date: Tue, 30 Apr 2024 13:16:46 GMT\r\n\r\n",
			stat_buf.st_size);
		c->status = 1;
		printf("header: %s\n", c->wbuffer);
	} else {
		int ret = sendfile(c->fd, filefd, NULL, stat_buf.st_size);
		if (ret == -1) {			
			printf("errno: %d\n", errno);		
		}
		c->status = 0;
		printf("sendfile\n");
		c->wlength = 0;
	}
	
	close(filefd);

#endif

	return c->wlength;

}



// websocket
#define GUID 					"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"


struct _nty_ophdr {

	unsigned char opcode:4,
		 rsv3:1,
		 rsv2:1,
		 rsv1:1,
		 fin:1;
	unsigned char payload_length:7,
		mask:1;

} __attribute__ ((packed));

struct _nty_websocket_head_126 {
	unsigned short payload_length;
	char mask_key[4];
	unsigned char data[8];
} __attribute__ ((packed));

struct _nty_websocket_head_127 {

	unsigned long long payload_length;
	char mask_key[4];

	unsigned char data[8];
	
} __attribute__ ((packed));

typedef struct _nty_websocket_head_127 nty_websocket_head_127;
typedef struct _nty_websocket_head_126 nty_websocket_head_126;
typedef struct _nty_ophdr nty_ophdr;




int base64_encode(char *in_str, int in_len, char *out_str) {    
	BIO *b64, *bio;    
	BUF_MEM *bptr = NULL;    
	size_t size = 0;    

	if (in_str == NULL || out_str == NULL)        
		return -1;    

	b64 = BIO_new(BIO_f_base64());    
	bio = BIO_new(BIO_s_mem());    
	bio = BIO_push(b64, bio);
	
	BIO_write(bio, in_str, in_len);    
	BIO_flush(bio);    

	BIO_get_mem_ptr(bio, &bptr);    
	memcpy(out_str, bptr->data, bptr->length);    
	out_str[bptr->length-1] = '\0';    
	size = bptr->length;    

	BIO_free_all(bio);    
	return size;
}


int readline(char* allbuf,int level,char* linebuf){    
	int len = strlen(allbuf);    

	for (;level < len; ++level)    {        
		if(allbuf[level]=='\r' && allbuf[level+1]=='\n')            
			return level+2;        
		else            
			*(linebuf++) = allbuf[level];    
	}    

	return -1;
}

void demask(char *data,int len,char *mask){    
	int i;    
	for (i = 0;i < len;i ++)        
		*(data+i) ^= *(mask+(i%4));
}



char* decode_packet(unsigned char *stream, char *mask, int length, int *ret) {

	nty_ophdr *hdr =  (nty_ophdr*)stream;
	unsigned char *data = stream + sizeof(nty_ophdr);
	int size = 0;
	int start = 0;
	//char mask[4] = {0};
	int i = 0;

	if ((hdr->mask & 0x7F) == 126) {

		nty_websocket_head_126 *hdr126 = (nty_websocket_head_126*)data;
		size = hdr126->payload_length;
		
		for (i = 0;i < 4;i ++) {
			mask[i] = hdr126->mask_key[i];
		}
		
		start = 8;
		
	} else if ((hdr->mask & 0x7F) == 127) {

		nty_websocket_head_127 *hdr127 = (nty_websocket_head_127*)data;
		size = hdr127->payload_length;
		
		for (i = 0;i < 4;i ++) {
			mask[i] = hdr127->mask_key[i];
		}
		
		start = 14;

	} else {
		size = hdr->payload_length;

		memcpy(mask, data, 4);
		start = 6;
	}

	*ret = size;
	demask(stream+start, size, mask);

	return stream + start;
	
}

int encode_packet(char *buffer,char *mask, char *stream, int length) {

	nty_ophdr head = {0};
	head.fin = 1;
	head.opcode = 1;
	int size = 0;

	if (length < 126) {
		head.payload_length = length;
		memcpy(buffer, &head, sizeof(nty_ophdr));
		size = 2;
	} else if (length < 0xffff) {
		nty_websocket_head_126 hdr = {0};
		hdr.payload_length = length;
		memcpy(hdr.mask_key, mask, 4);

		memcpy(buffer, &head, sizeof(nty_ophdr));
		memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_126));
		size = sizeof(nty_websocket_head_126);
		
	} else {
		
		nty_websocket_head_127 hdr = {0};
		hdr.payload_length = length;
		memcpy(hdr.mask_key, mask, 4);
		
		memcpy(buffer, &head, sizeof(nty_ophdr));
		memcpy(buffer+sizeof(nty_ophdr), &hdr, sizeof(nty_websocket_head_127));

		size = sizeof(nty_websocket_head_127);
		
	}

	memcpy(buffer+2, stream, length);

	return length + 2;
}

#define WEBSOCK_KEY_LENGTH	19

int handshark(struct conn *c) {

	//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(c->rbuffer, 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(c->wbuffer, 0, BUFFER_LENGTH); 

			c->wlength = sprintf(c->wbuffer, "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", c->wbuffer);

			break;
			
		}

	} while((c->rbuffer[idx] != '\r' || c->rbuffer[idx+1] != '\n') && idx != -1 );

	return 0;
}



int ws_request(struct conn *c) {

	if (c->status == 0) {
		
		handshark(c);
		c->status = 1;
		
	} else if (c->status == 1) {
		char mask[4] = {0};
		int ret = 0;

		char *data = decode_packet(c->rbuffer, mask, c->rlength, &ret);

		printf("data : %s , length : %d\n", data, ret);

		ret = encode_packet(c->wbuffer, mask, data, ret);
		c->wlength = ret;
	}

	return 0;
}

int ws_response(struct conn *c) {

	return 0;
}


reactor.h





#ifndef __REACTOR_H__
#define __REACTOR_H__


#define BUFFER_LENGTH		1024

typedef int (*RCALLBACK)(int fd);


struct conn {
	int fd;

	char rbuffer[BUFFER_LENGTH];
	int rlength;

	char wbuffer[BUFFER_LENGTH];
	int wlength;

	RCALLBACK send_callback;

	union {
		RCALLBACK recv_callback;
		RCALLBACK accept_callback;
	} r_action;

	int status; // { header, body}
};



#endif



编译指令

gcc -o server reactor.c server.c -lssl -lcrypto -mcmodel=medium

编译结果如下图所示即为成功编译

四、代码分析

(一)整体架构

这是一个基于epoll事件驱动的高性能服务器程序,可以同时处理HTTP和WebSocket协议。主要特点如下:

  1. 多端口监听:同时监听20个连续端口(2000-2019)

  2. 高并发处理:使用epoll实现IO多路复用,支持百万级连接

  3. 模块化设计:通过回调函数处理不同事件(接收连接、读取数据、发送数据)

  4. 协议支持:同时支持HTTP和WebSocket协议

(二)核心数据结构

struct conn {
    int fd; // 文件描述符
    
    char rbuffer[1024]; // 读缓冲区
    int rlength;        // 已读数据长度
    
    char wbuffer[1024]; // 写缓冲区
    int wlength;        // 待写数据长度
    
    int (*send_callback)(int); // 发送回调函数
    
    union {
        int (*recv_callback)(int);  // 普通连接的读回调
        int (*accept_callback)(int); // 监听socket的接受回调
    } r_action;
    
    int status; // 连接状态
};

这个结构体为每个连接保存了所有必要信息,就像给每个客人分配了一个专属服务员,记录客人的所有需求。

(三)工作流程

1. 初始化阶段

int main() {
    // 创建epoll实例
    epfd = epoll_create(1);
    
    // 创建20个监听端口
    for (i = 0; i < MAX_PORTS; i++) {
        int sockfd = init_server(port + i);
        // 注册监听socket到epoll
        set_event(sockfd, EPOLLIN, 1);
    }
    
    // 事件循环
    while (1) {
        int nready = epoll_wait(...);
        // 处理每个就绪的事件
    }
}

就像开了一家20个接待窗口的银行,每个窗口都有一个接待员(监听socket)。

2. 接受连接

int accept_cb(int fd) {
    int clientfd = accept(...);
    event_register(clientfd, EPOLLIN);
}

当有新客户到来时:

  1. 接待员(监听socket)通过accept发放排队号码(clientfd)

  2. 给新客户分配专属服务员(注册到epoll)

  3. 开始等待客户的具体业务需求(监听EPOLLIN事件)

3. 处理数据

int recv_cb(int fd) {
    // 读取客户端数据
    int count = recv(...);
    
    // 根据协议类型处理
    if (WebSocket模式) {
        ws_request(...);
    } else {
        http_request(...);
    }
    
    // 准备回复
    set_event(fd, EPOLLOUT, 0);
}

就像服务员拿到客户的业务单据:

  1. 读取客户需求

  2. 根据业务类型(HTTP/WebSocket)进行处理

  3. 准备回复材料

4. 发送回复

int send_cb(int fd) {
    if (WebSocket模式) {
        ws_response(...);
    } else {
        http_response(...);
    }
    
    send(...);
    // 切换回等待读取状态
    set_event(fd, EPOLLIN, 0);
}

服务员将处理结果反馈给客户后,继续等待客户的下一个需求。

(四)关键协议处理

HTTP协议处理

int http_response(struct conn *c) {
    // 构造HTTP头
    sprintf(c->wbuffer, "HTTP/1.1 200 OK\r\nContent-Length: ...");
    
    // 发送文件内容
    sendfile(c->fd, filefd, ...);
}

典型的HTTP响应流程:

  1. 构造响应头

  2. 发送文件内容(使用sendfile高效传输)

WebSocket协议处理

int ws_request(struct conn *c) {
    if (握手阶段) {
        // 生成加密响应
        SHA1(...);
        base64_encode(...);
    } else {
        // 处理数据帧
        decode_packet(...);
        encode_packet(...);
    }
}

WebSocket处理关键点:

  1. 握手阶段使用SHA1和Base64加密验证

  2. 数据帧需要处理掩码和长度编码

  3. 支持分片传输(FIN标志位)

(五)重要技术细节

1.epoll使用

  • EPOLLIN:数据可读事件
  • EPOLLOUT:数据可写事件
  • 边缘触发(ET) vs 水平触发(LT):本代码使用水平触发

2.高效文件传输

sendfile(c->fd, filefd, ...);

  • 直接在操作系统内核完成文件传输,无需用户空间拷贝

Q:sendfile有什么优势?
A:零拷贝技术,减少数据在内核态和用户态的复制次数

3.WebSocket帧结构

struct _nty_ophdr {
    unsigned char opcode:4,
                 fin:1;
    unsigned char payload_length:7,
                 mask:1;
};

处理各种长度的数据帧:

  • 普通帧(<126字节)

  • 扩展帧(126/127字节长度头)


总结

总结需要讲清楚一个事情,那就是这篇文章体现了reactor设计模式的一个优势:网络与业务隔离

Reactor 设计模式 可以通过其 事件驱动 的机制有效地实现 网络层和业务逻辑层的隔离。这一点在开发高性能网络应用时尤为重要,因为它可以简化代码结构,提高系统的可维护性,并减少不同层次之间的耦合度。

1. 网络层和业务层的定义:

  • 网络层:负责处理所有与网络相关的任务,例如监听连接、接收和发送数据等。这些操作通常是 I/O 密集型的,并且可能会有大量的并发请求。
  • 业务层:负责处理应用程序的具体业务逻辑,例如处理用户请求、计算、存储和返回数据等。这些操作通常是 CPU 密集型的,并且依赖于特定的业务需求。

2. Reactor 模式如何实现隔离:

Reactor 模式通过 事件驱动机制 将网络 I/O 操作和业务逻辑的处理分开,避免了它们之间的直接耦合。具体来说,Reactor 模式的核心是一个 事件循环,它负责监听和分发事件。

事件循环(Event Loop)

  • Reactor 模式的核心是一个事件循环,它会监听多个 I/O 事件(例如网络连接的到达、数据的读取或写入等)。当事件发生时,事件循环将这些事件分发给相应的 事件处理器

I/O 多路复用(I/O Multiplexing)

  • 使用 I/O 多路复用技术(如 epoll、select、poll),Reactor 模式可以在单个线程中高效地处理多个 I/O 操作,而不需要为每个连接创建一个线程或进程。

事件处理器(Event Handlers)

  • 事件处理器是 Reactor 模式中的核心组件,它们会针对不同的 I/O 事件执行相应的操作(例如,处理接收到的数据或发送数据)。在 Reactor 模式中,网络事件处理和业务逻辑处理是分开的
    • 网络事件处理:比如客户端连接建立、数据接收等网络相关操作,这些通常由专门的 I/O 处理器来处理。
    • 业务逻辑处理:一旦数据被接收到,业务层逻辑会通过事件处理回调来处理这些数据。业务层代码并不会直接关心网络层的细节(如如何接收数据、如何发送数据),而只关心如何根据接收到的数据做出相应的业务处理。

如何实现隔离:

  1. I/O 事件分发:网络层的 I/O 事件(如连接请求、数据到达等)由 Reactor(事件循环)管理,它会将这些事件分发给特定的 事件处理器。这些处理器只关心如何处理网络事件,例如接收数据、建立连接等。
  2. 回调机制:事件处理器通常使用 回调函数 来通知业务层执行特定的操作。当网络事件处理完成(例如数据被接收到),网络层会调用相应的业务逻辑处理函数(回调),而业务层则负责对这些数据进行处理和响应。
  3. 代码解耦:通过这种方式,网络层和业务层可以完全解耦。网络层只负责监听和处理网络 I/O 操作,业务层只负责处理实际的业务逻辑。两者之间没有直接的依赖关系,这样就实现了 网络和业务的隔离

示例:

假设你正在构建一个 Web 服务器:

  • 网络层:负责监听传入的 HTTP 请求、解析 HTTP 请求头、从客户端读取数据等。
  • 业务层:在接收到 HTTP 请求后,解析请求内容并返回适当的响应内容,可能涉及到数据库查询或业务计算。

在 Reactor 模式下,网络层(Reactor)负责处理客户端的连接和数据接收事件,当收到请求时,它会将请求传递给 HTTP 请求处理器。这个处理器只负责解析请求并将请求的数据交给 业务层 进行进一步处理。这样,网络和业务逻辑被清晰地分开,网络层不关心业务细节,业务层不关心网络通信的细节。

3. 优点:

  • 代码解耦:通过将网络操作和业务逻辑分离,系统的可维护性大大增强。你可以独立地修改网络层或业务层,而不影响另一个层。
  • 灵活性:你可以根据需求更改或扩展网络层和业务层。例如,如果你想将系统迁移到不同的网络框架或协议,只需修改网络层部分即可。
  • 清晰的结构:每个模块(网络层和业务层)职责明确,避免了复杂的代码交织,使得程序结构更加清晰。

总结:

Reactor 设计模式能够通过 事件驱动和回调机制 实现 网络层和业务层的隔离,即网络层只负责接收、发送数据和管理连接,而业务层则专注于处理业务逻辑。这样设计的优点是 解耦灵活性清晰的代码结构,使得系统更加容易扩展和维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值