Linux--ServerProgramming--(4)详解 I/O复用

news2024/12/19 20:49:16

1. I/O 复用功能

I/O 复用能同时监听多个文件描述符
I/O 复用本身是阻塞的

当有多个文件描述符同时就绪时:
	若不采取额外措施,程序就只能按顺序一次处理其中的每一个文件描述符,
	这使得服务器程序看起来是串行工作的。
	若要实现并发:
		只能通过多进程或多线程等并发手段。

Linux 实现 I/O 复用的系统调用主要有 select、poll 和 epoll

2.文件描述符就绪条件

2.1 下列情况下文件描述符 可读

1. socket 内核接收缓存区中的字节数大于或等于其低水位标记 SO_RCVLOWAT.
	此时可以无阻塞的读该 socket,并且读操作返回的字节数 大于 0.

2. socket 通信的对方关闭连接。
	此时对该 socket 读操作将返回 0.

3.监听 socket 上有新的连接请求

4.	socket 上有未处理的错误。
	此时我们可以使用 getsockopt 来读取和清除该错误。

2.2 下列情况下文件描述符 可写

1. socket 内核发送缓存区中的可用字节数大于或等于其低水位标记 SO_SNDLOWAT.
	此时可以无阻塞的 写 该 socket,并且写操作返回的字节数大于 0.
	
2. socket 的写操作被关闭。
	对 写操作被关闭的 socket 执行写操作将触发一个 SIGPIPE 信号。

3. socket 使用非阻塞 connect 连接成功或失败(超时)后。

4.  socket 上有未处理的错误。
	此时我们可以使用 getsockopt 来读取和清除该错误。

2.3 下列情况下文件描述符 出现异常

select 能处理的异常只有一种:
		1. socket 上接收到带外数据。详见下文 3.2 select 实例。

3. select 系统调用

select 系统调用用途
		在一段时间内,监听用户感兴趣的文件描述符上的 可读、可写 和 时间。

3.1 select API

原型如下:

man 2 select

#include <sys/select.h>
int select(int nfds,fd_set * readfds, fd_set * writefds, fd_set * execptfds, struct timeval * timeout);
参数:
	nfds:
			指定被监听的文件描述符的参数。
			通常被设置为 select 监听的所有文件描述符中的最大值 加 1,因为文件描述符是从 0 开始计数的。
	readfds:
			指向 可读事件对应的文件描述符集合。
	writefds:
			指向 可写事件对应的文件描述符集合。
	exceptfds:
			指向 异常等事件对应的文件描述符集合。
	timeout:
			用来设置 select 函数的超时时间。
			timeval 结构指针类型,
			采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。
			调用失败时,timeout的值是不确定的。
			
	返回值:
			成功:
					返回就绪(可读、可写、异常)文件描述符总数。
					若在超时事件内没有任何文件描述符就绪,select 返回 0.
			失败:
					返回 -1并设置errno。
			扩展:
					select 等待期间,程序收到信号,则select 立即返回 -1,并设置 errno为 EINTR.



fd_set  结构体定义:
#include <typesizes.h>
#define __FD_SETSIZE 1024

#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;

#undef __NFDBITS
#define _NFBDITS ( 8 * (int) sizeof(__fd_mask))

typedef struct
{
	#ifdef __USE_XOPEN
		__fd_mask fds_bits[ __FD_SETSIZE / __NFDBITS ];
	#define __FDS_BITS(set) ((set)->fds_bits)
	#else
		__fd_mask __fds_bits[ __FD_SETSIZE / __NFDBITS ];
	#define __FDS_BITS(set) ((set)->__fds_bits)
}fd_set;

详解fd_set:
	 	1.由结构体定义可见,仅包含一个整型数组,该数组每个元素每一位(bit)标记一个文件描述符。
	 	2.fd_set 能容纳的文件描述符数量由 FD_SETSIZE 指定,这就限制了 select 能同时处理的文件描述符总量。
	
	因为对位操作繁琐,故使用如下函数操作 fd_set :
	#include <sys/select.h>
	FD_ZERO(fd_set * fdset);			//清除 fdset 的所有位
	FD_SET(int fd,fd_set * fdset);		//设置 fdset 的位 fd
	FD_CLR(int fd,fd_set * fdset);		//清除 fdset 的位 fd
	int FD_ISSET(int fd, fd_set * fdset);//测试 fdset 的位 fd 是否被设置

