从socket开始讲解网络模式(epoll)

news2024/9/24 11:26:46

从socket开始讲解网络模式

windows采用IOCP网络模型,而linux采用epoll网络模型(Linux得以实现高并发,并被作为服务器首选的重要原因),接下来讲下epoll模型对网络编程高并发的作用

简单的socket连接

socket连接交互的流程如图:

20221231153933

服务端中个api的作用:

  • socket(IPv4/IPv6,TCP/UDP,0):创建socket套接字,获取listenfd
  • bind(listenfd,服务器地址,服务器地址长度):将套接字绑定服务器地址
  • listen(listenfd,size): 该套接字最多连接size个连接
  • accept(listenfd,客户端信息,len):客户端使用connect()后,与服务端进行三次握手,三次握手成功后,生成一个连接文件描述符connfd
  • recv(connfd,buff,len,0):从该连接的的buff中读取len字节数据 (对应图中的read())
  • send(connfd,buff,n,0): 读完数据后向buff写数据,以回应客户端。(对应图中的write())

原始代码实现:

int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {    // 监听tcp连接
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));    // 服务地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢
    servaddr.sin_port = htons(9999);    // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程
    
    // 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值
    // bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    // socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept
    // 该监听fd最多连接10个连接
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");
    while (1) {
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
    }
}

现在加入有两个客户端同时连接服务器,第二个客户端能连接成功,但发送不了数据,只有第一个客户端能发送数据。这是因为第二个客户端发送连接请求后,被服务器监听并成功连接,但是accept只取了第一个客户端的connfd,然后服务器就一直在while(1){}里跑了,只有第一个连接能发送数据。

解决方法,将accept()放入while(1)循环里

int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {    // 监听tcp连接
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));    // 服务地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢
    servaddr.sin_port = htons(9999);    // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程
    
    // 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值
    // bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    // socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept
    // 该监听fd最多连接10个连接
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    printf("========waiting for client's request========\n");

    while (1) {
        // 把accept放到while循环里
        // 新问题:每个连接都能发送数据,但只能发送一条数据
        // 原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在accept
		struct sockaddr_in client;     
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
}

产生了新问题:服务端无法正常接收数据,原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在accept

如:客户端A连接服务器=》客户端B连接服务器=>客户端B发送5次数据“B”(此时服务将无法读取这5个B,因为客户端A连接服务器后,还阻塞在recv(),等待读取A的数据),如果此时 =》 客户端A发送数据"A" => 服务器将会读取A,然后通过accept()获取客户端B的connfd,再读取客户端B之前发送的五个“B”

为每个socket连接设置一个线程

多线程:来一个连接新建一个线程,把connfd传给入口函数,接收发送数据

// 为方便讲解,listen()以上的代码略,最后会有一个整体的代码
int main(){
    ...

    // 客户端不多,可以用这种方法,客户端太多就不行
    // 如一个线程分配8M的栈空间,1G的内存只能分配128个线程左右 ,性能突破不了C10K
	while (1) {
		struct sockaddr_in client;
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }

		pthread_t threadid;
		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    }
}

void *client_routine(void *arg) {    // 线程入口函数,参数为connfd
	int connfd = *(int *)arg;
	char buff[MAXLNE];
	while (1) {
		int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
			break;
        }
	}
	return NULL;
}

这种方法简单,方便管理,不用担心一个连接阻塞其他连接了,但是有一个问题:线程需要独立的运行栈和其他的开销,一个线程大约是8M的栈空间,4G的内存最多只能支持512个连接,无法达到C10K级的并发。

select网络模型

多线程的方式并发量上不去,能不能用少数的线程处理所有fd呢?

