SOCKET網絡編程四:SELECT單進程併發服務器

一、在使用select時,我們需要了解linux的五種IO模型和TCP的11種狀態:

1、阻塞IO:recv接收緩衝區有數據後,就會解除阻塞。

2、非阻塞IO:忙等待 fcntl(fd,F_SETFL,flag|O_NONBLOCK);內核中沒有數據時,recv會返回-1,不會阻塞

3、IO複用(select和poll):一旦有一個文件描述符檢測到有文件過來,select就返回,(阻塞提前到selete處)recv就可以直接從內核空間得到數據。

 

4、信號驅動IO:以信號方式通知應用進程,有數據到來(信號是異步處理一種方式)應用進程調用recv將數據從內核空間拉到用戶空間--效率沒有異步IO高

5、異步IO :aio_read沒有數據到來,這個函數也會立刻返回,有數據到來,內核則將數據拷貝到應用層緩衝區,拷貝完成通過信號通知用戶。

TCP的11種狀態:

 

還有一種叫CLOSING狀態,產生原因是雙方同時關閉,客戶端會處於FIN_WAIT_1狀態,服務器端也處於FIN_WAIT_1狀態,雙方均在等待的狀態就是CLOSING狀態,收到對方ACK後,就會處於TIME_WAIT狀態,

 

二、select也會阻塞,相比於阻塞IOselect優點在哪裏?

當我們kill掉服務端的連接進程後,發現服務端處於FIN_WAIT2,不能立刻結束。

 原因是客戶端程序阻塞在了標準輸入位置,沒有機會調用close,因此導致服務端不能立刻結束。本質就是因爲從鍵盤接收數據和從網絡接收數據沒有辦法同時處理。這時用selete來進行管理,管理標準輸入IO和套接口IO。

用select便可以管理多個IO,一旦其中一個IO或者多個IO檢測到我們所感興趣的時間,select函數返回,返回值是檢測到的事件個數,並且返回那些IO發生了事件。這樣用戶可以遍歷這些事件去處理這些事件。

其次,服務端使用多個進程處理多個客戶端連接,能不能使用一個進程來處理?

三、select使用

參數含義:

nfds:讀、寫、異常集合中最大文件描述符值+1

readfds 可讀的集合,輸入輸出參數

writefds 可寫的集合,輸入輸出參數

exceptfds 異常集合,輸入輸出參數

timeout 超時時間結構體  填NULL,只有檢測到某個事件才返回,填寫超時時間,沒有事件到來,超時時間到後就返回事件個數0,失敗返回-1,輸入輸出參數。

FD_CLR:將文件描述符從集合中移除

FD_ISSET:判斷fd是否在集合中

FD_SET:將fd添加到集合中

FD_ZERO:清空集合

四、代碼

客戶端:

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>

#include <signal.h>//信號
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m)  \
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while (0)