timeval 结构体定义:
				struct timeval
				{
					long tv_sec;	//秒数
					long tv_usec;	//微秒数
				}
				timeval 结构体传值:
					1.若给 timeval 结构体两成员都传 0,
						则 select 将立即返回。
					2.若给 timeout 传 NULL,
						则 select 将一直堵塞,直到某个文件描述符就绪。

3.2 select 实例

socket 上接收到 普通数据 和 带外数据 都将使 select 返回,
但 socket 处于不同的就绪状态,前者 处于可读状态,后者 处于异常状态。
下例实现:
//server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char * argv[])
{
    if(argc <=2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]); //字符串转换为一个整数(类型为 int 型)。

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));

    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int listenfd = socket( PF_INET, SOCK_STREAM, 0);
    assert( listenfd >= 0);
    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert( ret != -1);
    ret = listen(listenfd , 5);
    assert( ret != -1);

    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    int connfd = accept(listenfd, (struct  sockaddr*)&client_address, &client_addrlength);
    if(connfd < 0)
    {
        printf("errno is : %d\n",   errno);
        close(listenfd);
    }

    char buf[1024];

    fd_set read_fds;
    fd_set exception_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);

    while(1)
    {
        memset(buf,'\0', sizeof(buf));

        //每次调用 select 前 都要重新在 read_fds 和 exception_fds 中设置文件描述符connfd, 因为事件发生后,文件描述符集合将被内核修改。
        FD_SET(connfd, &read_fds);
        FD_SET(connfd, &exception_fds);

        ret = select(connfd+1, &read_fds, NULL, &exception_fds, NULL);  //最后一个参数是 NULL,故此处会一直阻塞
        if(ret < 0)
        {
            printf(" selection failure\n");
            break;;
        }

        //对于可读事件,采用普通的 recv 函数读取数据
        if( FD_ISSET(connfd, &read_fds)) //如果connfd 是 read_fds 中的一员返回非0,否则返回0   
        {
            ret = recv(connfd, buf, sizeof(buf)-1, 0);
            if(ret <= 0)
            {
                break;;
            }
            printf("get %d bytes of normal data: %s\n",ret , buf);

        }

        else if(FD_ISSET(connfd, &exception_fds))   //如果connfd 是 exception_fds 中的一员返回非0,否则返回0 
        {
            ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);
            if(ret <= 0)
            {
                break;
            }
            printf("get %d bytes of oob data: %s\n",ret, buf);
        }
    }

    close(connfd);
    close(listenfd);
    return 0;
}

4. poll 系统调用

#include <poll.h>

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

	fds:
			是一个 pollfd 结构类型数组,指定所有感兴趣的文件描述符上发生的 可读、可写、异常等事件。
			pollfd :
					struct pollfd
					{
						int fd;			//文件描述符
						short events;	//注册的事件
						short revents;	//实际发生的事件,由内核填充
					}
					
					fd:
						指定文件描述符
					events:
						告诉 poll 监听 fd 上的哪些事件,他是一系列事件的按位或。(可能发生的事件见下表 9-1)
					revents:
						由内核修改,以通知应用程序 fd 上实际发生了哪些事件。(可能发生的事件见下表 9-1)
	nfd:
			指定被监听事件集合 fds 的大小。
			nfds_t 定义:
			typedef unsigned long int nfds_t;
	
	timeout:
			指定 poll 的超时值,单位是 毫秒。
			当 设为 -1 时,poll 调用将永远阻塞,直到某个事件发生。
			当 设为 0 时,poll 调用将立即返回。
	
	返回(和 select 相同):
			成功:
					返回就绪(可读、可写、异常)文件描述符总数。
					若在超时事件内没有任何文件描述符就绪,poll 返回 0.
			失败:
					返回 -1并设置errno。	

	
	扩展:
			通常,应用程序要根据 recv 调用的返回值来区分 socket 上接收到的有效数据还是对方关闭连接请求,并做相应处理。
			Linux 内核 2.6.17 开始, GNU为 poll 增加了一个 POLLRDHUP 事件,在 socket 上接收到对方关闭连接请求后触发。

