第2章 高性能网络设计 - I/O多路复用

本文深入探讨了Linux后端服务器架构开发中的I/O多路复用技术,包括阻塞与非阻塞I/O模型、I/O多路复用、select()、poll()和epoll()系统调用。详细阐述了各种模型的工作原理、优缺点,并通过实例展示了epoll在多线程服务器中的应用,强调了epoll在性能和灵活性上的优势。
摘要由CSDN通过智能技术生成

标题:Linux C 和 C++ 后端服务器架构开发

章节:第2章 高性能网络设计 —— I/O 多路复用

作者:嘉措一郎(Gyatso Ichiro)

时间:2021-11-25

邮箱:GeekDerverloper.org@outlook.com

标签:技术博客


2.2 I/O 多路复用


重点内容

  • 网络 I/O 模型、I/O 多路复用
  • 水平触发和边缘触发
  • select() 系统调用
  • poll() 系统调用
  • epoll() 编程接口

2.2.1 网络 I/O 模型与 I/O 多路复用

Unix 有五种 I/O 模型:阻塞 I/O 模型(blocking I/O model)、非阻塞 I/O 模型(non-blocking model)、多路复用 I/O 模型(I/O multiplexing model)、信号驱动式 I/O 模型(signal blocking I/O model)、异步 I/O 模型(asynchronous I/O model)

  • (1) 阻塞与非阻塞的概念
  • 阻塞:阻塞调用是指调用的 I/O 函数没有完成相关的功能,在调用结果返回之前,当前进程被挂起。函数只有在得到结果之后才会返回

    阻塞型 I/O 模型一般适用于单个设备的操作或者不是特别紧急传输数据时,例如:管道设备、终端设备、单客户端的网络设备,阻塞 I/O 最常用、最简单、效率也最低

    嘉措一郎(Gyatso Ichiro)
  • 非阻塞:非阻塞调用是指当请求的 I/O 操作不能完成时,即使不能立刻返回调用结果也不会阻塞当前进程,而是立即返回

    非阻塞 I/O 模型适用于 I/O 多路复用(一个进程处理多路数据),非阻塞 I/O 防止阻塞在 I/O 上,需要轮询

    嘉措一郎(Gyatso Ichiro)
  • (2) 多路复用 I/O 模型

I/O 多路复用允许同时检查多个文件描述符的一种机制

  • 内核添加一张表,监听表里面的信息,当有资源准备就绪,就执行
  • 资源 --文件描述符去除与否
  • 创建监听表
    嘉措一郎(Gyatso Ichiro)
  • (3) 信号驱动式 I/O 和异步 I/O 模型
  • 在信号驱动 I/O 中,文件描述符上可执行 I/O 操作时,进程请求内核为自己发送一个信号,之后进程就可以执行任何其它的任务直到 I/O 就绪为止,此时内核会发送信号给进程,要使用信号驱动 I/O ,程序需要按照如下步骤执行:
  • 为内核发送的通知信号安装一个信号处理例程
  • 设定文件描述符的属主,也就是当文件描述符上可执行 I/O 时,会收到通知信号的进程或进程组,fcntl(fd, F_SETOWN, pid);
  • 通过设定 O_NONBLOCK 标志使能非阻塞 I/O
  • 通过打开 O_ASYNC 标志使能信号驱动 I/O
  • 调用进程可以执行其它的任务了
  • 信号驱动 I/O 提供的是边缘触发通知
  • 异步 I/O 模型
  • 数据从磁盘加载到buffer中,再将buffer中的数据复制到用户进程缓存区。当数据复制完成后,内核会发生一个信号给进程,这样进程会在用户进程缓存区中读取数据,而后再返回给客户端。其工做原理如图所示
    嘉措一郎(Gyatso Ichiro)
  • 异步 I/O 模型与信号驱动 I/O 模型的主要区别在于;信号驱动模型是由内核告知什么时候启动 I/O 操做;而异步 I/O 模型是由内核告知 I/O 操做什么时候完成

2.2.2 水平触发和边缘触发

水平触发和边缘触发是文件描述符准备就绪的两种通知模式

  • 水平触发通知:如果文件描述符上可以非阻塞地执行 I/O 系统调用,此时认为它已经就绪
  • 边缘触发通知:如果文件描述符自上次状态检查以来有了新的 I/O 活动(比如新的输入),此时需要触发通知

