从socket开始讲解网络模式

news2024/11/15 23:56:51

从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/131450.html

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

相关文章

LaoCat带你认识容器与镜像(一)

准备更新一个容器与镜像相关的系列&#xff0c;从Docker到K8s的入门再到实际项目进阶应用&#xff0c;这里感谢好朋友泽鹏&#xff0c;是他让我结识容器与镜像&#xff1b;也感谢上家公司菲恩曼&#xff0c;是它给了我去学习、实践的机会&#xff1b;最后感谢翼哥&#xff0c;一…

Linux系统下at任务调度机制

Linux系统下at任务调度机制 基本介绍 at命令是一次性定时计划任务&#xff0c;at 的守护进程 atd 会以后台模式运行&#xff0c;检查作业队列来运行。默认情况下&#xff0c;atd 守护进程每60秒检查作业队列&#xff0c;有作业时&#xff0c;会检查作业运行时间&#xff0c;如果…

深入理解计算机系统_可重定位目标文件的格式---elf格式

本篇笔记记录可重定位目标文件的格式— elf格式&#xff0c;也是《深入理解计算机系统》第7章的内容。了解这些内容&#xff0c;对我们很有帮助&#xff0c;比如代码排错&#xff0c;可以深入了解C/C 实现原理。 分别介绍如何得到可重定位目标文件及其格式。 2.1 如何得到可重…

操作系统~Linux~线程的互斥,mutex互斥锁的使用及其原理

1.一些基本概念 1&#xff0e;临界资源&#xff1a;凡是被线程共享访问的资源都是临界资源&#xff08;多线程、多进程打印数据到显示器&#xff0c;显示器就是临界资源&#xff09; 2&#xff0e;临界区&#xff1a;代码中访问临界资源的代码&#xff08;在代码中&#xff0c;…

kotlin学习笔记之注解与反射

一、声明并应用注解 一个注解允许你把额外的元数据关联到一个声明上。然后元数据就可以被相关的源代码工具访问&#xff0c;通过编译好的类文件或是在运行时&#xff0c;取决于这个注解是如何配置的。 1、应用注解 在kotlin中使用注解的方法和java一样。要应用一个注解&#xf…

如何通过3个月自学成为网络安全工程师!

前言&#xff1a; 趁着今天下班&#xff0c;我花了几个小时整理了下&#xff0c;非常不易&#xff0c;希望大家可以点赞收藏支持一波&#xff0c;谢谢。 我的经历&#xff1a; 我 19 年毕业&#xff0c;大学专业是物联网工程&#xff0c;我相信很多人在象牙塔里都很迷茫&…

Pycharm配置关于pyside6的外部工具

文章目录一、前言二、Pycharm配置1、designer.exe&#xff08;1&#xff09;打开Pycharm的设置&#xff08;2&#xff09;相关参数&#xff08;可复制粘贴&#xff09;2、Pyside6-uic.exe&#xff08;1&#xff09;设置&#xff08;2&#xff09;相关参数&#xff08;可复制粘贴…

Java--抽象类和接口的区别

今天是22年最后一天了, 写篇博客记录一下吧, 这一年发生了很多事情, 也学到了很多知识, 后面要继续加油啊, 大家也要加油啊, 米娜桑. 目录 概述 区别 1. 定义关键字不同 2. 继承或实现的关键字不同 3. 子类扩展的数量不同 4. 属性访问控制符不同 5. 方法控制符不同 6.…

python互联网程序设计GUI程序设计和网络程序设计(人机互动聊天软件)

1&#xff0e;项目意义 1、了解网络的结构&#xff1b; 2、了解网络传输协议&#xff1b; 3、掌握基本的网络编程方法。 2&#xff0e;总体设计 使用 TCP 协议实现人机聊天互动&#xff0c;程序具有服务端和客户端&#xff1a; &#xff08;1&#xff09;必备功能&#xff1…