//ssize_t 有符號整數
//size_t 無符號整數	
ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count;//剩餘字節數
	ssize_t nread;//已接收字節數
	char *bufp = (char*) buf;

	while(nleft>0)
	{
		if((nread = read(fd,bufp,nleft))<0)
		{
			if(errno == EINTR)//被信號中斷
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//對等方關閉
		{
			count = count - nleft;//已經讀取的字節數
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft = count;//剩餘字節數
	ssize_t nwritten;//已接收字節數
	char *bufp = (char*) buf;

	while(nleft>0)
	{
		if((nwritten = write(fd,bufp,nleft))<0)
		{
			if(errno == EINTR)//被信號中斷
			{
				continue;
			}
			else
				return -1;
		}
		else if(nwritten == 0)//對等方關閉
		{
			continue;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}

//從套接口接收數據,但並不把數據從緩衝區清除
ssize_t recv_peek(int sockfd,void *buf,int len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);
		if((ret == -1) && (errno == EINTR))
			continue;

		printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
		return ret;
	}
}


//偷窺方案:
ssize_t readline(int sockfd,void *buf,size_t maxlen)//一行最大的字節數
{//只要遇到/n就返回
	int ret;
	int nread;
	char *bufp = (char *)buf;
	int nleft = maxlen;
	while(1)
	{
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret<0)
		{
			return ret;
		}
		else if(ret == 0)//表示對方關閉了套接口
		{
			return ret;
		}

		nread = ret;

		int i;
		for(i =0; i<nread;i++)
		{
			if(bufp[i] == '\n')//如果找到了結束符就將數據讀取出來
			{
				ret = readn(sockfd,bufp,i+1);//下標i,總共有i+1個字符
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//如果沒有找到結束符,就讀出來先緩存起來
		if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
			exit(EXIT_FAILURE);

		nleft -= nread;//剩餘的字節數

		ret = readn(sockfd,bufp,nread);
		if(ret != nread)//偷窺到的數據是可以全部讀取出來的
		{
			exit(EXIT_FAILURE);
		}
		bufp += nread;
	}
	return -1;
}


void echo_cli(int sock)
{
/*	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};

	while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
	{
		writen(sock,sendbuf,strlen(sendbuf));//發送

		int ret = readline(sock,recvbuf,sizeof(recvbuf));
		if(ret == -1)
		{
			ERR_EXIT("readline");
		}
		else if(ret == 0)
		{
			printf("client_close\n");
			break;
		} 	
		
		fputs(recvbuf,stdout);//打印
							
		memset(recvbuf,0,sizeof(recvbuf));
		memset(sendbuf,0,sizeof(sendbuf));

	}
	close(sock);
	*/
//用select統一管理標準輸入IO與套接口IO
	fd_set rset;
	FD_ZERO (&rset);

	int nready;
	int fd_stdin = fileno(stdin);
	//標準輸入的文件描述符,通過fileno獲取,
	//不能直接用STD_FILENO這個宏,
	//因爲不能確保標準輸入不被重定向
	//還有一個文件描述符爲sock
	int maxfd = fd_stdin?fd_stdin>sock:sock;
	
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};	

	while(1)
	{
		FD_SET(fd_stdin,&rset);
		FD_SET(sock,&rset);
		nready = select(maxfd+1,&rset,NULL,NULL,NULL);
		if(nready == -1)
			ERR_EXIT("select");
		if(nready == 0)
			continue;
		if(FD_ISSET(sock,&rset))
		{
			int ret = readline(sock,recvbuf,sizeof(recvbuf));
			if(ret == -1)
			{
				ERR_EXIT("readline");
			}
			else if(ret == 0)
			{
				printf("srv_close\n");
				break;
			} 	
			
			fputs(recvbuf,stdout);//打印							
			memset(recvbuf,0,sizeof(recvbuf));
		}
		if(FD_ISSET(fd_stdin,&rset))
		{
			if(fgets(sendbuf,sizeof(sendbuf),stdin)==NULL)
				break;
			writen(sock,sendbuf,strlen(sendbuf));//發送
			memset(sendbuf,0,sizeof(sendbuf));
		}
	}
	close(sock);//顯示關閉套接口
	
}
void handle_sigpipe(int sig)
{
	printf("recv a sig = %d\n",sig);
}

int main(void)
{
/*
	signal(SIGPIPE,handle_sigpipe);
*/
	signal(SIGPIPE,SIG_IGN);//當服務端down掉,客戶端發送數據後tcp協議棧會產生該信號
	int sock;//被動套接字
	if(	(sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//創建套接字小於0表示失敗
		ERR_EXIT("socket");

	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指定服務器端地址

	if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
		ERR_EXIT("connect");

	echo_cli(sock);

	return 0;
}

服務端:

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>

#include <signal.h>//信號
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m)  \
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while (0)
	

//ssize_t 有符號整數
//size_t 無符號整數
ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count;//剩餘字節數
	ssize_t nread;//已接收字節數
	char *bufp = (char*) buf;

	while(nleft>0)
	{
		if((nread = read(fd,bufp,nleft))<0)
		{
			if(errno == EINTR)//被信號中斷
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//對等方關閉
		{
			count = count - nleft;//已經讀取的字節數
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}
 
ssize_t writen(int fd,const void *buf,size_t count)
{

	size_t nleft = count;//剩餘字節數
	ssize_t nwritten;//已接收字節數
	char *bufp = (char*) buf;

	while(nleft>0)
	{
		if((nwritten = write(fd,bufp,nleft))<0)
		{
			if(errno == EINTR)//被信號中斷
			{
				continue;
			}
			else
			{
				return -1;
			}
		}
		else if(nwritten == 0)//對等方關閉
		{
			continue;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}


//從套接口接收數據,但並不把數據從緩衝區清除
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);
		if((ret == -1) && (errno == EINTR))
			continue;
		printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
		return ret;
	}
}


//偷窺方案:
ssize_t readline(int sockfd,void *buf,size_t maxline)//一行最大的字節數
{//只要遇到/n就返回
	int ret;
	int nread;
	char *bufp = (char *)buf;
	int nleft = maxline;
	while(1)
	{
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret<0)
			return ret;
		else if(ret == 0)//表示對方關閉了套接口
			return ret;

		nread = ret;

		int i;
		for(i =0; i<nread;i++)
		{
			if(bufp[i] == '\n')//如果找到了結束符就將數據讀取出來
			{
				ret = readn(sockfd,bufp,i+1);//下標i,總共有i+1個字符
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//如果沒有找到結束符,就讀出來先緩存起來
		if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
			exit(EXIT_FAILURE);

		nleft -= nread;//剩餘的字節數

		ret = readn(sockfd,bufp,nread);
		if(ret != nread)//偷窺到的數據是可以全部讀取出來的
		{
			exit(EXIT_FAILURE);
		}
		bufp += nread;
	}
	return -1;
}

void echo_srv(int conn)
{
	char recvbuf[1024];
	while(1)
	{
		memset(recvbuf,0,sizeof(recvbuf));
		int ret = readline(conn,recvbuf,1024);//按行接收

		if(ret == -1)
		{
			ERR_EXIT("readline");
		}
		if(ret == 0)
		{
			printf("client_close\n");
			break;
		} 	
	
		fputs(recvbuf,stdout);//打印
		writen(conn,recvbuf,strlen(recvbuf));//回射-這裏!!
		
	}
}

int main(void)
{
	signal(SIGCHLD,SIG_IGN);//處理殭屍進程

	int listenfd;//被動套接字
	if(	(listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//創建套接字小於0表示失敗
/*	if(	(listenfd = socket(PF_INET,SOCK_STREAM,0))<0);*///讓內核自己選定協議
		ERR_EXIT("socket");

	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本機的任意地址
	/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
	/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/

	int on = 1;//開啓地址重複利用
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		ERR_EXIT("setsockopt");

	if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
		ERR_EXIT("bind");

	if(listen(listenfd,SOMAXCONN)<0)//監聽後變爲被動套接字
		ERR_EXIT("listen");
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen;
	int conn;//主動套接字

/*
	//父子進程可以共享文件描述符
	pid_t pid;
	while(1)
	{
		if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
			ERR_EXIT("accept");

		printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		

		//也可以使用select實現併發服務器
		pid = fork();//創建進程實現併發處理
		if(pid == -1)
			ERR_EXIT("frok");
		if(pid == 0)
		{//子進程不需要處理監聽套接字
			close(listenfd);
			echo_srv(conn);
			exit(EXIT_SUCCESS);//如果通信結束(客戶端關閉)直接結束進程,否則子進程也會去accept
		}
		else
		{//父進程不需要處理連接套接字
			close(conn);
		}
	}
*/

//select單進程處理併發
	int i;
	//select中fd_set集合的限制FD_SETSIZE
	int client[FD_SETSIZE];//緩存select返回的有時間到來的套接口
	for(i = 0;i<FD_SETSIZE;i++)
	{
		client[i] = -1;//空閒
	}
	int nready;
	/*文件描述符
	0 標準輸入
	1 標準輸出
	2 標準錯誤*/

	int maxfd = listenfd;//第3個套接字就是監聽套接字,也是最大的套接字

	fd_set rset;//讀的集合
	fd_set allset;//所有集合
	
	FD_ZERO(&rset);
	FD_ZERO(&allset);
	//將監聽套接口放到allset中
	FD_SET(listenfd,&allset);

	while(1)
	{
		rset = allset;	
		nready = select(maxfd+1,&rset,NULL,NULL,NULL);//寫、異常、超時均不關心//超時時間爲NULL不可能返回零
		if(nready == -1)
		{
			if(errno == EINTR)
				continue;
			ERR_EXIT("select");
		}
		if(nready == 0)
			continue;
		if(FD_ISSET(listenfd,&rset))//是否是監聽套接口產生事件
		{//起初集合中只有一個監聽套接口,不用循環
			peerlen = sizeof(peeraddr);//一定要有初始值
			//這裏有個問題,就是雖然可以接收多個conn,但是多個conn會覆蓋
			conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);
			if(conn == -1)
				ERR_EXIT("accept");
			for(i = 0;i<FD_SETSIZE;i++)
			{
				if(client[i]<0)//找到空閒位置將conn存儲進去
				{
					client[i] = conn;
					break;
				}
			}
			if(i == FD_SETSIZE)
			{
				fprintf(stderr,"too many clients\n");
				exit(EXIT_FAILURE);
			}
			printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
			//獲得了套接口conn,下次循環我們也要關心conn的可讀事件
			FD_SET(conn,&allset);//不斷運行,添加到數組中的套接口就會越來越多
			if(conn>maxfd)
				maxfd = conn;
			if(--nready <= 0)
				continue;
		}
		for(i = 0;i<FD_SETSIZE;i++)
		{
			conn = client[i];//已連接套接口
			if(conn == -1)
				continue;
			if(FD_ISSET(conn,&rset))//產生可讀事件
			{
				char recvbuf[1024] = {0};
				int ret = readline(conn,recvbuf,1024);//按行接收

				if(ret == -1)
				{
					ERR_EXIT("readline");
				}
				if(ret == 0)
				{
					printf("client_close\n");
					//如果對方關閉,則從集合中清除,不再關心它的可讀事件
					FD_CLR(conn,&allset);
					client[i] = -1;
				} 	
			
				fputs(recvbuf,stdout);//打印
				writen(conn,recvbuf,strlen(recvbuf));//回射-這裏!!

				if(--nready <= 0)//所有事件都處理完畢退出。
					break;
			}
		}


	}
	return 0;
}


我們改造客戶端的echo_cli函數使得客戶端可以同時處理多個IO事件,改造服務端,使得服務端可以以一個進程處理多個客戶端連接,最終

 

發佈了117 篇原創文章 · 獲贊 81 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章