2.2.3 select()系统调用

  • 系统调用 selec t会一直阻塞,直到一个或多个文件描述符集合成为就绪态

  • (1) 函数原型


    #include <sys/select.h>

    int select(int nfds, fd_set *readfds, fd_set *writefds,
                        fd_set *exceptfds, struct timeval *timeout);

  • (2) 函数参数
  • nfds:文件描述符的范围,即所有文件描述符的最大值加 1,不能出错
  • readfds:监听读资源的文件描述符集合
  • writefds:监听写资源的文件描述符集合
  • exceptfds:监听异常资源的文件描述符集合
    -timeout:NULL 一直等待, 或者根据 struct timeval 设置等待时间的上限值

        struct timeval {
            long    tv_sec;         /* seconds */  /*  秒  */
            long    tv_usec;        /* microseconds */  /*  微妙  */
        };

readfds、writefds 和 exceptfds 所指向的结构体都是保存结果值的地方,在调用 select() 之前,这些参数指向的结构体必须初始化(通过 FD_ZERO() 和 FD_SET())

  • (3) 返回值:成功返回已经准备就绪的文件描述符个数, 失败返回 -1
  • 返回 -1 表示有错误发生
  • 返回 0 表示在任何文件描述符成为就绪态之前 select() 调用已经超时
  • 返回一个正整数表示有一个或多个文件描述符已经处于就绪态

通常数据类型 fd_set 以位掩码的形式来实现


    void FD_CLR(int fd, fd_set *set);    /*  将文件描述符 fd 从 set 所指向的集合中移除 */
    int  FD_ISSET(int fd, fd_set *set);  /*  判断文件描述符 fd 是否在 srt 所指向的集合中  */
    void FD_SET(int fd, fd_set *set);    /*  将文件描述 fd 添加到 set 所指向的集合中  */
    void FD_ZERO(fd_set *set);           /*  将 set 所指向的集合初始化为空  */

注意

select 正确返回时,会将准备好的文件描述符在集合中对应的位置置 1,其它位置全部置 0,为了保证任然可以监听其它没有 ready 的描述符,必须先将之前的集合保存下来

  • 在 Linux 上,异常情况只会在以下两种情况下发生:

    • 连接到处于信包模式下的伪终端主设备上的从设备状态发生了改变
    • 流式套接字上接收到了带外数据
  • 结构体 fd_set 的大小


        linux-5.15.2\include\linux\types.h
        typedef __kernel_fd_set		fd_set;

        linux-5.15.2\include\uapi\linux\posix_types.h
        #define __FD_SETSIZE	1024

        typedef struct {
	        unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
        } __kernel_fd_set;

  • (3) select() 系统调用的基本原理
    嘉措一郎(Gyatso Ichiro)

2.2.4 poll()系统调用

  • 系统调用 poll() 执行的任务同 select() 相似,主要区别在于如何指定待检查的文件描述符

  • 在 select() 中,提供三个集合,在每个集合中标明所需的文件描述符

  • poll() 管理多个文件描述符进行轮询操作(查询文件描述符,如果有指定的事件发生立刻返回),根据文件描述的状态进行处理,一般通过返回值来确定事件是否发生,没有文件描述符个数的限制

  • poll() 指定时间内轮询指定文件描述符,如果有指定事件发生返回一个真值

  • (1) 函数原型


    #include <poll.h>

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  • (2) 函数参数
  • 参数 fds 列出了需要 poll() 来检查的文件描述符,该参数为 pollfd 结构体:
        struct pollfd {
            int   fd;         /* file descriptor */    /*  文件描述符  */
            short events;     /* requested events */   /*  请求事件,位掩码  */
            short revents;    /* returned events */    /*  返回事件,位掩码  */
        };

  • 参数 nfds 指定了结构体 fds 中元素的个数,nfds_t 实际为无符号整型
  • 参数 timeout 决定了 poll() 的阻塞行为:
    • 如果 timeout 等于 -1,poll() 会一直阻塞直到 fds 中列出的文件描述符有一个达到就绪态(定义在对应的 events 字段中)或者捕获到一个信号
    • 如果 timeout 等于 0,poll() 不会阻塞,只执行一次检查,查看哪个文件描述符处于就绪态
    • 如果 timeout 大于 0,poll() 至多阻塞 timeout 毫秒,timeout 的精度受软件时钟粒度的限制
  • (3) 返回值:成功时 poll() 返回结构体中 events 域不为 0 的文件描述符个数,如果在超时前没有任何事件发生,poll() 会返回 0,失败返回 -1
  • 注意

    select() 和 poll() 返回正整数时的细小差别,如果一个文件描述符在返回的描述符集合中出现不止一次,系统调用 select() 会将同一个描述符计数多次,而系统调用 poll() 返回的是就绪态的文件描述符个数,且一个文件描述符只会统计一次,就算在相应的 revents 字段中设定了多个位掩码也是如此