在这里插入图片描述

表 9-1 注意点:
	POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND 由 XOPEN 规范定义。
	它们实际上是将 POLLIN 事件和 POLLOUT 事件分的更细致,以区别对待普通数据和优先数据。但 Linux 并不完全支持它们。

5. epoll 系统调用

与 select、poll 差异很大。

epoll 使用一系列函数来完成任务。
//1.	epoll 需要使用一个额外文件描述符,来唯一表示内核中这个事件表,创建方法如下函数:
#include <sys/epoll.h>
int epoll_create(int size);
	size:
			给内核一个提示,告诉内核事件表需要多大。
	返回:
			文件描述符,用于其他所有 epoll 系统调用的第一个参数,以指定要访问的内核事件表。

//2. 下面函数 操作 epoll 内核事件表
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
	fd:
			要操作的文件描述符。
	op:
			指定操作类型,可操作性类型如下三种:
				EPOLL_CTL_ADD:	往事件表上 注册 fd上的事件。
				EPOLL_CTL_MOD:	修改 fd 上的注册事件。
				EPOLL_CTL_DEL:	删除 fd 上的注册事件。
	event:
			指定事件。
			epoll_event 结构体指针:
			struct epoll_event
			{
				__uint32_t events;		//epoll 事件
				epoll_data_t data;		//用户数据
			}
			events:
					描述事件类型。
					epoll 事件的类型宏 和 poll 基本相同(详见下表 9-1)。
					epoll 事件类型宏是在 poll 类型宏前加上 “E”,如 可读事件 EPOLLIN。
					epoll 有两个额外的事件类型————EPOLLET 和 EPOLLONESHOT .
			data:
					用于存储用户数据。
					epoll_data_t 是一个联合体,定义如下:
					typedef union epoll_data
					{
						void * ptr;
						int fd;
						unit32_t u32;
						unit64_t u64;
					} epoll_data_t;
					fd:4 个成员中使用最多的是 fd,它指定事件所从属的目标文件描述符。
					ptr:
						可用来指定和 fd 相关的用户数据。
					扩展:
						因为是联合体,故不能同时 得到 fd 和 ptr,
						若要实现 文件描述符 和 用户数据关联起来,以实现快速数据访问,只能用其他手段。
						如,放弃 fd成员,在 ptr 指向的用户数据中包含 fd。
	
	返回:
			成功:
					0
			失败:
					-1 并设置 errno。


//3.在超时时间内等待一组文件描述符上的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
	epfd:
			内核事件表。
	
	events:
			只用于接收输出 epoll_wait 检测到的就绪事件。
			注意:不像 select 和 poll 的数组参数既用于传入用户注册的事件,又用于输出内核检测到的就绪事件(详见下图 9-2)。
			epoll_wait 函数如果检测到事件,就将所有就绪事件从内核事件表(epfd参数指定)中复制到它的第二个参数 events 指向的数组中。	
	maxevents:
			指定最多监听多少个事件,必须大于 0.
			
	timeout:
			与 poll 接口的 timeout 参数相同。
			指定 epoll 的超时值,单位是 毫秒。
			当 设为 -1 时,epoll_wait 调用将永远阻塞,直到某个事件发生。
			当 设为 0 时,epoll_wait 调用将立即返回。
			
	返回:
		成功:
			返回就绪的文件描述符的个数。
		失败:
			-1,并设置 errno。

在这里插入图片描述

表 9-1 注意点:
	POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND 由 XOPEN 规范定义。
	它们实际上是将 POLLIN 事件和 POLLOUT 事件分的更细致,以区别对待普通数据和优先数据。但 Linux 并不完全支持它们。

在这里插入图片描述

5.1 LT 和 ET 模式

