多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

IO 多路转接方式比较:


常见的 IO 多路转接方式有:select、poll、epoll,他们的区别为:

  • select 可以跨平台,Linux、Mac、Windows 都支持,而 poll 和 epoll 只能在 Linux 上使用
  • epoll 底层为红黑树,select 和 poll 底层为线性表,epoll 的效率较高
  • select 连接的设备上限为 1024,poll 和 epoll 没上限,取决于当前操作系统的配置


IO 多路转接本质:

  • 在服务器端有两类文件描述符,分别对应一个读缓冲区和写缓冲区
  • 用于监听的文件描述符对应的读缓冲区主要用来存储客户端的连接请求,当调用 accept 时会检测这个读缓冲区是否有连接请求
  • 用于通信的文件描述符对应的读缓冲区用于存储客户端发送来的数据,服务器调用 read 方法能够将数据读取出来,写缓冲区用于服务器通过 write 写入的数据
  • 当只有一个线程时,accpet、read、write 只要有一个阻塞,就不能继续运行了
  • IO 多路转接实际上就是将本该由用户进行的文件描述符读/写缓冲区的检测交给了内核,内核可以同时检测若干个文件描述符以及它们的读/写缓冲区,检测读缓冲区是否有数据/检测写缓冲区是否有剩余的空间,当条件满足时,内核会告知用户相关信息(可操作的文件描述符),此时 accpet、read、write 就不会阻塞了,若内核通知多个文件描述符,在用户空间处理时是按顺序处理的

select

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数

  2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

#include <sys/select.h>

/* According to earlier standards */

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

// 返回值就绪描述符的数目

int select(int nfds, fd_set *readfds, fd_set *writefds,

fd_set *exceptfds, struct timeval *timeout);

nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态

readfds: 监控有读数据到达文件描述符集合,传入传出参数

writefds: 监控写数据到达文件描述符集合,传入传出参数

exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数

timeout: 定时阻塞监控时间,3种情况

1.NULL,永远等下去

2.设置timeval,等待固定时间

3.设置timeval里时间均为0,检查描述字后立即返回,轮询

struct timeval {

long tv_sec; /* seconds */

long tv_usec; /* microseconds */

};

void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0

int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1

void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1

void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

示例图如下:

这个图的意思是,应用层先把要监听的文件描述符做标记1,之后再将其拷贝一份,将拷贝的这一份文件描述符在拷贝到内核中,让内核监听这些做标记的文件描述符,如果被监听的文件没有变化,那么内核中的文件描述的标记就会被抹消,然后在将改变的文件描述符集合复制到应用层,让其对改变的文件描述符进行读取,

例如应用层准备监听4567这四个文件描述符,复制到内核去监听,内核发现只有5号发生了改变,所以告知应用层去5号文件描述符读取数据,如果是lfd即4发生了变化,就说明有新的连接产生了

server

流程图示:

#include <stdio.h>
#include <sys/select.h> // select多路复用API
#include <sys/types.h>	// 基本系统数据类型
#include <unistd.h>		// POSIX API(read/write/close等)
#include "wrap.h"		// 自定义错误处理函数封装
#include <sys/time.h>#define PORT 8888 // 服务器监听端口int main(int argc, char *argv[])
{// 创建TCP套接字并绑定端口int lfd = tcp4bind(PORT, NULL);// 设置监听队列长度为128Listen(lfd, 128);int maxfd = lfd;	 // 初始化最大文件描述符(当前只有监听套接字)fd_set oldset, rset; // 定义两个fd_set:// oldset:永久记录所有需监控的fd// rset:每次select调用传入的临时集合FD_ZERO(&oldset); // 清空文件描述符集合FD_ZERO(&rset);FD_SET(lfd, &oldset); // 将监听套接字加入监控集合while (1){rset = oldset; // 复制永久集合到临时集合(select会修改传入的集合)// 核心:阻塞监听所有文件描述符的可读事件int n = select(maxfd + 1, &rset, NULL, NULL, NULL);// 错误处理if (n < 0){perror("select error");break;}else if (n == 0){ // 无事件发生(超时)continue;}// 处理监听套接字事件(新连接到达)查看lfd监听描述符是否在就绪的rset集合中,在表示有新连接if (FD_ISSET(lfd, &rset)){ // 检查监听套接字是否就绪struct sockaddr_in cliaddr;socklen_t len = sizeof(cliaddr);char ip[16] = "";// 接受新连接int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);printf("new client ip=%s port=%d\n",inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),ntohs(cliaddr.sin_port));// 将新连接加入永久监控集合FD_SET(cfd, &oldset);// 更新最大文件描述符if (cfd > maxfd)maxfd = cfd;// 若已无其他事件,跳过后续处理if (--n == 0)continue;}// 处理已连接套接字的数据事件//在 Unix/Linux 中,文件描述符按从小到大的顺序分配。lfd 是服务器启动时最早创建的套接字,其值通常为3(0-2 被标准输入/输出/错误占用),后续 cfd 依次递增(4, 5, ...)//因此 lfd + 1 自然指向第一个客户端连接套接字。for (int i = lfd + 1; i <= maxfd; i++){if (FD_ISSET(i, &rset)){ // 检查当前fd是否就绪char buf[1500] = "";int ret = Read(i, buf, sizeof(buf)); // 读取数据// 错误处理if (ret < 0){perror("read error");close(i);FD_CLR(i, &oldset); // 从监控集合移除}// 客户端关闭连接else if (ret == 0){struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("client%d close\n",remote_port);close(i);FD_CLR(i, &oldset);}// 正常数据处理else{struct sockaddr_in remote_addr;socklen_t len = sizeof(remote_addr);getpeername(i, (struct sockaddr *)&remote_addr, &len);int remote_port = ntohs(remote_addr.sin_port);printf("客户端%d:%s\n", remote_port,buf);Write(i, buf, ret); // 回显数据}}}}return 0;
}

几个问题?

1.select(maxfd + 1, &rset, NULL, NULL, NULL);为什么要maxfd + 1?

fd_set rset;
int maxfd = 5;  // 当前最大FD为5
FD_ZERO(&rset);
FD_SET(3, &rset);  // 监控FD=3
FD_SET(5, &rset);  // 监控FD=5

// 内核会检查0~5的FD(共6个),但仅FD=3和5实际被监控
select(5 + 1, &rset, NULL, NULL, NULL);

若误传 maxfd=5(未+1),内核可能漏检FD=5,导致数据就绪却未被触发

2.int n = select(maxfd + 1, &rset, NULL, NULL, NULL);这里是怎样遍历文件描述符集合的?是从0开始遍历rset里的文件描述符吗?

  • 在 select 函数中,内核遍历文件描述符集合(rset)的方式是通过线性扫描位图,从文件描述符 ​0​ 开始,依次检查每个比特位是否被置位(即是否为1),直到达到 maxfd + 1 指定的范围。从0到 maxfd,无论文件描述符是否打开或活跃。这种设计简单但效率低,是 select 被 epoll 取代的主要原因之一

3.select(maxfd + 1, &rset, NULL, NULL, NULL);里的rset作用是什么

  • 在 select 函数中,rset 是一个 fd_set 类型的位图集合,其核心作用是标识需要监控的可读文件描述符(FD)集合,并在函数返回时标记哪些FD已就绪可读

4.for (int i = lfd + 1; i <= maxfd; i++) 为什么从 lfd + 1开始遍历?

  • lfd 通常是较小的值:
    在 Unix/Linux 中,文件描述符按从小到大的顺序分配。lfd 是服务器启动时最早创建的套接字,其值通常为3(0-2 被标准输入/输出/错误占用),后续 cfd 依次递增(4, 5, ...)。

  • 因此 lfd + 1 自然指向第一个客户端连接套接字。

client

客户端使用:

nc 127.0.0.1 8888

模拟客户端链接服务器

结果显示如下:

但这样有个问题,无论连接服务器的客户端是否活跃,遍历时都会遍历这些连接的客户端,所以这就会引发一个问题(大量并发,少了活跃):