文件描述符何时就绪

  • SUSv3中:如果对 I/O 函数的调用不会阻塞,而不论该函数是否能够实际传输数据,此时文件描述符被认为是就绪的

  • (4) 比较 select() 和 poll()

    • Linux 内核层面:select() 和 poll() 都使用了相同的内核 poll 例程集合,每个历程都返回有关单个文件描述符就绪的信息,就绪信息以掩码的形式返回,如果被检查的文件描述当中有一个关闭了,poll() 会在 revents 字段中返回 POLLNVAL,而 select() 会返回 -1 且将错误码设为 EBADF

    • select() 所使用的数据类型 fd_set 对于被检查的文件描述符数量有上限限制,FD_SETSIZE 默认为1024,poll() 对于被检查的文件描述符数量本质上没有限制

    • 由于 select() 的参数 fd_set 同时也是保存调用结果的地方(保存返回值),如果在循环中重复调用 select() ,必须每次都要重新初始化 fd_set,而 poll() 通过独立的两个字段 events(针对输入)和 revents(针对输出)分别来处理,避免了每次重新初始化参数的操作

    • select() 提供的超时精度(微妙)比 poll() 提供的超时精度(毫秒)高,这两个系统调用的超时精度都受软件时钟粒度的限制

  • (5) select() 和 poll() 存在的问题

    • 每次调用 select() 和 poll(),内核必须检查所有被指定的文件描述符是否处于就绪态,当检查大量处于密集范围内的文件描述符时,该操作耗费的时间将大大超过接下来的操作

    • 每次调用 select() 和 poll(),程序必须传递一个表示所有需要被检查的文件描述符数据结构到内核中,内核检查过后修改这个数据结构并返回给程序,select() 每次都要初始化这个数据结构,poll() 随着待检查文件描述符的数量增加,从用户空间到内核空间来回拷贝将占用大量 CPU 时间

    • 通常程序重复调用这些系统调用所检查的文件描述符集合都是相同的,可内核并不会每次调用之后记录它们


2.2.5 epoll 编程接口

