hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之高性能服务器框架介绍,在这篇文章中,你将会学习到高效的创建自己的高性能服务器,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!
希望这篇文章能对你有所帮助,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!(注:这章对于高性能服务器的架构非常重要哟!!!)
目录
一.服务器模型
1.1 C/S模型
C/S模型的组成
C/S模型的通信过程
1.2 P2P模型
Linux 中的服务器 P2P 模型
二. 两种高效的服务器事件处理
2.1 Reactor模式
Linux 中的 Reactor 模式
2.2. Proactor模式
Linux 中的 Proactor 模式
一.服务器模型
1.1 C/S模型
C/S模型,即客户端/服务器模型(Client/Server Model),是一种网络计算模型,它将任务和工作负载分配到客户端和服务器两个不同的计算环境中。在这种模型中,客户端负责发送请求,而服务器负责处理请求并返回响应。
如图:
C/S模型的组成
客户端(Client):
客户端通常是用户直接交互的应用程序,例如网页浏览器、电子邮件客户端或移动应用。它向服务器发送请求,并接收服务器返回的数据。客户端可以执行一些计算任务,但主要依赖于服务器来处理复杂或数据密集型的任务。服务器(Server):
服务器是一个提供数据存储和服务的系统,它响应客户端的请求。服务器通常拥有强大的计算能力和存储空间,能够处理多个客户端的请求。服务器可以运行数据库管理系统,如 MySQL 或 PostgreSQL,以及各种服务器软件,如 HTTP 服务器 Apache 或 Nginx。C/S模型的通信过程
请求:客户端建立一个到服务器的连接,并发送一个请求。处理:服务器接收到请求后,对其进行处理。响应:服务器将处理结果作为响应发送回客户端。关闭连接:客户端接收响应后,通常关闭与服务器的连接我们可以使用多线程来进行实现,一个连接的业务处理分配一个线程:
核心代码如下:
线程处理函数:
// 定义线程函数void *handle_client(void *socket_desc) { int sock = *(int*)socket_desc; char *message; int len; // 接收客户端数据 while((len = read(sock, message, 1024)) > 0) { printf("收到数据:%s\n", message); // 发送响应 write(sock, "Hello, Client!", 14); memset(message, 0, 1024); } // 关闭套接字 close(sock); return 0;}
主函数:
int main() { int sock, newsock, clilen; struct sockaddr_in serv_addr, cli_addr; int *new_sock; pthread_t thread_id; // 创建套接字 sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) { printf("Could not create socket"); } // 填充服务器地址结构 serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(8080); // 绑定套接字到地址 if (bind(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind failed"); return -1; } // 监听套接字 listen(sock, 3); printf("Listening...\n"); clilen = sizeof(cli_addr); // 接受客户端连接 while((newsock = accept(sock, (struct sockaddr *)&cli_addr, (socklen_t*)&clilen)) > 0) { // 创建新线程 new_sock = malloc(1); *new_sock = newsock; if (pthread_create(&thread_id, NULL, handle_client, (void*)new_sock) < 0) { perror("could not create thread"); return -1; } pthread_detach(thread_id); } // 关闭套接字 if (newsock < 0) { perror("accept failed"); return -1; } return 0;}
1.2 P2P模型
在 Linux 环境中,P2P(点对点)模型是一种直接连接两个或多个计算机的网络通信方式,其中没有中心服务器参与数据传输。在 P2P 模型中,每个节点既是客户端也是服务器,可以相互发送和接收数据。
Linux 中的服务器 P2P 模型
节点间通信:
P2P 网络中的每个节点都直接与其他节点通信,没有中央服务器来处理连接和数据传输。节点之间通过套接字(sockets)进行通信,可以建立全双工连接。网络拓扑:
P2P 网络可以是星型、网状或混合型,取决于节点的连接方式和网络结构。节点可以通过各种机制发现其他节点,如 DHT(分布式哈希表)算法。如图:
代码和C/S相似,大家可以去网上自行寻找资料,这里就不再重复了哦!
二. 两种高效的服务器事件处理
2.1 Reactor模式
Reactor 模式是一种事件驱动的网络编程模式,用于处理高并发网络服务。在 Reactor 模式中,一个或多个线程负责监听网络事件,当事件发生时,例如新的连接请求、数据到达等,Reactor 模式会触发相应的处理函数来处理这些事件。
Linux 中的 Reactor 模式
事件循环:
Reactor 模式通常包含一个事件循环,该循环不断地轮询所有事件,等待事件发生并处理它们。事件处理器:
事件处理器是处理特定事件的函数,它们通常与事件类型相关联。事件分派器:
事件分派器负责将事件分发给相应的事件处理器。事件源:
事件源是产生事件的实体,例如网络套接字、文件描述符等。流程图如下:
使用的是同步I/O模型。
1) 主线程往epol l 内核事件表中注册socket上的读就绪事件。
2) 主线程调用epol l _ wait等待 socket 上有数据可读。
3)当socket 上有数据可读时, epoll _ wait通知主线程。主线程则将socket 可读事件放入请求队列。
4)睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往epol l内核事件表中注册该socket 上的写就绪事件。
5) 主线程调用epoll _ wait等待 socket 可写。
6)当socket 可写时, epoll wait通知主线程。主线程将socket 可写事件放入请求队列。
7)睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
2.2. Proactor模式
Proactor 模式是一种事件驱动的网络编程模式,与 Reactor 模式类似,但它使用异步 I/O 操作来处理网络事件。在 Proactor 模式中,一个或多个线程负责监听网络事件,当事件发生时,例如新的连接请求、数据到达等,Proactor 模式会触发相应的处理函数来处理这些事件。
Linux 中的 Proactor 模式
事件循环:
Proactor 模式通常包含一个事件循环,该循环不断地轮询所有事件,等待事件发生并处理它们。事件处理器:
事件处理器是处理特定事件的函数,它们通常与事件类型相关联。事件分派器:
事件分派器负责将事件分发给相应的事件处理器。事件源:
事件源是产生事件的实体,例如网络套接字、文件描述符等。异步 I/O 操作:
Proactor 模式使用异步 I/O 操作来处理网络事件,这样可以减少线程间的上下文切换,提高系统的性能。具体流程图如下:
1)主线程调用aio _ read 函数向内核注册socket 上的读完成事件, 并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例,详情请参考sigevent的man手册)。
2)主线程继续处理其他逻辑。
3)当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio _ write函数向内核注册 socket上的写完成事件, 并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)。
5)主线程继续处理其他逻辑。
6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
这里给出一个代码例子:
事件处理函数:
// 事件处理函数void *handle_connection(void *socket_desc) { int sock = *(int*)socket_desc; char *message; int len; // 接收客户端数据 while((len = read(sock, message, 1024)) > 0) { printf("收到数据:%s\n", message); // 发送响应 write(sock, "Hello, Client!", 14); memset(message, 0, 1024); } // 关闭套接字 close(sock); return 0;}
循环逻辑:
while(1) { activity = epoll_wait(epollfd, events, MAX_EVENTS, -1); if ((activity < 0) && (errno != EINTR)) { printf("epoll_wait error"); return -1; } for (i = 0; i < activity; i++) { if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) { // 处理错误情况 close(events[i].data.fd); continue; } if (events[i].data.fd == sock) { // 有新的客户端连接 newsock = accept(sock, (struct sockaddr *)&cli_addr, (socklen_t*)&clilen); printf("新的客户端连接:%s\n", inet_ntoa(cli_addr.sin_addr)); client_sockets[i] = newsock; event.events = EPOLLIN; event.data.fd = newsock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, newsock, &event) == -1) { perror("epoll_ctl failed"); return -1; } } else { // 处理客户端数据 new_sock = malloc(1); *new_sock = events[i].data.fd; if (pthread_create(&thread_id, NULL, handle_connection, (void*)new_sock) < 0) { perror("could not create thread"); return -1; } pthread_detach(thread_id); } } } // 关闭 epoll 实例 close(epollfd); return 0;}
在这个例子中,服务器端使用 epoll
函数来实现 Proactor 模式,创建一个简单的服务器。服务器使用 pthread
库来创建多线程来处理多个客户端的连接,每个连接都由一个单独的线程处理。服务器收到客户端的请求后,发送一个响应,并关闭与客户端的连接。(不是完整代码哦!)
好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!