epoll 对文件描述符操作有两种模式: 
1.LT(Level Trigger,电平触发)
		默认工作模式。
		这个模式下 epoll 相当于一个效率较高的 poll。
		扩展:
				此模式下,当 epoll_wait 检测到有事件发生并将此事件通知应用程序后,
			应用程序可以 不立即处理该事件,这样会导致 应用程序下一次调用 epoll_wait 时,
			还会再次向应用程序 通告该事件,直到该事件被处理。					
			
2.ET(Edge Trigger,边沿触发)
		设置方法:
			需手动往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件,epoll 将以 ET模式操作该文件描述符。
			ET 模式是 epoll 的高效工作模式。
		扩展:
			1.	当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,
			应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。
			2.	每个使用 ET 模式的文件描述符都应该是 非阻塞的。
			若是阻塞的,读或写操作将会因为没有后续的事件而一直处于阻塞状态。
总结:
		ET 模式降低了同一个 epoll 事件被重复触发的 次数,故效率比  LT 高。

LT 和 ET 模式例子:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

//将文件描述符设置成 非阻塞的
int setnonblocking(int fd)
{
    int old_option = fcntl(fd , F_GETFL);
    int  new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

// 将文件描述符 fd 上的 EPOLLIN 注册到 epollfd 指示的 epoll内核事件表中
//参数 enable_et 指定是否对 fd 启用 ET 模式
void  addfd(int epollfd, int fd, bool enable_et)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if( enable_et)
    {
        event.events |= EPOLLET;
    }

    epoll_ctl(epollfd, EPOLL_CTL_ADD,fd, &event);
    setnonblocking( fd );
}

//LT 模式工作流程
void lt(epoll_event * event, int number, int epollfd, int listenfd)
{
    char buf[BUFFER_SIZE];
    for (int i = 0; i < number; i++)
    {
        int sockfd = event[i].data.fd;
        if (sockfd == listenfd)
        {
            struct sockaddr_in address_client;
            socklen_t addresslen_client = sizeof(address_client);
            int connfd = accept(listenfd, (struct sockaddr*)&address_client, &addresslen_client);
            addfd(epollfd, connfd, false);  //对 connfd 禁用 ET
        }
        else if( event[i].events & EPOLLIN)
        {
            //只要 socket 读缓存中还有未读出的数据,这段代码就被触发
            printf("event trigger once\n");
            memset(buf, '\0', BUFFER_SIZE);
            int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
            if(ret <= 0)
            {
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content: %s\n", ret, buf);
        }
        else
        {
            printf("something else happend\n");
        }

    }
    
}


//ET 模式工作流程
void et(epoll_event * events, int number, int epollfd, int listenfd)
{
    char buf[BUFFER_SIZE];
    for (int i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;
        if (sockfd == listenfd)
        {
            struct  sockaddr_in address_client;
            socklen_t addrlen_client = sizeof(address_client);
            int connfd = accept(listenfd, (struct sockaddr*)&address_client, &addrlen_client);
            addfd(epollfd, connfd, true);   //对 connfd 开启 ET 模式
        }
        else if(events[i].events & EPOLLIN)
        {
            //这段代码不会被重复触发,故 循环读取数据,以确保吧 socket 中所有数据读出
            printf("event trigger once\n");
            while(1)
            {
                memset(buf, '\0', BUFFER_SIZE);
                int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                if(ret < 0)
                {
                    //对于非阻塞 I/O,下面的条件成立表示数据已经全部读取完毕,
                    //此后 epoll 就能再次触发 sockfd 上的 EPOLLIN 事件,以驱动下一次读操作
                    if((errno == EAGAIN) || (errno == EWOULDBLOCK))
                    {
                        printf("read later\n");
                        break;
                    }
                    close(sockfd);
                    break;
                }
                else if(ret == 0)
                {
                    close(sockfd);
                }
                else 
                {
                    printf("get %d butes of content: %s\n", read, buf);
                }
            }
        }
        else
        {
            printf("something else happend \n");
        }
        
    }
    
}



int main(int argc, char * argv[] )
{
    if(argc <= 2)
    {
        printf("usage:  %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);

    int ret =0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_port = htons(port);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != -1);

    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create( 5 );
    assert(epollfd != -1);
    addfd(epollfd, listenfd , true);

    while (1)
    {
        int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if(ret < 0)
        {
            printf("epoll failure\n");
            break;
        }

        lt(events, ret, epollfd, listenfd); //使用 LT 模式
        //et(events, ret, epollfd, listenfd); //使用 ET 模式
    }
    
    close(listenfd);
    return 0;
}

5.2 EPOLLONESHOT 事件

ET模式下,一个 socket 上某个事件还有可能被触发 多次:
场景:
		如一个 线程(或进程)在读取完某个 socket 上的数据后开始处理这些数据,
	而在数据处理过程中该连接上又有新的数据可读(EPOLLIN 再次被触发),此时另一个线程被唤醒来读取这些新的数据。
	故,出现了两个线程同时操作一个 socket 的局面。
	
期望:
		任何时刻 同一个 socket 连接都只被一个线程(或进程)处理。
	
实现方式:
		使用 epoll 的 EPOLLONESHOT 事件实现。

对注册了 EPOLLONESHOT 事件的文件描述符:
		1.	操作系统最多触发其注册的一个可读、可写或异常事件,且只触发一次,
		除非我们使用 epoll_ctl 寒暑表重置该文件描述符上注册的 EPOLLONESHOT 事件。
		例:
			一旦被某个线程处理完毕,该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,
			以确保这个 socket 下次可读,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会处理这个 socket。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024

struct fds
{
    int epollfd;
    int sockfd;
};


//设置文件描述符非阻塞
int setnonblocking(int fd)
{
    int old_option = fcntl(fd,F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL,new_option);
    return old_option;
}


//将 fd 上的 EPOLLIN 和 EPOLLET 事件注册到 epollfd 指示的 epoll 内核事件中,参数 oneshot 指定是否注册 fd 上的 EPOLLONESHOT 事件
void addfd(int epollfd, int fd, bool oneshot)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

//重置 fd 上的事件。这样操作之后,尽管 fd 上的 EPOLLONESHOT 事件被注册,但是操作系统仍然会触发 fd 上的 EPOLLIN 事件,且只触发一次。
void reset_oneshot(int epollfd, int fd)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event );
}