select: 当一个fd接收到数据时,服务器能知道是哪个连接发的数据,并进行处理,其处理流程为(select用的很少,可直接看下一节的poll网络模型):

  • 创建事件集合,事件分为三类:可读、可写、异常,集合为长度为1024的bit-set,当有事件发生时,将对应的bit位设置为1
  • 进入while(1){}循环
  • 调用select(),将所有fd拷贝到内核,select会轮询所有fd是否有事件触发,触发了就置为1,并分别把(可读、可写、异常)事件从内核返回到用户态,这里可只考虑可读事件
  • 对fd进行进行处理:socket中的fd分为两类listenfd和connfd,需分别处理
    • listenfd对应的bit-set[listenfd]为1,且为可读事件时,说明服务器监听到了新的连接,需要将该connfd加入到事件集合中
    • listenfd是一个bit-map的做法,01固定,为保准输入输出,从3开始递增分配(3,4,5,6),如果4回收了,再从4分配,所以listenfd比所有connfd都小
    • 遍历bit-set,判断事件类型做出处理,如bit-set[connfd]=1,表示该fd有可读事件,就recv()读取数据,并send()响应客户端

代码如下,注意下select()函数参数的意义:

int main(){

    // 对fd的处理包括两部分,listenfd和读写fd
    // 因为首先要将rset拷贝到内核,再全部拷贝出来,开销太大
	fd_set rfds, rset;   

	FD_ZERO(&rfds);        // 先把bit-set清空
	FD_SET(listenfd, &rfds);      // 将listenfd加入 rfds读事件集合 

	int max_fd = listenfd;   // 当前管理的所有文件描述符的最大值,也就bit-set的长度

	while (1) {

		rset = rfds;   // 为啥还要弄这两个变量:防止读集合rfds在select被修改了
        // 第二、三、四个参数:要管理哪些文件描述符的读、写、异常的事件,放到相应的集合
        // 第五个超时时间:如果隔这么久一直没有事件发生,就返回,为NULL就是没有事件一直阻塞(select自带阻塞)
        // 调用select需要把fd从用户态拷贝到内核态,而且需要在内核遍历传递进来的所有fd
        // 把rfds给内核,rset是返回给用户的发生事件的文件描述符
		int nready = select(max_fd+1, &rset, &wset, NULL, NULL);    // 返回事件的数量,这里其实只有读事件

		if (FD_ISSET(listenfd, &rset)) { // listenfd是否在读事件集合中

			struct sockaddr_in client;
		    socklen_t len = sizeof(client);
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {  // 将connfd加入到读事件集合
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }

			FD_SET(connfd, &rfds);   // 将connfd加入读事件集合
			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;    // 全部加完了,去对事件进行操作
		}

		int i = 0;
		for (i = listenfd+1;i <= max_fd;i ++) {   // 遍历所有fd
			if (FD_ISSET(i, &rset)) { // 

				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					send(i, buff, n, 0);
		        } else if (n == 0) { //
					FD_CLR(i, &rfds);  // 从读事件集合删除
		            close(i);
		        }
				if (--nready == 0) break;
			} 
		}
	}
}

一个select能管理1024个fd,那么多弄几个select(一个进程或线程一个select),就能到达C10K了,但很难达到C1000K,其有以下缺点:

  1. 调用select时,事件集合需要从内核态拷贝到内核态,返回时,又需要全部从内核态拷贝到用户态。
  2. 需要轮询所有fd
  3. 单个select支持的fd太少了,默认为1024

poll网络模型

和select模型很像,区别就是用pollfd结构(fd的数量可自定义)代替了select的bit_set结构,

pollfd结构为:

struct pollfd
  {
    int fd;			/* File descriptor to poll.  */
    short int events;		/* Types of events poller cares about.  */
    short int revents;		/* Types of events that actually occurred.  */
  };

其处理流程为:

  • 定义pollfd列表,其中第一个元素为listenfd
  • 初始化每个列表元素,fd为-1,event为(POLLIN、POLLOUT、POLLPRI等),select将事件分为三类,poll统一管理
  • 进入while(1){}循环
  • 接下里的对fd的处理流程和select一样了
    • 调用poll(), 把fd都拷贝到内核,轮询后拷贝回用户态
    • 如果listenfd有可读事件发生,将connfd加入到poll
    • 遍历所有fd,如果fd有可读事件发生,recv()读取数据并send()响应客户端,数据读取完成后关闭connfd

代码实现:

int main(){
    ...

    struct pollfd fds[POLL_SIZE] = {0};   // fd的数量可自定义
	fds[0].fd = listenfd;   // 先将listenfd加入poll
	fds[0].events = POLLIN;   // select将事件分为三类,poll将这三类事件统一管理

	int max_fd = listenfd;
	int i = 0;
	for (i = 1;i < POLL_SIZE;i ++) {
		fds[i].fd = -1;           // 将poll中的fd置为-1
	}

	while (1) {
		int nready = poll(fds, max_fd+1, -1);   // 把fd拷贝到内核,再拷贝出来
		if (fds[0].revents & POLLIN) {      // 如果listenfd在poll中,且有可读事件发生(也就是来连接了),revents实际发生的事件,pollout为可写事件
			struct sockaddr_in client;
		    socklen_t len = sizeof(client);         // 取connfd
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }
			printf("accept \n");
			fds[connfd].fd = connfd;      // 将connfd加入poll
			fds[connfd].events = POLLIN;

			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;
		}

		//int i = 0;
		for (i = listenfd+1;i <= max_fd;i ++)  {
			if (fds[i].revents & POLLIN) {   // fd i 发生了且为POLLIN类型
				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					send(i, buff, n, 0);
		        } else if (n == 0) { // 无数据可读后,关闭该connfd
					fds[i].fd = -1;
		            close(i);
		        }
				if (--nready == 0) break;
			}
		}
	}
}

poll解决的问题:没有最大文件描述符数量的限制

但fd在内核态与用户态的来回拷贝,以及需要轮询所有fd,使得其监听事件的开销过大,无法支持太大的并发量,且poll不像select可以跨平台,其只能在Linux平台使用

epoll网络模型

select和poll都是只需调用一个函数,epoll需要调用三个:epoll_create、epoll_ctl(ADD, DEL, MOD)、epoll_wait

epoll结构为:

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

其处理流程为:

  • epoll_create(): 创建epfd,创建红黑树以及就绪列表(链表)
  • 将listendfd绑定可读事件,加入epoll (listenfd和connfd都只需要从用户态拷贝到内核态一次了)
  • 进入while(1){}循环
  • epoll_wait():将就绪列表从内核态拷贝到用户态(从内核态拷贝到用户态只要拷贝就绪的事件了)
  • 遍历就绪列表(不需要遍历全部fd了)
    • 如果是listenfd有可读事件,将connfd加入到epoll中
    • 如果是connfd有可读事件,读取数据,并send(),读完了从epoll中移除,并关闭该fd

代码实现:

int main(){
    int epfd = epoll_create(1); //int size(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfd

	struct epoll_event events[POLL_SIZE] = {0};   // 这里POLL_SIZE就是每次取事件的最大数量,小一点没关系(如50),因为即使百万并发,活跃的也就1w,多跑几次了就是了
	struct epoll_event ev;

	ev.events = EPOLLIN;
	ev.data.fd = listenfd;

	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);  // 将listenfd加入epoll,拷贝到内核:只需要拷贝一次,不需要拷贝出来

	while (1) {
		int nready = epoll_wait(epfd, events, POLL_SIZE, 5);   // 取事件, 拷贝到用户态:只拷贝就绪的事件了
		if (nready == -1) {
			continue;
		}
		int i = 0;
        // 遍历就绪队列
		for (i = 0;i < nready;i ++) {
			int clientfd =  events[i].data.fd;
			if (clientfd == listenfd) {   // 处理listenfd.如果监听到了多个连接,也只一个一个地取,多取几次就是了
				struct sockaddr_in client;
			    socklen_t len = sizeof(client);
			    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
			        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
			        return 0;
			    }
				printf("accept\n");
				ev.events = EPOLLIN;
				ev.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
			} else if (events[i].events & EPOLLIN) {   // 处理connfd
				n = recv(clientfd, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					send(clientfd, buff, n, 0);
		        } else if (n == 0) { //  读完了就从epoll中移除connfd
					ev.events = EPOLLIN;
					ev.data.fd = clientfd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
		            close(clientfd);
		        }
			}
		}
	}
    close(listenfd);
    return 0;
}

