网络io,select,poll与epoll的初步认识

网络io与select,poll,epoll的初步认识


文章目录

  • 网络io与select,poll,epoll的初步认识
  • 一、网络io
  • 二、select
  • 三、poll
  • 四、epoll的初步认识
  • 总结


一、网络io

.为什么单线程能接受多个连接但是不能传输数据?
因为listenfd处于listen状态,三次握手是在协议栈完成的,不受应用程序控制。三次连接不发生在任意函数中,是协议栈自动完成的!
一请求一线程
2.如何解决多个客户端的连接问题:
一个请求一个线程
逻辑简单,不适合高并发。一个线程栈8M,难以突破C10k。
普通的网络io当中,基本上是一请求一回应
下面是一请求一回应

// 请求与响应
int main(int argc, char **argv) 
{
//以下服务端的处理
    int listenfd, connfd, n; //创建fd
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
 // 利用socket函数创建套接字
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 
        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);
    servaddr.sin_port = htons(9999);
//绑定
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
//调用listen函数将套接字转为可接收的状态
    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);
    //调用accept函数接受连接请求
    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);
        }
        close(connfd);
    }
 }//以上这个是,一个客户端和一个服务端的交互

以上这个是,一个客户端和一个服务端的交互,有了这个我们再去思考一个问题就是,如何进行多个客户端交互呢
我们先想了个办法,我们把

	struct sockaddr_in client;
  socklen_t len = sizeof(client);
  //调用accept函数接受连接请求
  if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
  }

这部分代码复制到while循环里

 printf("========waiting for client's request========\n");
    while (1) {
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    //调用accept函数接受连接请求
    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);
     }
        close(connfd);
    }

我们就会发现,虽然能连接多个客户端,但是客户端不能持续地发送消息,只能发送一条。
所以这种方式只解决了多个客户端连接的问题,并不能正常工作
因为一个客户端一连接,就会阻塞在accept这里,accept一返回,然后往下走,send也会阻塞。所以只会多个连接,每个客户端只会发一条
在这里,我们又想到多线程,一个连接分配一个线程,这样可行吗
我们继续改一下

 printf("========waiting for client's request========\n");
    while (1) {
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    //调用accept函数接受连接请求
    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) { //

	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大小的线程来算的话,4个g的内存,那么就是512个线程
随着客户端数量越来越多,那么你的内存空间会越来越满,导致系统可能重启,很难突破一个数量,所以很难实现万级连接

二、select

在以上的场景中,我们会阻塞在accept和recv这两个场景
我们就要想一个办法,去分清fd,能够知道fd是哪个客户端发送的,哪个fd是需要处理的,理清楚哪个连接是出问题的,把若干个fd,放到一个组件中集中去处理
我们这里引入一个io多路复用的组件,叫select
一个客户端连接进来都有一个fd,选择需要处理的fd,首先我们需要一个fd的集合
fd_set rfds;//fd_set通过一个bit_set,一个fd进来后,如果有事件需要处理,就把这个bit位置为1,bit的下标对应fd的值。
fd_set有多大呢,1024/(8*sizeof(long))

	fd_set rfds, rset, wfds, wset;// fd的集合,bit_set 
	FD_ZERO(&rfds);//先清空fd集合 
	FD_SET(listenfd, &rfds);//监控listenfd是需要处理的,是有数据的是可读的 
	FD_ZERO(&wfds);//清空可写的集合 
	int max_fd = listenfd;//需要select轮询的fd的最大值,也相当于公司员工id的最大值 
	while (1) {
		rset = rfds;
		wset = wfds;
		int nready = select(max_fd+1, &rset, &wset, NULL, NULL);//监控是否可读可写,第二个三个四个参数是fd的属性,第五个参数就是多长时间轮询一次 ,select函数本身一个阻塞的,如果监控的集合里边没有符合要求(可读的)的,如果是null则会一直阻塞,如果传进去一个值就是 
		if (FD_ISSET(listenfd, &rset)) { //如果是listen的fd 
			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;
		    }
			FD_SET(connfd, &rfds);
			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;
		}
		int i = 0;
		for (i = listenfd + 1;i <= max_fd;i ++) {// 从listen的后一位开始 
			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);这里用send也可以,因为buff没有满
		        } else if (n == 0) { //
					FD_CLR(i, &rfds);//清除fd,如果不清除,会一直在这里处理
						printf("disconnect\n");
		          	close(i);
		        }
				if (--nready == 0) break;
			} else if (FD_ISSET(i, &wset)) {//判断是否可写
				send(i, buff, n, 0);//发送数据
				FD_SET(i, &rfds);
			}
		}
	}

