当前位置: 首页 > news >正文

网络编程:一个 TCP 服务器的简易实现(epoll 版本)

传统多线程模型的局限

让我们先看一个基于多线程的 TCP 服务器实现(注释部分):

// 多线程处理方式(注释部分)
while(1){struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(struct sockaddr_in));socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);pthread_t thread_id;pthread_create(&thread_id, NULL, client_routine, &clientfd);
}

这种模型的工作流程很简单:

  1. 服务器监听端口,等待客户端连接
  2. 每接受一个连接(accept),就创建一个新线程处理该连接
  3. 线程中通过 recv 循环读取客户端数据

然而,这种模型存在明显缺陷:

        资源消耗大:每个线程都需要独立的栈空间和系统资源

        上下文切换开销:大量线程会导致频繁的内核态 / 用户态切换

        扩展性差:在高并发场景下(如数千个连接),系统性能会急剧下降

epoll 多路复用模型的优势

为了解决多线程模型的局限,我们引入 epoll 多路复用技术,其核心优势在于:

       1.单线程(或少量线程)即可处理大量并发连接

        2.减少不必要的系统调用和上下文切换

        3.基于事件驱动,只处理活跃的连接

创建一个socket监听窗口

1. 先创建一个sockfd 用于监听指定端口

2. 并绑定再相应的ip和端口(服务器)上

为什么需要 INADDR_ANY?

一台服务器(比如你的 192.168.150.129)可能不止一个 IP 地址:

        物理网卡的真实 IP:192.168.150.129(局域网内可见);

        本地回环 IP:127.0.0.1(本机内部可见,比如自己连自己);

        如果服务器有多个网卡,还可能有 192.168.1.100、10.0.0.5 等其他 IP。

如果服务端绑定死某个具体 IP(比如 addr.sin_addr.s_addr inet_addr("192.168.150.129")),那它只能接收发送到「192.168.150.129: 端口」的连接,发送到 127.0.0.1: 端口的连接会被拒绝。

而 INADDR_ANY(即 0.0.0.0)是一个 “通配符 IP”,它告诉操作系统:“把发送到本机任意 IP 地址、且目标端口是我绑定的这个端口的连接,都交给我处理”。

		int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in addr;memset(&addr, 0, sizeof(struct sockaddr_in));addr.sin_family = AF_INET;addr.sin_port = htons(port+i); // 8888 8889 8890 8891 .... 8987addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {perror("bind");return 2;}if (listen(sockfd, 5) < 0) {perror("listen");return 3;}

bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)

相当于把这个套接字个他接到对应的端口上,就像把一个电话接到对应的电话线上一样。

listen(sockfd, 5)

listen相当于把这个电话打开,让他处于监听状态,此时就可以接受外面的连接了,参数5代表还未完全连接的一个半连接状态的存储队列此处等学三次时候再展开。

        int epfd = epoll_create(1);       

        struct epoll_event ev;

        ev.events = EPOLLIN;

        ev.data.fd = sockfd;

        epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

        sockfds[i] = sockfd;

现在我们就把刚刚的socketfd文件表述符纳入到epoll的管理范围内, ev.events = EPOLLIN;表示让epoll关注他是否可读的状态,也就是说看看这个关于这个文件描述是否有内容发送过来,对于这个fd而言其实就是看看有没有客户端发来的连接请求,epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);就是把他加入到这个监听的集 epfd 和中去。

epoll的监听并处理所有网络事件

int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1);

当 epoll_wait 的最后一个参数设为 -1 时,函数会让当前线程进入阻塞睡眠状态,直到有注册的事件发生(比如新连接、客户端发数据)才会被唤醒。

作用:阻塞等待 epoll 实例(epfd)中注册的事件发生    

参数说明

        epfdepoll实例的文件描述符(之前通过epoll_create` 创建);

        events`:输出参数,用于存储 “就绪事件” 的数组(发生了什么事件);

        EPOLL_SIZEevents` 数组的最大容量(最多处理多少个事件);

        -1:超时时间(-1` 表示无限等待,直到有事件发生)。

        返回值:nready 表示本次就绪的事件数量(有多少个事件需要处理)。

一部分是连接的请求

 if (events[i].data.fd == sockfd) {struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(struct sockaddr_in));socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);// 此处分水平触发和边缘触发,水平触发式只要io里面有数据就会触发,边缘触发是从无数据到有数据触发ev.events = EPOLLIN | EPOLLET;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);}

events[i].data.fd == sockfd; 表示目前活跃事件的fd是负责监听连接需求的socket套接字

int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); 此处是服务端针对于客户端发来的连接请求,单独为这个连接去创建一个只处理这个连接的文件描述符clientfd ,并让这个clientfd 文件专区全权去负责处理后面客户端发来的各种信息内容(此处执行完后表示3次握手已经完成并已经处于全连接状态)。