epoll解决了select面临的三大问题,可实现C1000K的并发量

完整代码

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <sys/select.h>
#include <sys/poll.h>
#include <sys/epoll.h>

#include <pthread.h>
 
#define MAXLNE  4096
#define POLL_SIZE	1024

//8m * 4G = 128 , 512
//C10k
void *client_routine(void *arg) { //
	int connfd = *(int *)arg;
	char buff[MAXLNE];
	while (1) {
		int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
			break;
        }
	}
	return NULL;
}


int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {    // 监听tcp连接
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 
    memset(&servaddr, 0, sizeof(servaddr));    // 服务地址
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 将uint32值转换为网络字节顺序,0.0.0.0系统将调用默认ip地址,为啥不是127.0.0.1呢
    servaddr.sin_port = htons(9999);    // 将整型变量转换成网络字节顺序,通过端口socket绑定到某一进程
    
    // 当用socket()函数创建套接字以后,套接字在名称空间(网络地址族)中存在,但没有任何地址给它赋值
    // bind()把用addr指定的地址赋值给sockfd。addrlen指定了以addr所指向的地址结构体的字节长度。一般来说,该操作称为“给套接字命名”。
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    // socket创建套接字=》bind(),给套接字地址=》listen监听连接=》accept
    // 该监听fd最多连接10个连接
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
 #if 0
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("========waiting for client's request========\n");
    while (1) {

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
    // tip: 至此,都两个客户端同时连接accept时,第二个客户端只能连接成功,但发送不了数据
    // 这是因为accept只取了一个连接,只有第一个连接能发送数据
#elif 0
    printf("========waiting for client's request========\n");
    while (1) {
        // 把accept放到while循环里
        // 新问题:每个连接都能发送数据,但只能发送一条数据
        // 原因是while中又两个阻塞点:accept和recv,一个连接发送数据后,服务器将阻塞在accept
		struct sockaddr_in client;     
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }

        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);

	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }

#elif 0
    // 多线程:来一个连接新建一个线程,把connfd传给入口函数,接收发送数据
    // 客户端不多,可以用这种方法,客户端太多就不行
    // 如一个线程分配8M的栈空间,1G的内存只能分配128个线程左右 ,性能突破不了C10K
	while (1) {

		struct sockaddr_in client;
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }

		pthread_t threadid;
		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    }

