147 Linux 网络编程3 ,高并发服务器 --多路I/O转接服务器 - select

从前面的知识学习了如何通过socket ,多进程,多线程创建一个高并发服务器,但是在实际工作中,我们并不会用到前面的方法 去弄一个高并发服务器,有更加好用的方法,就是多路I/O转接器

零 多路I/O转接服务器

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

主要使用的方法有三种 select , poll , epoll

其中select 可以跨平台

poll 和 epoll不能跨平台,只能在linux上使用。

重点是epoll

次重点是 select

poll知道就行了,再学习后我们会知道原因。

本章主要学习 select。

一 select

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
  2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。

select 函数借助内核监听两件事,:客户端连接,数据通信事件。

核心函数: select函数 ,FD_CLR,FD_ISSET,FD_SET,FD_ZERO

int select(int nfds, fd_set *readfds, fd_set *writefds,

fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd位清0

int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1

void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1

void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

复制代码
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);

	nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
	readfds:	监控有读数据到达文件描述符集合,传入传出参数
	writefds:	监控写数据到达文件描述符集合,传入传出参数
	exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
	timeout:	定时阻塞监控时间,3种情况
				1.NULL,永远等下去
				2.设置timeval,等待固定时间
				3.设置timeval里时间均为0,检查描述字后立即返回,轮询
	struct timeval {
		long tv_sec; /* seconds */
		long tv_usec; /* microseconds */
	};
	void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd位清0
	int FD_ISSET(int fd, fd_set *set); 	//测试文件描述符集合里fd是否置1
	void FD_SET(int fd, fd_set *set); 	//把文件描述符集合里fd位置1
	void FD_ZERO(fd_set *set); 			//把文件描述符集合里所有位清0

返回值:所有监听的集合中,满足事件发生的总数

server 端代码实现1

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <errno.h>
#include <sys/select.h>


#include "wrap.h"