同 I/O 多路复用和信号驱动 I/O 一样,linux 的 epoll(event poll) API 可以检查多个文件描述符上的 I/O 就绪状态

  • (1) epoll API 的优势

    • 当检查大量的文件描述符时,epoll 的性能延展性比 select() 和 poll() 高

    • epoll API 既支持水平触发也支持边缘触发,select() 和 poll() 只支持水平触发,信号驱动 I/O 只支持边缘触发

    • 可以避免复杂的信号处理流程,比如信号队列溢出时的处理

    • 灵活性高,可以指定我们希望检查的数据类型,例如,检查套接字文件描述符的读就绪、写就绪或者两者同时指定

  • (2) epoll API 由3个系统调用组成

    • 系统调用 epoll_create() 创建一个 epoll 实列,返回代表该实例的文件描述符

    • 系统调用 epoll_ctl() 操作同 epoll 实列相关的兴趣列表,通过 epoll_ctl() 可以增加新的文件描述符到列表中,也可以将已有的文件描述符从列表中移除,以及修改代表文件描述符上事件类型的位掩码

    • 系统调用 epoll_wait() 返回与 epoll 实例相关联的就绪列表中的成员

  • (4) 创建 epoll 实例:epoll_create()

  • 函数原型

    #include <sys/epoll.h>

    /*  epoll_create, epoll_create1 - open an epoll file descriptor  */
    int epoll_create(int size);  /*  On success, these system calls return a nonnegative file descriptor.  On error, -1 is returned, and errno is set to indicate the error  */

    /*  linux-5.15.2\fs\eventpoll.c  */
    SYSCALL_DEFINE1(epoll_create, int, size)
    {
	    if (size <= 0)  /*  epoll_create 的参数 size 只要大于 0 即可,无实际作用  */
		    return -EINVAL;

	    return do_epoll_create(0);
    }

  • 函数参数
    • 参数:size 只需大于 0 即可,从 linux2.6.8 版本开始忽略不用
  • 返回值:返回代表新创建 epoll 实例的文件描述符,这个文件描述在其它几个 epoll 系统调用中表示 epoll 实例,当这个文件描述符不再需要时,通过 close() 关闭
  • 自 linux2.6.27 版内核开始, 支持一个新的系统调用 epoll_create1(),功能与 epoll_create 相同,去掉了无用参数 size,增加了可以修改系统调用行为的 flag 参数,目前 只支持一个 flag 标志:EPOLL_CLOEXEC

  • (5) 修改 epoll 的兴趣列表:epoll_ctl()

  • 系统调用 epoll_ctl() 能够修改由文件描述符 epfd 所代表的 epoll 实例中的兴趣列表
  • 函数原型

    #include <sys/epoll.h>

    /*  epoll_ctl - control interface for an epoll descriptor  */
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  /*  When successful, epoll_ctl() returns zero.  When an error occurs, epoll_ctl() returns -1 and errno is set appropriately  */

    /*  结构体 epoll_event 中的 data 字段的类型  */
    typedef union epoll_data {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
    } epoll_data_t;

    /*  参数 event 指向的结构体  */
    struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
    };

  • 函数参数
    • 参数 epfd 文件描述符
    • 参数 fd 指明了要修改兴趣列表中的哪一个文件描述符的设定
    • 参数 op 指定需要执行的操作,有以下几种值
    • EPOLL_CTL_ADD:将描述符 fd 添加到 epoll 实例 epfd 中的兴趣列表中去
    • EPOLL_CTL_MOD:修改描述符 fd 上设定的事件,需要用到由 ev 所指向的结构体中的信息
    • EPOLL_CTL_DEL:将文件描述符 fd 从 epfd 的兴趣列表中移除
    • 参数 event 是指向结构体 epoll_event 的指针,结构体 epoll_event 中的 data 字段类型为 epoll_data
    • 参数 event 为文件描述符 fd 所做的设置如下:
  • 结构体 epoll_event 中的 events 字段是一个位掩码,它指定了待检查的描述符 fd 上所感兴趣的事件集合

  • data 字段是一个共用体,当描述符 fd 稍后成为就绪态时,共用体的成员用来指定传回给调用进程的信息

  • max_user_watches 上限

    每个注册到 epoll 实例上的文件描述符需要占用一小段不能被交换的内核空间,max_user_watches 可以查看和修改可以注册到 epoll 实例上的文件描述符总数,默认的上限值根据系统可用内存大小计算得出
  • (6) 事件等待:epoll_wait()
  • 系统调用 epoll_wait() 返回 epoll 实例中处于就绪态的文件描述符信息,单个 epoll_wait() 调用能返回多个就绪态文件描述符的信息
  • 函数原型

    NAME
           epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor

    SYNOPSIS
           #include <sys/epoll.h>

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

  • 函数参数
    • 参数 epfd 代表 epoll 实例中兴趣列表的的文件描述符
    • 参数 events 所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组 events 的空间由调用者指定,所包含的元素个数在 maxevents 中指定

      在数组 events 中,每个元素返回的都是单个就绪态文件描述符的信息,events 字段返回了在该描述符上已经发生的事件掩码,data 字段返回的是在描述符上使用 epoll_ctl() 注册感兴趣的事件时在 ev.data 中所指定的值,注意:data 字段是唯一可获知同这个事件相关的文件描述符号的途径
    • 参数 timeout 用来确定 epoll_wait() 的阻塞行为,有以下几种

      如果 timeout 等于 -1,将一直阻塞,直到兴趣列表中的文件描述符上有事件产生,或直到捕获到一个信号为止

      如果 timeout 等于 0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上产生了哪个事件

      如果 timeout 大于 0,调用将阻塞至多 timeout 毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止
  • 返回值,调用成功后 epoll_wait() 返回数组 events 中的元素个数,如果在 timeout 超时间隔内没有任何文件描述符处于就绪态,返回 0,出错返回 -1
  • 在多线程中,可以在一个线程中使用 epoll_ctl() 将文件描述添加到另一个线程中由 epoll_wait() 所监视的 epoll 实例的兴趣列表中,这些对兴趣列表的修改将立刻得到处理,而 epoll_wait() 调用将返回有关新添加的文件描述符的就绪信息

  • (7) epoll 事件

  • 当调用 epoll_ctl() 时在 ev.events 中指定的位掩码以及由 epoll_wait() 返回的 events[].event 中的值
  • epoll 中 events 字段上的位掩码值
    • EPOLLIN 作为 epoll_ctl() 的输入,由 epoll_wait() 返回,可读取非高优先级的数据
    • EPOLLPRI 作为 epoll_ctl() 的输入,由 epoll_wait() 返回,可读取高优先级的数据
    • EPOLLOUT 作为 epoll_ctl() 的输入,由 epoll_wait() 返回,普通数据可写
    • EPOLLET 作为 epoll_ctl() 的输入,采用边缘事件触发通知
  • (8) epoll 水平触发通知和边缘触发通知
  • epoll 默认提供水平触发,epoll 会通知程序何时能再文件描述夫上以以非阻塞方式执行 I/O,同 select 和 poll 提供的通知类型相似
  • epoll 边缘触发,程序调用 epoll_ctl() 时在 ev.events 字段中指定 EPOLLET 标志,epoll 会通知程序自从上一次调用 epoll_wait() 以来文件描述符上是否已经有 I/O 活动了
  • 采用边缘触发通知机制的程序基本框架

    1.让所有待监视的文件描述符都成为非阻塞的

    2.通过 epoll_ctl() 构建 epoll 的兴趣列表

    3.通过如下的循环处理 I/O 事件

    (a)通过 epoll_wait() 取得处于就绪态的文件描述符列表

    (b)针对每一个处于就绪态的文件描述符,不断进行 I/O 处理直到相关的系统调用(如:read()、write()、recv()、send() 或 accept())返回 EAGAIN 或 EWOULDBLOCK 错误

  • (9) epoll 的基本原理
    在这里插入图片描述