#elif 0

    //   所以能不能用少数的线程处理所有fd呢 
    //   当一个fd接收到数据时,服务器能知道是哪个连接发的数据,并进行处理
	//   // fd_set就是个bit-set,第n个bit有事件到达,就将第n个bit位置1
    // 由于是bit-set,能监听的fd是固定的,要改还得去内核改,默认是1024

    // 对fd的处理包括两部分,listenfd和读写fd
    // 一个select能管理1024个fd,那么多弄几个select(一个进程或线程一个select),就能到达C10K了,但很难达到C1000K
    // 因为首先要将rset拷贝到内核,再全部拷贝出来,开销太大
	fd_set rfds, rset, wfds, wset;   

	FD_ZERO(&rfds);        // 先把bit-set清空
    // 设置listenfd-set (也就是我们要监控哪些集合,这个集合copy到内核);还有个集合是哪些fd置1了(这个集合从内核copy出来)
	FD_SET(listenfd, &rfds);      // 将listenfd加入 rfds读事件集合 
	FD_ZERO(&wfds);     // 写事件集合

	int max_fd = listenfd;   // 当前管理的所有文件描述符的最大值,也就bit-set的长度

	while (1) {

		rset = rfds;   // 为啥还要弄这两个变量:防止读集合rfds在select被修改了
		wset = wfds;
        // 第二、三个参数:要管理哪些文件描述符的读(写)的事件,放到相应的集合
        // 第四个是异常事件,第五个超时时间:如果隔这么久一直没有事件发生,就返回,为空就是没有事件一直阻塞(select自带阻塞)
        // 调用select需要把fd从用户态拷贝到内核态,而且需要在内核遍历传递进来的所有fd
        // 把rfds和wfds给内核,rset和wset是返回给用户的发生事件的文件描述符
		int nready = select(max_fd+1, &rset, &wset, NULL, NULL);    // 返回事件的数量,这里其实只有读事件

		if (FD_ISSET(listenfd, &rset)) { // listenfd是否在读事件集合中

			struct sockaddr_in client;
		    socklen_t len = sizeof(client);
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {  // 将connfd加入到读事件集合
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }

			FD_SET(connfd, &rfds);   // 将connfd加入读事件集合
			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;    // 全部加完了,去对读写事件进行操作
		}

		int i = 0;
        // listenfd是一个bit-map的做法,01固定,从3开始递增分配(3,4,5,6),如果4回收了,再从4分配
		for (i = listenfd+1;i <= max_fd;i ++) {   // 遍历所有fd,依次进行读写操作,,这里不应该是可读可写事件集合吗

			if (FD_ISSET(i, &rset)) { // 

				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					FD_SET(i, &wfds);   // 数据读完要加入写事件集合??一次没读完怎么办?
					//reactor
					//send(i, buff, n, 0);
		        } else if (n == 0) { //
					FD_CLR(i, &rfds);  // 从读事件集合删除
					//printf("disconnect\n");
		            close(i);
		        }
				if (--nready == 0) break;
			} else if (FD_ISSET(i, &wset)) {
				send(i, buff, n, 0);
				FD_SET(i, &rfds);       // 发送完了这个fd为啥要加入读事件集合,为啥不从写事件集合删除??
			}
		}
	}

#elif 0

	struct pollfd fds[POLL_SIZE] = {0};   // fd的数量可自定义
	fds[0].fd = listenfd;   // 先将listenfd加入poll
	fds[0].events = POLLIN;   // select将事件分为三类,poll将这三类事件统一管理

	int max_fd = listenfd;
	int i = 0;
	for (i = 1;i < POLL_SIZE;i ++) {
		fds[i].fd = -1;           // 将poll中的fd置为-1
	}

	while (1) {
		int nready = poll(fds, max_fd+1, -1);   // 把fd拷贝到内核,再拷贝出来
		if (fds[0].revents & POLLIN) {      // 如果listenfd在poll中,且有可读事件发生(也就是来连接了),revents实际发生的事件,pollout为可写事件
			struct sockaddr_in client;
		    socklen_t len = sizeof(client);         // 取connfd
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }
			printf("accept \n");
			fds[connfd].fd = connfd;      // 将connfd加入poll
			fds[connfd].events = POLLIN;

			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;
		}

		//int i = 0;
		for (i = listenfd+1;i <= max_fd;i ++)  {
			if (fds[i].revents & POLLIN) {   // fd i 发生了且为POLLIN类型
				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					send(i, buff, n, 0);
		        } else if (n == 0) { // 无数据可读后,关闭该connfd
					fds[i].fd = -1;
		            close(i);
		        }
				if (--nready == 0) break;
			}
		}
	}
#else
	int epfd = epoll_create(1); //int size(为了兼容,以前就绪队列是固定的,后面改成链表了) 创建epfd

	struct epoll_event events[POLL_SIZE] = {0};   // 这里POLL_SIZE就是就绪队列的大小了,小一点没关系(如50),因为即使百万并发,活跃的也就1w,多跑几次了就是了
	struct epoll_event ev;

	ev.events = EPOLLIN;
	ev.data.fd = listenfd;

	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);  // 将listenfd加入epoll,拷贝到内核:只需要拷贝一次,不需要拷贝出来

	while (1) {
		int nready = epoll_wait(epfd, events, POLL_SIZE, 5);   // 取事件, 拷贝到用户态:只拷贝就绪的事件了
		if (nready == -1) {
			continue;
		}
		int i = 0;
        // 遍历就绪队列
		for (i = 0;i < nready;i ++) {
			int clientfd =  events[i].data.fd;
			if (clientfd == listenfd) {   // 处理listenfd
				struct sockaddr_in client;
			    socklen_t len = sizeof(client);
			    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
			        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
			        return 0;
			    }
				printf("accept\n");
				ev.events = EPOLLIN;
				ev.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
			} else if (events[i].events & EPOLLIN) {   // 处理connfd
				n = recv(clientfd, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					send(clientfd, buff, n, 0);
		        } else if (n == 0) { //  读完了就从epoll中移除connfd
					ev.events = EPOLLIN;
					ev.data.fd = clientfd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
		            close(clientfd);
		        }
			}
		}
	}
	
