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

从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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值