2.2.6 实例程序

struct thread_arg
{
	int clifd;
	struct sockaddr_in cliaddr;
};

void* start_routine(void* arg)
{
	char recvmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端接收数据缓冲区  */
	char sendmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端发送数据缓冲区  */

	struct thread_arg *sarg= (struct thread_arg*)arg;

	while (1)
	{
		bzero(recvmsg, TCP_SERVER_BUFF_SIZE);
		ssize_t reclen = recv(sarg->clifd, recvmsg, TCP_SERVER_BUFF_SIZE, 0);
		if (reclen > 0)
		{
			recvmsg[reclen] = '\0';
			printf("[TCP Server]: From %s client message: (%s)\n", inet_ntoa(sarg->cliaddr.sin_addr), recvmsg);

			strcpy(sendmsg, recvmsg);
			send(sarg->clifd, sendmsg, strlen(sendmsg) + 1, 0);
			printf("[TCP Server]: To client send message: (%s)\n", inet_ntoa(sarg->cliaddr.sin_addr));
			if (0 == strncmp("exit", recvmsg, 4))
			{
				break;
			}
		}
		else
		{
			break;
		}
	}
	close(sarg->clifd);
	printf("TCP Server close\n");
	return NULL;
}

/*  一请求一线程  */
int tcp_server_thread(void)
{
	/*  创建套接字 sockfd  */
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		printf("[TCP Server]: Create a socket file descriptor error\n");
		return -1;
	}
	printf("[TCP Server]: Create a socket file descriptor succeed\n");

	/*  填充数据结构体  struct sockaddr_in  */
	struct sockaddr_in seraddr;
	memset(&seraddr, 0, sizeof(seraddr));
	seraddr.sin_family = AF_INET;                   /*  地址族:IPv4  */
	seraddr.sin_port = htons(TCP_SERVER_PORT);      /*  端口号  */
	seraddr.sin_addr.s_addr = htonl(TCP_SERVER_IP); /*  IP地址  */

	/*  绑定端口号和IP地址到套接字 sockfd  */
	if (-1 == bind(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr)))
	{
		printf("[TCP Server]: Bind a server IP addr  and port to sockfd error\n");
		return -2;
	}
	printf("[TCP Server]: Bind a server IP addr  and port to sockfd succeed\n");

	/*  开始监听,设置最大链接数  */
	if (-1 == listen(sockfd, TCP_SERVER_BACKLOG))
	{
		printf("[TCP Server]: Listen for connections on a sockfd error\n");
		return -3;
	}
	printf("[TCP Server]: Listen for connections on a sockfd succeed\n");

	printf("[TCP Server]: Waiting for client's link request\n");

	while (1)
	{
		/*  等待来自客户端的连接请求  */
		struct sockaddr_in cliaddr;
		memset(&cliaddr, 0, sizeof(cliaddr));
		socklen_t cliaddrlen = sizeof(cliaddr);
		int clifd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
		if (-1 == clifd)
		{
			printf("[TCP Server]: Accept a connection on a clifd error\n");
			return -4;
		}
		printf("[TCP Server]: Accept a connection on a clifd succeed\n");

		struct thread_arg arg;
		memset(&arg, 0, sizeof(arg));
		arg.clifd = clifd;
		arg.cliaddr.sin_addr = cliaddr.sin_addr;
		pthread_t thread = { 0 };
		pthread_create(&thread, NULL, start_routine, (void*)&arg);
	}
	close(sockfd);
	printf("[TCP Server]: Close\n");
	return 0;
}