#endif
 
    close(listenfd);
    return 0;
}

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

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

相关文章

Python学习笔记-PyQt6之MVC项目结构初试

MVC结构是之model-view-controller三层架构的开发框架&#xff0c;用以将项目界面和逻辑进行解耦分析&#xff0c;便于维护。与WPF的MVVM相似。 项目开发做了一个秒表试手&#xff1a; 1.项目架构如下 controller&#xff1a;用于放置界面的控制逻辑model&#xff1a;用于放置…

回顾这十年,感悟

十年前&#xff0c;我35岁了&#xff0c;在体制内工作&#xff0c;到了很多人眼里的躺平的年龄。我眼里的世界&#xff0c;也就那么大&#xff0c;没有想过更进一步&#xff0c;有点中年油腻了&#xff0c;体质也差了。……终于有一天&#xff0c;醒悟了&#xff0c;不想过这样…

高并发系统设计 -- 秒杀系统

高并发秒杀 秒杀问题相信大家都知道的&#xff0c;虽然是一个烂大街的项目&#xff0c;但是秒杀问题背后的知识是很值得学习的&#xff0c;很多高并发系统设计都可以参照秒杀系统来进行实现。而且顺着这个问题&#xff0c;我会教给大家如何进行高并发的系统设计。 我们先来看…

Android集成三方浏览器之Crosswalk

上一篇讲解了腾讯 X5 内核的集成&#xff0c;这一篇是讲解 Crosswalk 的集成 Crosswalk 也是采用了Chromenium 内核&#xff0c;是一款开源的 web 引擎&#xff0c;开发者可以直接把 Crosswalk 嵌入到应用之中&#xff0c;当然也支持共享模式&#xff08;系统中没有对应的 Cros…

费解的开关(BFS+哈希表+二进制枚举)

费解的开关&#xff08;BFS哈希表二进制枚举&#xff09;一、题目二、思路分析1、算法标签2、思路梳理方法1&#xff1a;BFS哈希表方法2&#xff1a;二进制枚举DFS一、题目 二、思路分析 1、算法标签 这道题考察的是BFS哈希表,DFS二进制枚举 2、思路梳理 方法1&#xff1a;…

Cohen–Sutherland 算法介绍(简单易懂)

目录 一、算法介绍 二、算法描述 三、算法总结 一、算法介绍 Cohen–Sutherland 算法用于直线段裁剪&#xff0c;通过判断直线与窗口之间的关系&#xff0c;来决定直线段部分的保留与舍弃。 二、算法描述 ① 首先&#xff0c;我们把屏幕分割成 9 个区域块&#xff0c;最中间区…

音乐相册如何制作?一步一步教会你

很多小伙伴会在旅行时&#xff0c;拍摄各种好看的照片&#xff0c;一趟旅途下来能留下好多照片呢&#xff0c;有些人会习惯将这些照片归类到一个相册里。其实我们也可以使用一些免费的软件将这些照片制作成有纪念意义的音乐相册&#xff0c;那大家知道免费制作音乐相册怎么做吗…

npm install 报警告npm WARN