假设现在 4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了,遍历时还是要从4-1023进行遍历,实际只需要遍历4、1001-1023即可。

假设现在 4-1023个文件描述符需要监听,但是只有 5,1002 发来消息,遍历时还是要从4-1023进行遍历,实际只需要遍历5、1002即可。

select进阶优化版

之前的代码,如果最大fd是1023,每次确定有事件发生的fd时,就要扫描3-1023的所有文件描述符,这看起来很蠢。于是定义一个数组,把要监听的活跃的文件描述符存下来,每次扫描这个数组就行了。看起来科学得多。

server

//进阶版select,通过数组防止遍历1024个描述符
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>#include "wrap.h"#define SERV_PORT 8888int main(int argc, char *argv[])
{int i, j, n, maxi;int nready, client[FD_SETSIZE];                 /* 自定义数组client, 防止遍历1024个文件描述符  FD_SETSIZE默认为1024 */int maxfd, listenfd, connfd, sockfd;char buf[BUFSIZ], str[INET_ADDRSTRLEN];         /* #define INET_ADDRSTRLEN 16 */struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;fd_set rset, allset;                            /* rset 读事件文件描述符集合 allset用来暂存 */listenfd = Socket(AF_INET, SOCK_STREAM, 0);//端口复用int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family= AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port= htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);maxfd = listenfd;                                           /* 起初 listenfd 即为最大文件描述符 */maxi = -1;                                                  /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1;                                         /* 用-1初始化client[] */FD_ZERO(&allset);FD_SET(listenfd, &allset);                                  /* 构造select监控文件描述符集 */while (1) {   rset = allset;                                          /* 每次循环时都从新设置select监控信号集 */nready = select(maxfd+1, &rset, NULL, NULL, NULL);      //2  1--lfd  1--connfdif (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */clie_addr_len = sizeof(clie_addr);connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {                            /* 找client[]中没有使用的位置 */client[i] = connfd;                         /* 保存accept返回的文件描述符到client[]里 */break;}if (i == FD_SETSIZE) {                              /* 达到select能监控的文件个数上限 1024 */fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */if (connfd > maxfd)maxfd = connfd;                                 /* select第一个参数需要 */if (i > maxi)maxi = i;                                       /* 保证maxi存的总是client[]最后一个元素下标 */if (--nready == 0)continue;} for (i = 0; i <= maxi; i++) {                               /* 检测哪个clients 有数据就绪 */if ((sockfd = client[i]) < 0)continue;//数组内的文件描述符如果被释放有可能变成-1if (FD_ISSET(sockfd, &rset)) {if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {    /* 当client关闭链接时,服务器端也关闭对应链接 */Close(sockfd);FD_CLR(sockfd, &allset);                        /* 解除select对此文件描述符的监控 */client[i] = -1;} else if (n > 0) {for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);Write(STDOUT_FILENO, buf, n);}if (--nready == 0)break;                                          /* 跳出for, 但还在while中 */}}}Close(listenfd);return 0;
}

若 lfd = 3maxfd = 100,但仅 5 个活跃连接,基础版仍需循环 97 次(4~100),而进阶版仅需循环 5 次(数组中的活跃 FD)。

局限性


虽然使用 select 这种 IO 多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:

待检测集合(第 2、3、4 个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低

内核对于 select 传递进来的待检测集合的检测方式是线性的

检测效率与集合内待检测的文件描述符有关:如果集合内待检测的文件描述符很多,检测效率会比较低;如果集合内待检测的文件描述符相对较少,检测效率会比较高

使用 select 能够检测的最大文件描述符个数有上限,默认是 1024,这是在内核中被写死了的

poll

poll 的机制与 select 类似,与 select 在本质上没有多大差别,使用方法也类似,下面的是对于二者的对比:

  • 内核对应文件描述符的检测也是以线性的方式进行轮询,根据描述符的状态进行处理
  • poll 和 select 检测的文件描述符集合会在检测过程中频繁的进行用户区和内核区的拷贝,它的开销随着文件描述符数量的增加而线性增大,从而效率也会越来越低。
  • select 检测的文件描述符个数上限是 1024,poll 没有最大文件描述符数量的限制
  • select 可以跨平台使用,poll 只能在 Linux 平台使用
  • select 通过 fd_set(位图集合)来记录文件描述符,poll 使用一个整型数来记录


poll 函数

#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
    int   fd;         /* 委托内核检测的文件描述符 */
    short events;     /* 委托内核检测文件描述符的什么事件 */
    short revents;    /* 文件描述符实际发生的事件 -> 传出 */  // 不需要进行初始化
};