/*  TCP Server I/O 多路复用 select  */
static int tcp_server_select(void)
{
	char recvmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端接收数据缓冲区  */
	char sendmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端发送数据缓冲区  */

	/*  创建套接字 sockfd  */
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		printf("[TCP Server]: Create a socket file descriptor error\n");
		return -1;
	}
	printf("[TCP Server]: Create a socket file descriptor succeed\n");

	/*  填充数据结构体  struct sockaddr_in  */
	struct sockaddr_in seraddr;
	memset(&seraddr, 0, sizeof(seraddr));
	seraddr.sin_family = AF_INET;                   /*  地址族:IPv4  */
	seraddr.sin_port = htons(TCP_SERVER_PORT);      /*  端口号  */
	seraddr.sin_addr.s_addr = htonl(TCP_SERVER_IP); /*  IP地址  */

	/*  绑定端口号和IP地址到套接字 sockfd  */
	if (-1 == bind(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr)))
	{
		printf("[TCP Server]: Bind a server IP addr  and port to sockfd error\n");
		return -2;
	}
	printf("[TCP Server]: Bind a server IP addr  and port to sockfd succeed\n");

	/*  开始监听,设置最大链接数  */
	if (-1 == listen(sockfd, TCP_SERVER_BACKLOG))
	{
		printf("[TCP Server]: Listen for connections on a sockfd error\n");
		return -3;
	}
	printf("[TCP Server]: Listen for connections on a sockfd succeed\n");

	fd_set rfds = { 0 };
	fd_set rset = { 0 };
	fd_set wfds = { 0 };
	fd_set wset = { 0 };
	FD_ZERO(&rfds);
	FD_ZERO(&wfds);
	FD_SET(sockfd, &rfds);

	int maxfds = sockfd;

	printf("[TCP Server]: Waiting for client's link request\n");

	while (1)
	{
		rset = rfds;
		wset = wfds;
		/*  等待来自客户端的连接请求  */
		struct sockaddr_in cliaddr;

		int nready = select(maxfds + 1, &rset, &wset, NULL, NULL);
		if (FD_ISSET(sockfd, &rset))
		{
			memset(&cliaddr, 0, sizeof(cliaddr));
			socklen_t cliaddrlen = sizeof(cliaddr);
			int clifd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
			if (-1 == clifd)
			{
				printf("[TCP Server]: Accept a connection on a clifd error\n");
				return -4;
			}
			printf("[TCP Server]: Accept a connection on a clifd succeed\n");

			FD_SET(clifd, &rfds);

			if (clifd > maxfds)
			{
				maxfds = clifd;
			}

			if (0 == --nready)
			{
				continue;
			}
		}
		for (int i = sockfd + 1; i <= maxfds; i++)
		{
			if (FD_ISSET(i, &rset))
			{
				bzero(recvmsg, TCP_SERVER_BUFF_SIZE);
				ssize_t reclen = recv(i, recvmsg, TCP_SERVER_BUFF_SIZE, 0);
				if (reclen > 0)
				{
					recvmsg[reclen] = '\0';
					printf("[TCP Server]: From %s client message: (%s)\n", inet_ntoa(cliaddr.sin_addr), recvmsg);

					FD_CLR(i, &rfds);
					FD_SET(i, &wfds);
				}
				else if (0 == reclen)
				{
					FD_CLR(i, &rfds);
					printf("disconnect\n");
					close(i);
				}
				if (0 == --nready)
				{
					break;
				}
			}
			if (FD_ISSET(i, &wset))
			{
				bzero(sendmsg, TCP_SERVER_BUFF_SIZE);
				strcpy(sendmsg, recvmsg);

				send(i, sendmsg, strlen(sendmsg) + 1, 0);

				FD_CLR(i, &wfds);    /*  清除写事件标志  */
				FD_SET(i, &rfds);    /*  修改为读事件  */

				printf("[TCP Server]: To client send message: (%s)\n", inet_ntoa(cliaddr.sin_addr));
				if (0 == strncmp("exit", recvmsg, 4))
				{
					break;
				}
			}
		}
	}
	close(sockfd);
	printf("[TCP Server]: Close\n");
	return 0;
}