npm install 报警告npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents1.2.0 (node_modules\fsevents npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN fsevents1.2.0 had bundled packages that do not match the requi…

Crack:Inobitec DICOM Viewer Pro 2.9 多语言版本

Inobitec DICOM Viewer Pro 的使命是扩大医生可见和可能的范围。通过为医学提供高质量的创新 IT 解决方案&#xff0c;Ω578867473为改善全世界人民的健康做出了贡献。感受到自己工作的价值&#xff0c;意识到 21 世纪医学面临的挑战的重要性&#xff0c;以及解决这些挑战的乐趣…

WordPress使用二级域名存储图片等静态资源达到网站加速的详细配置

最近发现源站压力较大&#xff08;水管太小&#xff09;于是想着把WordPress博客的图片等静态资源分离到二级域名中&#xff0c;二级域名再使用一次云盾免费加速CDN&#xff0c;达到动静分离的效果&#xff0c;在这个过程中遇到一些坑&#xff0c;特此记录一下&#xff0c;方便…

NumpyPandas 数据处理与挖掘

笔记来源B站&#xff1a;https://www.bilibili.com/video/BV1xt411v7z9?p21 python学习笔记1 Numpy1.1 Numpy优势1.1.1 Numpy介绍1.1.2 ndarray介绍1.1.3 ndarray与Python原生list效率对比1.1.4 ndarray优势1.2 认识N维数组-ndarray属性1.2.1 ndarray的属性1.2.2 ndarray的形状…

11.1、基于Django4的可重用、用户注册和登录系统搭建

文章目录系统的功能思路分析搭建项目环境创建项目&#xff08;虚拟环境&#xff09;创建子应用修改语言、时区创建数据库表启动项目git提交项目代码到本地仓库git initi 初始化&#xff0c;创建本地git仓库pycharm安装 .ignore插件&#xff0c;来设置git的忽略文件提交代码修改…

SpringBoot+VUE前后端分离项目学习笔记 - 【09 SpringBoot集成MyBatis-Plus和SwaggerUI】

集成mybatis-plus依赖 官网 : https://baomidou.com/ pom.xml <!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version></depe…

01月份图形化一级打卡试题

活动时间 从2023年 1月1日至1月21日&#xff0c;每天一道编程题。 本次打卡的规则如下&#xff1a; &#xff08;1&#xff09;小朋友每天利用10~15分钟做一道编程题&#xff0c;遇到问题就来群内讨论&#xff0c;我来给大家答疑。 &#xff08;2&#xff09;小朋友做完题目后&…

认证的未来:2023 年值得关注的四大趋势

在经济不确定性和地缘政治紧张的一年中&#xff0c;数字领域充满网络威胁也就不足为奇了。从广泛的假冒诈骗到日益增多的短信网络钓鱼&#xff0c;网络攻击的频率和严重程度在 2022 年有所增加&#xff0c;这突显了所有行业的组织身份验证漏洞。 因此&#xff0c;当我们翻开新…

amis组件学习的配置介绍(二)

table view 表格视图 这个看文档也很好理解&#xff0c;但是还是需要介绍一下。 trs&#xff1a; <Array>设置表格行属性。tds: <Array>设置单元格属性。 {"type": "table-view",// 设置表格行"trs": [{"background": &…

常见排序算法(上)

篮球哥温馨提示&#xff1a;编程的同时不要忘记锻炼哦&#xff01;稳定的排序算法&#xff0c;可以设计成不稳定的. 目录 1、 认识排序 2、常见排序的分类 3、直接插入排序 4、希尔排序(缩小增量排序) 5、选择排序 6、堆排序 1、 认识排序 在学校中&#xff0c;如果我们…

QML学习笔记【03】:动画

动画是在指定的时间内&#xff0c;一系列属性的持续变化 1 动画元素&#xff08;Animation Elements&#xff09; 有几种类型的动画&#xff0c;每一种都在特定情况下都有最佳的效果&#xff0c;下面列出了一些常用的动画&#xff1a; PropertyAnimation&#xff08;属性动画…

人工智能学习07--pytorch01

一、pytorch简介 1、与TensorFlow区别 2、常用网络层 二、pytorch需要&#xff1a; 1、anaconda 2、CUDA 只能在NVIDIA上运行 ↓我发现电脑果然没有这个显卡 https://zhidao.baidu.com/question/2084255692200398828.html 3、pycharm 新项目要配置python的编译器&#xff…

Leetcode 36. 有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09;注意…