struct pollfd myfd[100];   // 可能需要检测若干个文件描述符,要存储在数组中
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 


参数含义:


fds: 这是一个 struct pollfd 类型的数组, 里边存储了待检测的文件描述符的信息,这个数组中有三个成员:

fd:委托内核检测的文件描述符
events:委托内核检测的 fd 事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果(不需要初始化,根据 events 委托内核检测的时间传出结果)

nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数 1 数组的元素总个数)

timeout: 指定 poll 函数的阻塞时长

-1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
大于 0:阻塞指定的毫秒(ms)数之后,解除阻塞


返回值:成功返回集合中已就绪的文件描述符的总个数,失败返回-1

server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h" // 自定义的包裹函数头文件(如Socket、Bind等)#define MAXLINE 80     // 缓冲区大小
#define SERV_PORT 6666 // 服务器端口
#define OPEN_MAX 1024  // 最大文件描述符数量int main(int argc, char *argv[])
{int i, j, maxi, listenfd, connfd, sockfd;int nready;                              // poll返回的就绪文件描述符数量ssize_t n;                               // 读取的字节数char buf[MAXLINE], str[INET_ADDRSTRLEN]; // 缓冲区和IP地址字符串socklen_t clilen;                        // 客户端地址长度struct pollfd client[OPEN_MAX];          // poll监控的文件描述符数组struct sockaddr_in cliaddr, servaddr;    // 客户端和服务器地址结构/* 1. 创建监听套接字 */listenfd = Socket(AF_INET, SOCK_STREAM, 0); // IPv4 TCP套接字/* 2. 绑定服务器地址 */bzero(&servaddr, sizeof(servaddr));           // 清空结构体servaddr.sin_family = AF_INET;                // IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有本地IPservaddr.sin_port = htons(SERV_PORT);         // 设置端口Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* 3. 开始监听 */Listen(listenfd, 20); // 监听队列最大长度为20/* 4. 初始化poll监控数组 */client[0].fd = listenfd;       // 第一个元素是监听套接字client[0].events = POLLRDNORM; // 监听普通读事件(新连接)for (i = 1; i < OPEN_MAX; i++)client[i].fd = -1; // 其余元素初始化为-1(表示空闲)maxi = 0;              // 当前client数组中有效元素的最大下标/* 5. 主循环:处理poll事件 */for (;;){// 阻塞等待事件发生,监控maxi+1个描述符(从0到maxi),无限等待nready = poll(client, maxi + 1, -1);/* 5.1 处理监听套接字(新连接) */if (client[0].revents & POLLRDNORM) // 监听套接字可读(有新连接){clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); // 打印客户端IP和端口/* 将新连接加入client数组 */for (i = 1; i < OPEN_MAX; i++){if (client[i].fd < 0){                          // 找到第一个空闲位置client[i].fd = connfd; // 存储新连接的描述符break;}}if (i == OPEN_MAX) // 超过最大连接数限制perr_exit("too many clients");client[i].events = POLLRDNORM; // 对新连接监控读事件if (i > maxi)maxi = i; // 更新最大有效下标if (--nready <= 0) // 如果没有更多就绪事件,继续pollcontinue;}/* 5.2 处理已连接套接字的数据 */for (i = 1; i <= maxi; i++) // 遍历所有可能的连接{if ((sockfd = client[i].fd) < 0) // 跳过无效描述符continue;/* 检查读事件或错误事件 */if (client[i].revents & (POLLRDNORM | POLLERR)){n = Read(sockfd, buf, MAXLINE); // 读取数据if (n < 0){// 读取错误if (errno == ECONNRESET) // 客户端发送RST重置连接{printf("client[%d] aborted connection\n", i);Close(sockfd);client[i].fd = -1; // 重置为未使用}else{perr_exit("read error"); // 其他错误直接退出}}else if (n == 0) // 客户端关闭连接{printf("client[%d] closed connection\n", i);Close(sockfd);client[i].fd = -1;}else // 正常读取数据{for (j = 0; j < n; j++) // 转为大写buf[j] = toupper(buf[j]);Writen(sockfd, buf, n); // 回写给客户端}if (--nready <= 0) // 没有更多就绪事件,跳出循环break;}}}return 0;
}