我们来分析一下select函数
int select (int maxfd + 1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout);最大的特点是带五个参数,分别传入要监听读、写、错误事件的fd_set,并返回对应位是否有事件。maxfd + 1传给内核,内核根据maxfd + 1遍历所有fd,如果缓冲区有数据可读且注册了改fd的读事件就将readset对应位置1后返回给用户。挨个检测fd, fd有数据可读,就置可读事件。 fd的sendbuffer为空,就置为可写。
使用FD_SET(socket,&fds)就是把对应为置1,表示监听该socket,最大的socket+1就是set的有效长度,也是内核遍历次数。读、写、错误事件分别对应不同的set,select的第二、三四个函数既是传入参数又是返回值。
比一请求一线程好在哪里,难以突破c10k,select能够解决大部分,1024个fd,多做几个select,c10k可以突破,但是难以突破c1000k,为什么呢,select需要把rset集合copy到内核里边,可能集合里边就一个可读,然后再返回出来。对于大量的fd的数量select还是有局限的。

三、poll

我们来分析一下poll
和select的监听机制是差不多的,但是在区分记录读、写、错误事件返回给用户的方式不同,select选择以3个独立的set区分,而poll是将其记录在返回的结构体的字段中。因此接口也有些不同。

struct pollfd fds[POLL_SIZE] = {0};//POLL_SIZE=1024
	fds[0].fd = listenfd;
	fds[0].events = POLLIN;

	int max_fd = listenfd;
	int i = 0;
	for (i = 1;i < POLL_SIZE;i ++) {
		fds[i].fd = -1;
	}
	while (1) {
		int nready = poll(fds, max_fd+1, -1);//
		if (fds[0].revents & POLLIN) {
			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");
			fds[connfd].fd = connfd;
			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) {//判断是否可读
				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) { //
					fds[i].fd = -1;
		            close(i);					
		        }
				if (--nready == 0) break;
			}
		}
	}

四、epoll的初步认识

epoll有了一个epoll的表去存取所有的内容,就相当于一栋楼,有了个快递员利用一个信箱去处理整栋楼住户想处理的信息。

//poll/select --> 
	// epoll_create 创建一个集合,快递员负责的那栋楼
	// epoll_ctl(ADD, DEL, MOD) 添加,删除,第三个就是修改
	// epoll_wait 多长时间执行一次,快递员多久去看一次

	int epfd = epoll_create(1); //系统调用,参数必须大于0
	struct epoll_event events[POLL_SIZE] = {0};//相当于快递员的盒子
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = listenfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);//往快递员的盒子里添加需要处理的邮件
	while (1) {
		int nready = epoll_wait(epfd, events, POLL_SIZE, 5);//在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) {//如果不是listenfd,判断是否可读
				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);//这里先recv再send的做法是比较业余的,以后会改正
		        } else if (n == 0) { //
					ev.events = EPOLLIN;
					ev.data.fd = clientfd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
		            close(clientfd);				
		        }
			}
		}
	}
	

epoll_wait(int epfd, epoll_event events, int max events, int timeout)
底层将注册和监听的逻辑分开,不是每次都要遍历fd进行添加和删除。epoll_wait的第二个参数,用户层有了rdlist,直接返回有事件的socket的引用,不再需要遍历判断是否有事件,直接拿出来用就可以,效率比较高。

总结

服务器核心的点就是一个循环,反复去轮询检查是否有事件可以处理
关于epoll的知识,还有更多的东西还未讨论,未完待续。。。

你可能感兴趣的:(进阶知识,网络,服务器,linux)