//工作线程
void * worker(void * arg)
{
    int sockfd = ((fds*)arg)->sockfd;

    int epollfd = ((fds*)arg)->epollfd;
    printf("start new thread to receive data on fd: %d\n",sockfd);
    char buf[BUFFER_SIZE];
    memset(buf, '\0', BUFFER_SIZE);

    //循环读取 sockfd 上的数据,直到遇到 EAGAIN 错误。
    while(1)
    {
        int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
        if(ret == 0)
        {
            close(sockfd);
            printf("foreiner closed the connection\n");
            break;
        }
        else if(ret < 0)
        {
            if(errno == EAGAIN)
            {
                reset_oneshot(epollfd,sockfd);
                printf("read later\n");
                break;
            }
        }
        else 
        {
            printf("get content: %s\n",buf);

            //休眠 5 s,模拟输出处理过程
            sleep(5);
        }
    } 

    printf("end thread recving data on fa: %d\n", sockfd);
}

int main(int argc, char * argv[])
{
    if(argc <= 2)
    {
        printf("usage: %s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);

    int ret =0;
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
    inet_pton(AF_INET, ip, &address.sin_addr);

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);

    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != 1);

    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert( epollfd != -1);
    
    //注意,监听 socket listenfd 上是不能注册 EPOLLONESHOT 事件的,
    //否则 应用程序只能监听一个客户连接, 因为 后续客户连接不在触发 listenfd 的  EPOLLIN 事件
    addfd(epollfd, listenfd, false);

    while (1)
    {
        int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if( ret < 0)
        {
            printf("epoll failure\n");
            break;
        }

        for (int i = 0; i < ret; i++)
        {
            int sockfd = events[i].data.fd;
            if (sockfd == listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlen = sizeof(client_address);
                int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);

                //对每个非监听文件描述符都注册 EPOLLONESHOT 事件
                addfd(epollfd, connfd, true);
            }
            else if(events[i].events & EPOLLIN)
            {
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;

                pthread_create(&thread , NULL, worker,(void *)&fds_for_new_worker);
            }
            else
            {
                printf("something else happend\n");
            }
        }
        
    }
    
    close(listenfd);
    return 0;
}