client

客户端使用:

nc 127.0.0.1 6666

模拟客户端链接服务器

epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

目前epell是linux大规模并发网络程序中的热门首选模型。

epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

可以使用cat命令查看一个进程可以打开的socket描述符上限。

cat /proc/sys/fs/file-max

如有需要,可以通过修改配置文件的方式修改该上限值。

sudo vi /etc/security/limits.conf

在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。

* soft nofile 65536

* hard nofile 100000

基础API

1.创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。

#include <sys/epoll.h>

int epoll_create(int size) size:监听数目

2.控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epfd: 为epoll_creat的句柄

op: 表示动作,用3个宏来表示:

EPOLL_CTL_ADD (注册新的fd到epfd),

EPOLL_CTL_MOD (修改已经注册的fd的监听事件),

EPOLL_CTL_DEL (从epfd删除一个fd);

event: 告诉内核需要监听的事件

struct epoll_event {

__uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

};

typedef union epoll_data {

void *ptr;

int fd;

uint32_t u32;

uint64_t u64;

} epoll_data_t;

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

EPOLLOUT: 表示对应的文件描述符可以写

EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

EPOLLERR: 表示对应的文件描述符发生错误

EPOLLHUP: 表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3.等待所监控文件描述符上有事件的产生,类似于select()调用。

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

events: 用来存内核得到事件的集合,

maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

timeout: 是超时时间

-1: 阻塞

0: 立即返回,非阻塞

>0: 指定毫秒

返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

server

处理流程:


1.创建 epoll 实例对象 epoll_create

2.将用于监听的套接字添加到 epoll 实例中 epoll_ctl

3.检测添加到 epoll 实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理 epoll_wait

  • 如果是监听的文件描述符,和新客户端建立连接,将得到的文件描述符添加到 epoll 实例中
  • 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从 epoll 实例中删除
     
#include <stdio.h>
#include <fcntl.h>
#include "wrap.h"
#include <sys/epoll.h>
int main(int argc, char *argv[])
{//创建套接字 绑定int lfd = tcp4bind(8000,NULL);//监听Listen(lfd,128);//创建树int epfd = epoll_create(1);//将lfd上树struct epoll_event ev,evs[1024];ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);//while监听while(1){int nready = epoll_wait(epfd,evs,1024,-1);//监听printf("epoll wait _________________\n");if(nready <0){perror("");break;}else if( nready == 0){continue;}else//有文件描述符变化{   int i = 0;for( i=0;i<nready;i++){//判断lfd变化,并且是读事件变化if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN){struct sockaddr_in cliaddr;char ip[16]="";socklen_t len = sizeof(cliaddr);int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);//提取新的连接printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));//设置cfd为非阻塞int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位flags |= O_NONBLOCK;fcntl(cfd,F_SETFL,flags);//将cfd上树ev.data.fd =cfd;ev.events =EPOLLIN | EPOLLET;//设置为边沿触发epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else if( evs[i].events & EPOLLIN)//cfd变化 ,而且是读事件变化{while(1){char buf[4]="";//如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果//是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAINint n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听if(errno == EAGAIN){break;}//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}}}}}return 0;
}

client

客户端使用:

nc 127.0.0.1 8000

模拟客户端链接服务器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87312.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87312.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/87312.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