ev.events = EPOLLIN | EPOLLET;表示让关注clientfd 的可读缓冲区是否有内容,也就是关注客户端是否发来信息。

        EPOLLET表示的是边缘触发,表示clientfd中的可读缓冲区的数据从无到有这个状态会激活这个事件,从 “无数据” 变为 “有数据” 时,才通知一次(无论数据是否读完)。

        与之对应的LT水平触发,只要文件描述符有数据可读 / 可写,就持续通知(一次数据可能通知多次)

处理已有客户端的数据

int clientfd = events[i].data.fd;char buffer[BUFFER_LENGTH] = {0};int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);if (len < 0) {close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);break;} else if (len == 0) {close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);break;} else {printf("Recv : %s, %d byte(s)\n", buffer, len);}

已有客户端的数据则就是客户端发来的数据,直接用buffer读取即可,有时可能一次读不完,要反复读这里省略了没写循环读写,用 recv 从套接字(socket)的接收缓冲区读取数据时,操作系统内核会自动 “删除”(准确说是 “释放”)已经被成功读取的那部分数据,剩余未读的数据会继续保留在接收缓冲区中,等待下一次 recv 调用读取。这里要注意的就是边缘触发的话就不适合阻塞读写io的搭配了。ET 模式必须配合非阻塞 IO,否则毫无意义。

整体实现

#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>#define BUFFER_LENGTH 1024
#define EPOLL_SIZE 100void* client_routine(void* arg) {int clientfd = *(int*)arg;while (1) {char buffer[BUFFER_LENGTH] = {0};int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);if (len < 0) {close(clientfd);break;} else if (len == 0) {close(clientfd);break;} else {printf("Recv : %s, %d byte(s)\n", buffer, len);}}
}int main(int argc, char* argv[]) {if (argc < 2) {printf("Param error\n");return -1;}int port = atoi(argv[1]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in addr;memset(&addr, 0, sizeof(struct sockaddr_in));addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {perror("bind");return 2;}if (listen(sockfd, 5)) {perror("listen error");return 3;}// while(1){//     struct sockaddr_in client_addr;//     memset(&client_addr, 0, sizeof(struct sockaddr_in));//     socklen_t client_len = sizeof(client_addr);//     int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);//     pthread_t thread_id;//     pthread_create(&thread_id, NULL, client_routine, &clientfd);// }int epfd = epoll_create(1);struct epoll_event events[EPOLL_SIZE] = {0};struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while (1) {int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1); //-1int i = 0;for (i = 0; i < nready; i++) {if (events[i].data.fd == sockfd) {struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(struct sockaddr_in));socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);// 此处分水平触发和边缘触发,水平触发式只要io里面有数据就会触发,边缘触发是从无数据到有数据触发ev.events = EPOLLIN | EPOLLET;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else {int clientfd = events[i].data.fd;char buffer[BUFFER_LENGTH] = {0};int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);if (len < 0) {close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);break;} else if (len == 0) {close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);break;} else {printf("Recv : %s, %d byte(s)\n", buffer, len);}}}}return 0;
}

引用自0voice

http://www.dtcms.com/a/392944.html

相关文章:

  • 【MySQL学习】关于MySql语句执行、查询、更新流程原理总结
  • C++语法深度剖析与面试核心详解
  • 【Tomcat】基础总结:类加载机制
  • 127、【OS】【Nuttx】【周边】效果呈现方案解析:比较浮点数(上)
  • 计网协议簇具体协议
  • 电路分析基础笔记
  • 【JVM 常用工具命令大全】
  • 从iload_1 iload_2 iadd字节码角度看jvm字节码执行
  • openssl 启用AES NI加速对AES加密性能影响的测试
  • LeetCode:32.随机链表的复制
  • 基于SpringBoot+Vue的旅游系统【协同过滤推荐算法+可视化统计】
  • 前端实现一个星空特效的效果(实战+讲解)
  • 【嵌入式】【科普】软件模块设计简介
  • 【ROS2】ROS2通讯机制Topic常用命令行
  • 欧姆龙NJ系列PLC编程标准化案例
  • 【OpenGL】LearnOpenGL学习笔记25 - 法线贴图 NormalMap
  • UE5 基础应用 —— 09 - 行为树 简单使用
  • 客户端实现信道管理
  • 异常解决记录 | Yarn NodeManager 注册异常
  • 【C#】C# 调用 Python 脚本正确姿势:解决 WaitForExit 死锁与退出检测问题
  • Java25新特性
  • 卷积神经网络CNN-part9-DenseNet
  • 深入浅出密码学第一章课后题(持续更新)
  • Mysql 入门概览
  • 大模型中权重共享的作用?
  • 【精品资料鉴赏】55页可编辑PPT详解 数字化高校智慧后勤解决方案
  • LLM大模型 - 实战篇 - AI Agents的开发应用
  • 【分布式技术】RedisShake相关功能详细介绍
  • qsv:一款高性能的CSV数据处理工具
  • `html` 将视频作为背景