6.select 、poll、epoll 异同

在这里插入图片描述

7. 利用 I/O 复用 同时处理 TCP 和 UDP 服务

实际应用中,有的服务器程序能`同时监听多个端口`。
如,超级服务 inetd 和 android 的调试服务 adbd。

由 bind 系统调用来看,一个 socket 只能与一个 socket 地址绑定,即一个端口只能监听一个端口。
故,服务器要监听多个端口就必须创建多个 socket,并绑定到相应的端口。
这就可以用 I/O 复用来实现。

若服务器要同时监听同一个端口上的 TCP 和 UDP请求,也需要创建两个 socket:
1.	流socket (TCP)
2.	数据报 socket	(UDP)
最后将它们绑定到一个端口上。

例,
	如下所示回射服务器可同时处理一个端口的 TCP 和 UDP 请求。
//回射服务器源码,同时处理一个端口的 TCP 和 UDP 请求

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT市场营销指南震撼出炉,你错过了?!

ChatGPT是一种基于AI技术的语言模型&#xff0c;它可以与用户进行对话和交互。它被广泛应用于各个领域&#xff0c;包括市场营销。作为一名市场营销人员&#xff0c;您可以使用ChatGPT来获得创意、解决问题和生成内容。 下面是190个ChatGPT提示&#xff0c;可帮助营销人员更好…

oracle自定义函数 for in loop示例

1、新建type&#xff0c;就是返回结果集有什么&#xff0c;这里就写什么&#xff08;相当于表的字段&#xff09; CREATE OR REPLACE TYPE "TYPE_NQ_FORM_STATISTICS" as object (recordid varchar2(500),form_name varchar2(200),sortone varchar2(100),sorttwo …

华为od机试题目回顾

今天去做了华为机试&#xff0c;两道一星题&#xff0c;一道二星题。 一星题 1&#xff1a; 题目主要大意&#xff1a; 输入一串字符串&#xff0c;里面可能包含有(x,y)的坐标。 0<x<1000&#xff0c;0<y<1000&#xff0c;类似(01,1)、(1,01)、(0,100)的都是非法坐…

Java开发手册中为什么不建议在for循环中使用“+“进行字符串操作

场景 java开发手册中对于循环体中进行字符串的拼接要求如下&#xff1a; 【推荐】循环体内&#xff0c;字符串的连接方式&#xff0c;使用 StringBuilder 的 append 方法进行扩展。 说明&#xff1a;下例中&#xff0c;反编译出的字节码文件显示每次循环都会 new 出一个 Str…

【Linux】-自动化构建工具(make/makefile)

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 文章目录 前言 前言 今天我们来讲讲再Linux中开发必备的一项技能&#xff0c;没有这个…

【OpenMMLab AI实战营第二期】深度学习预训练与MMPretrain

深度学习预训练与MMPretrain MMPreTrain 算法库介绍 MMPretrain 是一个全新升级的预训练开源算法框架&#xff0c;旨在提供各种强大的预训练主干网络&#xff0c; 并支持了不同的预训练策略。MMPretrain 源自著名的开源项目 MMClassification 和 MMSelfSup&#xff0c;并开发…

项目干系人管理实用方法,让你的项目顺风顺水

项目管理中的干系人是每个项目的一个重要方面&#xff0c;因为项目的结果取决于他们。然而&#xff0c;管理各种各样的干系人的艺术很有挑战性。在项目管理中根本没有出错的余地&#xff0c;本文将带你了解项目干系人以及如何管理他们以促进项目的全面成功。 谁是项目管理的干…

第3章:SpringMVC获取请求参数

一、SpringMVC获取请求参数 1.通过servletAPI获取 将HttpServletRequest作为控制器方法的形参&#xff0c;此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象 <a th:href"{/testServletAPI(usernameadmin,password123456)}">测试API<…

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示 如下图所示,在博途中新建项目后,添加自己所需的数据类型,然后选中该数据类型,右击选择“从块生成源“—”仅所选块“, 如下图所示,设置文件名后点击保存, 如下图所示,选中需要导出的DB块,右击选…