/*  客户端 I/O 多路复用 poll  */
static int tcp_server_poll(void)
{
	char recvmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端接收数据缓冲区  */
	char sendmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端发送数据缓冲区  */

	/*  创建套接字 sockfd  */
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		printf("[TCP Server]: Create a socket file descriptor error\n");
		return -1;
	}
	printf("[TCP Server]: Create a socket file descriptor succeed\n");

	/*  填充数据结构体  struct sockaddr_in  */
	struct sockaddr_in seraddr;
	memset(&seraddr, 0, sizeof(seraddr));
	seraddr.sin_family = AF_INET;                   /*  地址族:IPv4  */
	seraddr.sin_port = htons(TCP_SERVER_PORT);      /*  端口号  */
	seraddr.sin_addr.s_addr = htonl(TCP_SERVER_IP); /*  IP地址  */

	/*  绑定端口号和IP地址到套接字 sockfd  */
	if (-1 == bind(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr)))
	{
		printf("[TCP Server]: Bind a server IP addr  and port to sockfd error\n");
		return -2;
	}
	printf("[TCP Server]: Bind a server IP addr  and port to sockfd succeed\n");

	/*  开始监听,设置最大链接数  */
	if (-1 == listen(sockfd, TCP_SERVER_BACKLOG))
	{
		printf("[TCP Server]: Listen for connections on a sockfd error\n");
		return -3;
	}
	printf("[TCP Server]: Listen for connections on a sockfd succeed\n");

	struct pollfd fds[POLLFD_SIZE] = { 0 };
	fds[0].fd = sockfd;
	fds[0].events = POLLIN;

	int maxfds = sockfd;

	printf("[TCP Server]: Waiting for client's link request\n");
	struct sockaddr_in cliaddr;
	while (1)
	{
		int nready = poll(fds, maxfds + 1, -1);
		if (fds[0].revents & POLLIN)
		{
			/*  等待来自客户端的连接请求  */
			memset(&cliaddr, 0, sizeof(cliaddr));
			socklen_t cliaddrlen = sizeof(cliaddr);
			int clifd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
			if (-1 == clifd)
			{
				printf("[TCP Server]: Accept a connection on a clifd error\n");
				return -4;
			}
			printf("[TCP Server]: Accept a connection on a clifd succeed\n");

			fds[clifd].fd = clifd;
			fds[clifd].events = POLLIN;

			if (clifd > maxfds)
			{
				maxfds = clifd;
			}
			if (0 == --nready)
			{
				continue;

			}
		}
		for (int i = sockfd + 1; i <= maxfds; i++)
		{

			if (fds[i].revents & POLLIN)
			{
				bzero(recvmsg, TCP_SERVER_BUFF_SIZE);
				ssize_t reclen = recv(i, recvmsg, TCP_SERVER_BUFF_SIZE, 0);
				if (reclen > 0)
				{
					recvmsg[reclen] = '\0';
					printf("[TCP Server]: From %s client message: (%s)\n", inet_ntoa(cliaddr.sin_addr), recvmsg);

					strcpy(sendmsg, recvmsg);
					send(i, sendmsg, strlen(sendmsg) + 1, 0);
					printf("[TCP Server]: To client send message: (%s)\n", inet_ntoa(cliaddr.sin_addr));
				}
				else if (0 == reclen)
				{

					fds[i].fd = -1;
					printf("disconnect\n");
					close(i);

				}
				if (0 == --nready)
				{
					break;
				}
			}
		}
	}
	close(sockfd);
	printf("[TCP Server]: Close\n");
	return 0;
}

