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事件,改造服务端,使得服务端可以以一个进程处理多个客户端连接,最终

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值