人事项目开发记录-登录模块

人事项目开发记录 后端接口实现 后端接口实现 后端权限认证采用Spring Security实现&#xff08;本小节中大量知识点与第10章的内容相关&#xff0c;需要读者熟练掌握第10章的内容&#xff09;&#xff0c;数据库访问使用MyBatis&#xff0c;同时使用Redis实现认证信息缓存。因…

如何在Centos的SSH2终端中终止-停止-结束某个Python程序的运行?

python3 /opt/python_scripts/retr/P-0006.py &我在运行上面的命令后&#xff0c;得到了下面的提示&#xff1a; [1] 42335 如果我想终止这个我自己写的Python程序&#xff0c;该怎么做呢&#xff1f; 答&#xff1a; 当您在后台运行一个命令时&#xff0c;终端会显示类似…

Django中使用openldap实现账号的统一管理

了解和安装 ldap ldap介绍 LDAP&#xff08;Lightweight Directory Access Protocol&#xff09;是一种轻量级的目录访问协议&#xff0c;它用于访问和维护分布式目录服务。 LDAP最初设计用于提供对X.500目录服务的简化访问&#xff0c;后来被广泛应用于各种应用程序和系统中…

SpringBoot源码分析:SpringBoot整合Tomcat(三)

一、概述 SpringBoot整合Tomcat整体启动流程如下图&#xff0c;接下来我们就按照改流程分析SpringBoot中内嵌Tomcat的启动流程。 二、启动流程 通过AbstractApplicationContext.refresh方法进入AbstractApplicationContext.onRefresh方法。 之后进入子类ServletWebServerAppl…

Word控件Spire.Doc 【其他】教程(6):从 Word 中提取 OLE 对象

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

chatgpt赋能python:Python后退命令:如何让你的程序退回到之前的状态

Python后退命令&#xff1a;如何让你的程序退回到之前的状态 Python是一种高级编程语言&#xff0c;因其易读易懂而闻名于世。Python中有很多命令用于编写程序&#xff0c;其中一项重要的命令是后退命令。本文将介绍Python后退命令的使用方法&#xff0c;并为您提供详细的步骤…

ChatGPT热度不减!华为宣布入局,盘古GPT能否大杀四方!

ChatGPT热度不减 六月份了&#xff0c;朋友们&#xff0c;来到六月份了已经&#xff0c;ChatGPT的热度依旧不减&#xff0c;各大论坛网站的榜单上还飘着ChatGPT相关话题的文章&#xff0c;且排名靠前。由此可见&#xff0c;这ChatGPT这股子热潮还得持续一段时间呢。 而且ChatG…

cuda block grid等介绍

这里写目录标题 cuda层次结构程序架构层次结构cuda程序调用cuda 内置变量GPU内存模型内存结构 可编程内存内存作用域寄存器本地内存共享内存共享内存访问冲突常量内存全局内存gpu缓存 cuda层次结构 程序架构 申请内存时是线性的内存&#xff0c;需要知道是按行还是按列排列 设计…

【Linux】Linux内核编译与入门

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Linux内核编译。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习知识&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&am…

DM数据库TEXT类型字段查询返回时的问题

一、问题描述&#xff1a; 在项目中使用到了DM数据库连接&#xff0c;查询某些数据&#xff0c;然后以Map形式返回时TEXT类型是会出现一些问题&#xff0c;默认会转换为dm.jdbc.driver.DmdbNClob类型的对象&#xff0c;某些情况下&#xff08;不知道具体原因&#xff09;在JSON…

移动端布局之flex布局1:flex布局体验、flex布局原理、flex布局父项常见属性

移动端布局之flex布局1 flex布局体验传统布局和flex弹性布局的区别初体验index.html flex布局原理布局原理 flex布局父项常见属性常见父项属性flex-direction设置主轴的方向&#xff08;重要&#xff09;主轴与侧轴属性值flex-direction: row;flex-direction: row-reverse;flex…