/*  epoll  */
static int tcp_server_epoll(void)
{
	char recvmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端接收数据缓冲区  */
	char sendmsg[TCP_SERVER_BUFF_SIZE] = { '\0' };  /*  服务器端发送数据缓冲区  */

	/*  创建套接字 sockfd  */
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		printf("[TCP Server]: Create a socket file descriptor error\n");
		return -1;
	}
	printf("[TCP Server]: Create a socket file descriptor succeed\n");

	/*  填充数据结构体  struct sockaddr_in  */
	struct sockaddr_in seraddr;
	memset(&seraddr, 0, sizeof(seraddr));
	seraddr.sin_family = AF_INET;                   /*  地址族:IPv4  */
	seraddr.sin_port = htons(TCP_SERVER_PORT);      /*  端口号  */
	seraddr.sin_addr.s_addr = htonl(TCP_SERVER_IP); /*  IP地址  */

	/*  绑定端口号和IP地址到套接字 sockfd  */
	if (-1 == bind(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr)))
	{
		printf("[TCP Server]: Bind a server IP addr  and port to sockfd error\n");
		return -2;
	}
	printf("[TCP Server]: Bind a server IP addr  and port to sockfd succeed\n");

	/*  开始监听,设置最大链接数  */
	if (-1 == listen(sockfd, TCP_SERVER_BACKLOG))
	{
		printf("[TCP Server]: Listen for connections on a sockfd error\n");
		return -3;
	}
	printf("[TCP Server]: Listen for connections on a sockfd succeed\n");

	int epfd = epoll_create(1); /*  参数只要大于0即可,实际无意义  */

	struct epoll_event events[EPOLL_EVENTS_SIZE] = { 0 };
	struct epoll_event ev;
	memset(&ev, 0, sizeof(ev));
	ev.events = EPOLLIN;
	ev.data.fd = sockfd;

	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
	struct sockaddr_in cliaddr;
	printf("[TCP Server]: Waiting for client's link request\n");
	while (1)
	{
		int nready = epoll_wait(epfd, events, EPOLL_EVENTS_SIZE, 5);
		if (-1 == nready)
		{
			continue;
		}

		for (int i = 0; i < nready; i++)
		{
			int evfd = events[i].data.fd;
			if (evfd == sockfd)
			{
				/*  等待来自客户端的连接请求  */
				memset(&cliaddr, 0, sizeof(cliaddr));
				socklen_t cliaddrlen = sizeof(cliaddr);
				int clifd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
				if (-1 == clifd)
				{
					printf("[TCP Server]: Accept a connection on a clifd error\n");
					return -4;
				}
				printf("[TCP Server]: Accept a connection on a clifd succeed\n");

				ev.events = EPOLLIN;
				ev.data.fd = clifd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &ev);
			}
			else
			{
				if (events[i].events & EPOLLIN)
				{
					bzero(recvmsg, TCP_SERVER_BUFF_SIZE);
					ssize_t reclen = recv(evfd, recvmsg, TCP_SERVER_BUFF_SIZE, 0);
					if (reclen > 0)
					{
						recvmsg[reclen] = '\0';
						printf("[TCP Server]: From %s client message: (%s)\n", inet_ntoa(cliaddr.sin_addr), recvmsg);
						bzero(sendmsg, TCP_SERVER_BUFF_SIZE);
						strcpy(sendmsg, recvmsg);
						send(evfd, sendmsg, strlen(sendmsg) + 1, 0);
						printf("[TCP Server]: To client send message: (%s)\n", inet_ntoa(cliaddr.sin_addr));
					}
					else if (0 == reclen)
					{
						ev.events = EPOLLIN;
						ev.data.fd = evfd;
						epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
						printf("Client close\n");
						close(evfd);
					}
				}
			}
		}
	}
	close(sockfd);
	printf("TCP Server close\n");
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值