//select 实现高并发代码示例
int main() {
	int listenfd;

	int ret = 0;
	//第一步:创建socket。 打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	//第二步 build IP+PORT到网络地址 到socket 创建出来的listenfd
	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(8000);

	Bind(listenfd, (struct sockaddr *) &servaddr,sizeof(servaddr));
	
	//第三步,设置可以同时监听的最大的数量为1024
	Listen(listenfd, 1024);

	//第四步,这时候 
	//void FD_CLR(int fd, fd_set *set);
	//int  FD_ISSET(int fd, fd_set *set);
	//void FD_SET(int fd, fd_set *set);
	//void FD_ZERO(fd_set *set);

	fd_set readfds;
	FD_ZERO(&readfds);
	FD_SET(listenfd, &readfds);
	fd_set allfds =  readfds;

	int maxfd = listenfd;

	//第五步,这时候就要弄一个循环去监听了。
	//第一次的时候,肯定是只监听了listenfd一个,都是后面,如果有cfd连接上的话,那就监听的多了
	//因此要使用一个maxfd 作为备份记录
	//select 函数的意义是:
//	int select(int nfds, fd_set *readfds, fd_set *writefds,
//		fd_set *exceptfds, struct timeval *timeout);
//
//	nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
//	readfds:	监控有读数据到达文件描述符集合,传入传出参数
//	writefds:	监控写数据到达文件描述符集合,传入传出参数
//	exceptfds:	监控异常发生达文件描述符集合, 如带外数据到达异常,传入传出参数
//	timeout:	定时阻塞监控时间,3种情况
//	1.NULL,永远等下去
//	2.设置timeval,等待固定时间
//	3.设置timeval里时间均为0,检查描述字后立即返回,轮询
//	struct timeval {
//	long tv_sec; /* seconds */
//	long tv_usec; /* microseconds */
//};返回值:所有监听的集合中,满足事件发生的总数

	int nready = 0;
	struct sockaddr_in clie_addr;
	int clie_addrlen = sizeof(clie_addr);
	int connectfd = 0;
	char buf[BUFSIZ];
	char str[INET_ADDRSTRLEN];

	while (1) {
		readfds = allfds;
		nready = select(maxfd + 1, &readfds,NULL,NULL,NULL);
		//nready为所有监听的集合中,满足事件发生的总数
		//第三个参数timeval我们设置的是NULL,表示阻塞等待
		//因此如果有事件发生,那么就会走到后面的代码,且readfds集合中会改动成为真正
		//有监听事件发生后,第一步做错误判断处理
		if (nready < 0 ) {
			//说明有error发生
			perr_exit("select error");
		}
		//第六步,判断nready中是否有 新的连接事件发生,也就是说listenfd是否
		if (FD_ISSET(listenfd, &readfds)) {
			//走到这里,说明有新的链接过来了,那么我们要做如下的几件事
			//6.1 server赶紧连接,注意,这时候并不会阻塞,因为listenfd已经有了读取事件才会走到这里
			connectfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addrlen);
			//这里加一行log,目的是将连接的客户端的信息打印出来
			printf("received from %s at PORT %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
				ntohs(clie_addr.sin_port));
			//这里添加一行判断,如果connectedfd已经超过1024,则不支持,select对于每个进程或者线程只能支持最多1024个
			if (connectfd == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
				fputs("too many clients\n", stderr);
				exit(1);
			}
			//6.2 将这个新的链接添加到 监听的readfds中

			FD_SET(connectfd,&allfds); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
		

			//6.3 更新maxfd
			if (connectfd > maxfd) {
				maxfd = connectfd;    /* 有可能最大的connectfd 需要变化,这是因为当有新的connectfd之前,如果没有任何的connectfd没有断开,则会变成最大,但是如果中间有connect断开,则这个新的connectfd的值,有可能不是最大。select第一个参数需要 */
									//假设之前来了,4,5,6,这时候客户端4已经断开了,这时候 connectfd就等于4,如果4,5,6都没有断开过,则这时候新来的会是7
			}

			//6.4 判断select 的返回值 是否只有一个,且是listenfd事件,如果走到这里说明这一个已经被处理了,因此就没有必要往下再执行了
			if (--nready == 0 ) {
				continue;
			}

		}



		//第七步,走到这里说明nready中有一个或者多个 客户端发送数据的事件过来。
		//我们当前的写法是通过循环1024次,挨个往出拿,/* 检测哪个clients 有数据就绪 */
		char buf[BUFSIZ] = {0};
		int realreadnum = 0;
		for (int i = listenfd+1; i < 1024;++i) {
			bzero(buf, BUFSIZ);
			if (FD_ISSET(i, &readfds)) {
			REREADPOINT:
				realreadnum = Read(i, buf, BUFSIZ); //真正的读取到了数据
				if (realreadnum == 0 ) {//在网络环境下,read函数返回0,说明是对端关闭了,也就是说,客户端关闭了
					//那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
					Close(i);
					FD_CLR(i, &allfds);
				}
				else if (realreadnum == -1) {
					
					if (errno == EINTR) {
						//说明是被信号打断的,一般要重新read
						printf("信号打断\n");
						goto REREADPOINT;
					}
					else if(errno == EAGAIN || errno == EWOULDBLOCK)
					{
						printf(" WOULDBLOCK \n");
						//说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
						//当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
						//一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
						goto REREADPOINT;
					}
					else if (errno == ECONNRESET) {
						//ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
						Close(i);
						FD_CLR(i, &allfds);
					}
					else {
						//这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
						//不退出程序是因为,这时候还有其他的链接连上的
						perror("read num <0");
					}
				}
				else if (realreadnum > 0 ) {
					//真正的读取到了客户端发送过来的数据
					for (int j = 0; j < realreadnum;++j) {
						buf[j] = toupper(buf[j]);
					}
					Write(i, buf,realreadnum);
					Write(STDOUT_FILENO,buf,realreadnum);
				}
				if (--nready == 0) { //有可能50个已经连接上的链接中,这时候只有3个有数据发送过来了,还是要从50个中遍历,但是不同的是,假设是 5,19,30,那么写这一块代码就能节省时间了,当遍历到30后,--nready 就会等于0,直接跳出
					break;    /* 跳出for, 但还在while中 */
				}
			}
		}

	}


	return ret;

}