最新临时文件快传系统源码 轻量化 带后台

简介&#xff1a; 最新临时文件快传系统源码 轻量化 带后台 首发 轻松上传文件并生成提取码分享给他人&#xff0c;无需注册&#xff0c;方便快捷。 图片&#xff1a;

MyBatis多数据源动态连接工具类实现

这个DatabaseService工具类提供了动态创建MyBatis SqlSession的能力&#xff0c;可以灵活地连接到不同的数据库&#xff0c;非常适合需要动态切换数据源的场景。 package com.cmes.immp.device.utils;import lombok.SneakyThrows; import org.apache.commons.dbcp2.BasicDataS…

用亮数据 MCP 驱动 Trae 智能体:打造高效亚马逊商品采集与分析助手

本文适合希望快速构建数据驱动型智能体的开发者、数据工程师及 AI 产品设计者阅读 并非广告&#xff0c;希望本文可以帮助有需求的同学&#xff0c;祝大家天天开心 在数字时代&#xff0c;数据是决策与洞察趋势的关键。但移动互联网数据获取不易&#xff0c;传统爬虫技术面对复…

如何降低AIGC生成内容的重复率?五种免费降AI率的方法 (25年更新)

随着AI生成内容&#xff08;AIGC&#xff09;的普及&#xff0c;越来越多的学术写作依赖AI工具来生成论文和文章。然而&#xff0c;AI生成内容的查重率常常偏高&#xff0c;导致很多论文无法通过学术查重系统。为了解决这一问题&#xff0c;以下是五种有效的免费降AIGC率的方法…

小米YU7使用UWB技术,厘米级定位精准迎宾,安全防破解无感控车

当您双手抱着快递走向爱车时&#xff0c;车门自动解锁&#xff1b;当您站在前备箱前稍作停留&#xff0c;箱盖优雅升起——这不是科幻电影&#xff0c;而是小米YU7搭载UWB技术带来的真实体验。在2025年5月的小米15周年战略新品发布会上&#xff0c;雷军揭晓了这项革命性技术&am…

WPF学习(动画)

文章目录 一、图像变换 RenderTransform1、常见变换类型2、RenderTransform 的核心作用3、RenderTransform 的使用方式4、与 LayoutTransform 的对比5、在动画中的应用 二、 滚动的椭圆三、Storyboard放置位置1. **元素的 Resources 集合**2. **控件模板&#xff08;ControlTem…

Crossbar结构的排队策略

目录 一、概述 二、排队策略 三、输入排队结构(IQ) 3.1 结构特点 3.2 改进方案 四、输出排队结构&#xff08;OQ&#xff09; 五、输入输出联合排队结构(CIOQ) 六、输入交叉节点联合排队结构(CICQ) 一、概述 Crossbar是一种全连接的交换结构&#xff0c;由 MN 个交叉…

状态模式 - Flutter中的状态变身术,让对象随“状态“自由切换行为!

订单状态流转/播放器控制/游戏角色行为…一个模式搞定所有状态驱动型逻辑&#xff01; 经典场景&#xff1a;订单状态管理 假设你在开发一个外卖App&#xff0c;订单有以下状态&#xff1a; 等待接单已接单配送中已完成已取消 每个状态下&#xff1a; 显示的UI不同可执行的…

数据库9:数据库字符编码调整与校队(排序)规则

一.常用字符编码 1.ASCII编码 用一个字节表示一个字符 2.ANSI编码 每个国家为了显示本国的语言而对ASCII码进行了拓展 用两个字节表示一个汉字&#xff0c;中国的ANSI编码是GB2312编码&#xff08;简体&#xff09;&#xff0c;日本的ANSI编码是JIS编码&#xff0c;台湾的A…

人脸活体识别4:Android实现人脸眨眼 张嘴 点头 摇头识别(可实时检测)

人脸活体识别4&#xff1a;Android实现人脸眨眼 张嘴 点头 摇头识别(可实时检测) 目录 人脸活体识别4&#xff1a;Android实现人脸眨眼 张嘴 点头 摇头识别(可实时检测) 1. 前言 2.人脸活体识别方法 &#xff08;1&#xff09;基于人脸动作的检测​​ &#xff08;2&…