Java财务在线咨询网站系统财务咨询网

简介 财务咨询网站&#xff0c;可以咨询公司代办&#xff0c;代理记账等一系列的财务问题的资讯服务网站 演示视频 https://www.bilibili.com/video/BV1T54y1H7Ar/?share_sourcecopy_web&vd_sourceed0f04fbb713154db5cc611225d92156 角色 管理员客服注册用户游客 技术…

Spring之DI入门案例

目录 一&#xff1a;DI入门案例实现思路分析 1.要想实现依赖注入&#xff0c;必须要基于 IOC 管理 Bean 2.Service 中使用 new 形式创建的 Dao 对象是否保留 ? 3.Service 中需要的 Dao 对象如何进入到 Service 中 ? 4.Service 与 Dao 间的关系如何描述 ? 二&#xff1…

(Qt) cmake编译Qt项目

文章目录前言环境cmake基础预备的项目代码文件资源路径demo.promain.cppres.qrcmywidget.cppmywidget.hmywidget.ui运行效果CMake文件资源路径CMakeLists.txt生成与构建END前言 通常我们在编写qt的时候都是在Qt creator中。而如何在VS Code中编写qt就是本文需要解决的问题 环…

顺序表 —— 初始化、销毁、打印、增加、删除、查找、修改

1.何为线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串…线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直…

zotero导出pdf

今天老师给我改论文的时候布置了一个任务&#xff0c;让我把所有论文的pdf按格式打包发给她。可是之前我用zotero的时候都是在线保存的&#xff0c;有些是没有pdf的&#xff0c;怎么办&#xff1f;而且就算有pdf&#xff0c;他们的命名格式也五花八门&#xff0c;难道一个个手改…

kafka 消息日志原理 指定偏移量消费 指定时间戳消费

Kafka 日志详解 Apache Kafka日志存储在物理磁盘上各种数据的集合&#xff0c;日志按照topic分区进行文件组织&#xff0c;每一个分区日志由一个或者多个文件组成。生产者发送的消息被顺序追加到日志文件的末尾。 如上图所述&#xff0c;Kafka主题被划分为3个分区。在Kafka中&…

vscode使用跳板机(密钥)进入内网并连接内网中其它机器(密码)

经过简单测试 1、不能像xshell一样选择服务器的密钥登陆&#xff0c;只能通过将本机的公钥传到服务器上 2、不能使用本地socket5做代理登录 3、不能使用系统代理登录 一、使用密钥连接到跳板机 1、内网穿透 2、将本机公钥上传到服务器上 1&#xff09;建立密钥对 无论是win…

redis集群 mac安装

1.安装redis mac环境用brew install安装 brew install redis 安装好后默认配置启动单点服务 redis-server 注&#xff1a;brew默认程序安装在/usr/local/Cellar目录下 /usr/local/Cellar/redis 默认配置文件在 /usr/local/etc/redis.conf 2.创建配置文件 准备创建6个节…

谣言检测数据集

1 PHEME-R 这是一个在PHEME FP7项目的新闻学用例中收集和注释的数据集。这些谣言与9个不同的突发新闻相关。它是为分析社交媒体谣言而创建的&#xff0c;包含由谣言推文发起的推特对话&#xff1b;对话包括对这些谣言推文的回应推文。这些推文已经被注解为支持度、确定性和证…

VS2012安装教程

我要学只有我们两个人懂得C语言。 安装包&#xff1a;https://pan.baidu.com/s/1YR7Xk9Zlv7zQWCsERdVgIQ [提取码]&#xff1a;stvi 1、右键以管理员身份运行 “vs_ultimate.exe” 2、编辑软件安装位置&#xff0c;然后点击同意许可&#xff0c;之后点下一步即可&#xff01; 3…

mongoDB聚合查询

管道 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。 聚合管道操作 可参考菜鸟文档&#xff1a;菜鸟文档 命令 功能描述 $project指定输出…