server 端代码优化

可以看到,select函数在有客户端和服务器端沟通的过程中,需要依次遍历对比知道具体是哪一个connectfd有数据发送过来,这很影响工作效率,因此有了如下的优化。

优化的整体思路是:当有了connectfd链接后,就将这些connectfd记录到另一个数组中,并记录这个数组的最大下标。当客户端的链接断开的时候,则将当前的下标重置

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <errno.h>
#include <sys/select.h>


#include "wrap.h"
//从select1.c我们看到,代码还有可以完善的地方,这里就完善一下
//1.最后每次都要循环1020次,遍历查看哪些cfd有数据发送过来,这是不合理的
//因此我们需要搞一个数组,用这个数组记录真正的有数据访问的cfd
//并且弄一个int 值,记录这个数组的最大下标 maxi

//select 实现高并发代码示例
int main() {
	int listenfd;
	//额外添加代码 start
	int client[FD_SETSIZE];  /* 自定义数组client, 防止遍历1024个文件描述符  FD_SETSIZE默认为1024 */
	int maxi;

	maxi = -1;                                                  /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
	for (int i = 0; i < FD_SETSIZE; i++)
		client[i] = -1;                                         /* 用-1初始化client[] */

	//额外添加代码 end

	int ret = 0;
	//第一步:创建socket。 打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	//端口复用的代码添加上,端口复用的代码需要写在bind之前,socket之后
	int opt = 1; //1表示可以让端口复用,0表示不让端口复用

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	//第二步 build IP+PORT到网络地址 到socket 创建出来的listenfd
	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(8000);

	Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

	//第三步,设置可以同时监听的最大的数量为1024
	Listen(listenfd, 1024);

	//第四步,这时候 
	//void FD_CLR(int fd, fd_set *set);
	//int  FD_ISSET(int fd, fd_set *set);
	//void FD_SET(int fd, fd_set *set);
	//void FD_ZERO(fd_set *set);

	fd_set readfds;
	FD_ZERO(&readfds);
	FD_SET(listenfd, &readfds);
	fd_set allfds = readfds;

	int maxfd = listenfd;

	//第五步,这时候就要弄一个循环去监听了。
	//第一次的时候,肯定是只监听了listenfd一个,都是后面,如果有cfd连接上的话,那就监听的多了
	//因此要使用一个maxfd 作为备份记录
	//select 函数的意义是:
//	int select(int nfds, fd_set *readfds, fd_set *writefds,
//		fd_set *exceptfds, struct timeval *timeout);
//
//	nfds: 		监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
//	readfds:	监控有读数据到达文件描述符集合,传入传出参数
//	writefds:	监控写数据到达文件描述符集合,传入传出参数
//	exceptfds:	监控异常发生达文件描述符集合, 如带外数据到达异常,传入传出参数
//	timeout:	定时阻塞监控时间,3种情况
//	1.NULL,永远等下去
//	2.设置timeval,等待固定时间
//	3.设置timeval里时间均为0,检查描述字后立即返回,轮询
//	struct timeval {
//	long tv_sec; /* seconds */
//	long tv_usec; /* microseconds */
//};返回值:所有监听的集合中,满足事件发生的总数

	int nready = 0;
	struct sockaddr_in clie_addr;
	int clie_addrlen = sizeof(clie_addr);
	int connectfd = 0;
	char buf[BUFSIZ];
	char str[INET_ADDRSTRLEN];

	while (1) {
		readfds = allfds;
		nready = select(maxfd + 1, &readfds, NULL, NULL, NULL);
		//nready为所有监听的集合中,满足事件发生的总数
		//第三个参数timeval我们设置的是NULL,表示阻塞等待
		//因此如果有事件发生,那么就会走到后面的代码,且readfds集合中会改动成为真正
		//有监听事件发生后,第一步做错误判断处理
		if (nready < 0) {
			//说明有error发生
			perr_exit("select error");
		}
		//第六步,判断nready中是否有 新的连接事件发生,也就是说listenfd是否
		if (FD_ISSET(listenfd, &readfds)) {
			//走到这里,说明有新的链接过来了,那么我们要做如下的几件事
			//6.1 server赶紧连接,注意,这时候并不会阻塞,因为listenfd已经有了读取事件才会走到这里
			connectfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addrlen);
			//这里加一行log,目的是将连接的客户端的信息打印出来
			printf("received from %s at PORT %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
				ntohs(clie_addr.sin_port));

			//额外添加代码 start
			int i = 0;
			for (; i < FD_SETSIZE; i++)
				if (client[i] < 0) {                            /* 找client[]中没有使用的位置 */
					client[i] = connectfd;                         /* 保存accept返回的文件描述符到client[]里 */
					break;
				}
			//额外添加代码 end
			//这里添加一行判断,如果connectedfd已经超过1024,则不支持,select对于每个进程或者线程只能支持最多1024个
			if (connectfd == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
				fputs("too many clients\n", stderr);
				exit(1);
			}
			//6.2 将这个新的链接添加到 监听的readfds中

			FD_SET(connectfd, &allfds); /* 向监控文件描述符集合allset添加新的文件描述符connfd */


			//6.3 更新maxfd
			if (connectfd > maxfd) {
				maxfd = connectfd;    /* 有可能最大的connectfd 需要变化,这是因为当有新的connectfd之前,如果没有任何的connectfd没有断开,则会变成最大,但是如果中间有connect断开,则这个新的connectfd的值,有可能不是最大。select第一个参数需要 */
									//假设之前来了,4,5,6,这时候客户端4已经断开了,这时候 connectfd就等于4,如果4,5,6都没有断开过,则这时候新来的会是7
			}
			//额外添加代码 start
			if (i > maxi) {
				maxi = i;           /* 保证maxi存的总是client[]最后一个元素下标 */
			}
			//额外添加代码 end


			//6.4 判断select 的返回值 是否只有一个,且是listenfd事件,如果走到这里说明这一个已经被处理了,因此就没有必要往下再执行了
			if (--nready == 0) {
				continue;
			}

		}



		//第七步,走到这里说明nready中有一个或者多个 客户端发送数据的事件过来。
		//我们当前的写法是通过循环1024次,挨个往出拿,/* 检测哪个clients 有数据就绪 */
		char buf[BUFSIZ] = { 0 };
		int realreadnum = 0;
		//for (int i = listenfd + 1; i < 1024; ++i) {
		//	bzero(buf, BUFSIZ);
		//	if (FD_ISSET(i, &readfds)) {
		//	REREADPOINT:
		//		realreadnum = Read(i, buf, BUFSIZ); //真正的读取到了数据
		//		if (realreadnum == 0) {//在网络环境下,read函数返回0,说明是真的读取到文件末尾了
		//			//既然已经读取到了文件末尾,那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
		//			Close(i);
		//			FD_CLR(i, &allfds);
		//		}
		//		else if (realreadnum == -1) {

		//			if (errno == EINTR) {
		//				//说明是被信号打断的,一般要重新read
		//				printf("信号打断\n");
		//				goto REREADPOINT;
		//			}
		//			else if (errno == EAGAIN || errno == EWOULDBLOCK)
		//			{
		//				printf(" WOULDBLOCK \n");
		//				//说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
		//				//当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
		//				//一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
		//				goto REREADPOINT;
		//			}
		//			else if (errno == ECONNRESET) {
		//				//ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
		//				Close(i);
		//				FD_CLR(i, &allfds);
		//			}
		//			else {
		//				//这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
		//				//不退出程序是因为,这时候还有其他的链接连上的
		//				perror("read num <0");
		//			}
		//		}

		//		else if (realreadnum > 0) {
		//			//真正的读取到了客户端发送过来的数据
		//			for (int j = 0; j < realreadnum; ++j) {
		//				buf[j] = toupper(buf[j]);
		//			}
		//			Write(i, buf, realreadnum);
		//			Write(STDOUT_FILENO, buf, realreadnum);
		//		}
		//	}
		//}

		//额外添加代码 start

		int sockfd = 0;
		for (int i = 0; i <= maxi; i++) {  /* 检测哪个clients 有数据就绪 */
			if ((sockfd = client[i]) < 0)
				continue;
			bzero(buf, BUFSIZ);
			if (FD_ISSET(sockfd, &readfds)) {
			REREADPOINT:
				realreadnum = Read(sockfd, buf, BUFSIZ); //真正的读取到了数据
				if (realreadnum == 0) {//在网络环境下,read函数返回0,说明是对端关闭了,也就是说,客户端关闭了
					//那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
					Close(sockfd);
					FD_CLR(sockfd, &allfds);
					printf("read done\n");
					//额外添加的代码 start
					client[i] = -1;
					//额外添加的代码 end
				}
				else if (realreadnum == -1) {

					if (errno == EINTR) {
						//说明是被信号打断的,一般要重新read
						printf("信号打断\n");
						goto REREADPOINT;
					}
					else if (errno == EAGAIN || errno == EWOULDBLOCK)
					{
						printf(" WOULDBLOCK \n");
						//说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
						//当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
						//一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
						goto REREADPOINT;
					}
					else if (errno == ECONNRESET) {
						//ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
						Close(sockfd);
						FD_CLR(sockfd, &allfds);
						//额外添加的代码 start
						client[i] = -1;
						//额外添加的代码 end
					}
					else {
						//这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
						//不退出程序是因为,这时候还有其他的链接连上的
						perror("read num <0");
					}
				}

				else if (realreadnum > 0) {
					//真正的读取到了客户端发送过来的数据
					for (int j = 0; j < realreadnum; ++j) {
						buf[j] = toupper(buf[j]);
					}
					Write(sockfd, buf, realreadnum);
					Write(STDOUT_FILENO, buf, realreadnum);
				}
				if (--nready == 0) { //有可能50个已经连接上的链接中,这时候只有3个有数据发送过来了,还是要从50个中遍历,但是不同的是,假设是 5,19,30,那么写这一块代码就能节省时间了,当遍历到30后,--nready 就会等于0,直接跳出
					break;    /* 跳出for, 但还在while中 */
				}
			}
		}
		//额外添加代码 end

}


	return ret;

}
相关推荐
vortex534 分钟前
探索 Shell 中的扩展通配符:从 Bash 到 Zsh
linux·运维·bash·shell·zsh
czhc114007566338 分钟前
网络3 子网掩码 划分ip地址
服务器·网络·tcp/ip
不爱学英文的码字机器38 分钟前
[操作系统] 进程间通信:system V共享内存
linux·服务器·ubuntu
秋名RG39 分钟前
计算机网络起源
服务器·网络·计算机网络
‍。。。41 分钟前
Ubuntu 24.04 中文输入法安装
linux·运维·ubuntu
努力的搬砖人.1 小时前
nacos配置达梦数据库驱动源代码步骤
java·服务器·数据库·经验分享·后端
Yang三少喜欢撸铁1 小时前
【Linux部署DHCP服务】
linux·运维·服务器
GanGuaGua1 小时前
linux系统下如何提交git和调试
服务器·git
太极淘1 小时前
桌面版本及服务器版本怎么查看网络源软件包的url下载路径
运维·服务器·网络
自由与自然1 小时前
乐观锁与悲观锁的使用场景
java·服务器·数据库