DAY1-Linux操作系统1

文章参考【黑马程序员Python教程_600集Python从入门到精通教程&#xff08;懂中文就能学会&#xff09;】 https://www.bilibili.com/video/BV1ex411x7Em/?p40&share_sourcecopy_web&vd_source263bbee2ddeb835c3ab6d9d3c80e0f7c 一.常用命令简单介绍 使用软件 虚拟机…

第十二节:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入门 - 两种权限控制方式(附前后端代码)

Vben5 系列文章目录 💻 基础篇 ✅ 第一节:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入门 ✅ 第二节:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入门 - Python Flask 后端开发详解(附源码) ✅ 第三节:Vben Admin 最新 v5.0 (vben5) + Python Flask 快速入…

华为云Flexus+DeepSeek征文 | 华为云 ModelArts Studio 赋能 AI 法务:合同审查与法律文件生成系统

一、引言 在法律行业数字化转型的浪潮中&#xff0c;AI 技术正重塑法律服务的流程与效率。本文介绍如何利用华为云 ModelArts Studio 构建一套完整的 AI 法务系统&#xff0c;实现合同审查、法律文件生成、法律咨询与风险识别的智能化解决方案。 二、系统架构设计 &#xff0…

SQL的底层逻辑解析

SQL的底层逻辑涉及数据库管理系统(DBMS)如何解析、优化和执行SQL查询&#xff0c;主要包括以下几个层面&#xff1a; ​查询处理流程​ 解析器(Parser)&#xff1a;将SQL语句转换为语法树查询优化器(Optimizer)&#xff1a;基于统计信息和成本模型生成最优执行计划执行引擎(Exe…

深入剖析AI大模型:PyTorch 技术详解

今天说一说PyTorch。作为一名python程序员&#xff0c;可能对它了解起来还是很快的。在人工智能浪潮席卷全球的当下&#xff0c;深度学习作为其核心技术&#xff0c;被广泛应用于图像识别、自然语言处理、语音识别等多个领域。而在深度学习的开发框架中&#xff0c;PyTorch 凭借…

物联网架构:定义、解释和实例

物联网&#xff08;IoT&#xff09;架构是一个复杂且多维度的概念&#xff0c;构成了物联网系统的核心框架。它是勾勒物联网设备、应用程序和技术如何相互交互以实现预期功能的蓝图。物联网架构并非 “一刀切” 的模型&#xff0c;而是会根据相关物联网系统的具体需求而有所不同…

拿到一台新服务器,怎么跑AI项目

公司新采购一台AI服务器&#xff0c;花大本钱装了个A6000显卡&#xff0c;今天来记录下新服务的使用步骤。 1、查看系统。 这台服务器预装了Ubuntu20.04系统。 lsb_release -a 查看下cpu、内存情况 top 看着还行。 再看下硬盘空间 df -h 空间不算小&#xff0c;2T。 2、…

IO--进程实操

1.创建一个进程扇 #include <051head.h> int main(int argc, const char *argv[]) {pid_t pid;for(int i0;i<4;i){pidfork();if(pid-1) //父进程{ERRLOG("fork error..\n");} else if(pid0) //这是子进程{ …

模型预测控制(MPC)概览

模型预测控制&#xff08;Model Predictive Control, MPC&#xff09; 一、理论基础与发展脉络 1. 历史起源 20世纪70年代起源于工业过程控制&#xff08;如化工领域的动态矩阵控制DMC、模型算法控制MAC&#xff09;&#xff0c;由Richalet、Mehra等学者提出&#xff0c;核心…

Python初体验:从入门到实践

Python无疑是开启编程世界大门的绝佳钥匙。今天,就让我们一起踏上Python的学习之旅。 #01 编写第一个Python程序 环境搭建好之后,上节已经编写了第一个Python程序。现在就好比,我们已经准备好了厨房和食材,要开始做第一道菜了。启动Jupyter后,在Jupyter中新建一个文件,…