用select改进回射客户-服务器模型

这一节主要来说一下如何用select函数来改进我们前面的客户端-服务器模型。

前面我们在处理多客户端模型时,每当连接一个客户端时,服务器端就需要开辟一个新的进程来处理新的客户端,这样就会耗费很大的内存资源。

而select函数允许进程指示内核等待多个事件中的任何一个发生,并只有在有一个或多个事件事件发生或经历一段指定的时间后才唤醒它。或者说select具有管理多个I/O的能力,对于多个套接口,一旦某个套接口发生了我们所感兴趣的事件,select函数返回,返回值为监测到的事件个数。而且由于select函数的参数是“值—结果”型的,因此我们也知道哪些套接口发生了事件,然后遍历这些套接口并处理相关事件。

关于select参数的具体用法这里就不做过多说明,大家可以参考网上资料或者《UNIX网络编程:卷一》上的相关内容。

下面给出改进后的回射客户-服务器模型的代码。

服务器端:echosrv

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

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


ssize_t readn(int fd, void* buf, size_t count)
{
	//由于不能保证一次能够读取count个字节
	//因此我们需要循环进行读取
	//直到读取的字节数为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;
			return -1;
		}
		if(nread == 0)
			//表示对等方关闭,这里直接返回
			return count-nleft;
		nleft -= nread;//每次读取后剩余的字节数
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	//我们每次希望写入的字节数为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;
			return -1;
		}
		if(nwritten == 0)
			//什么都没发生
			continue;
		nleft -= nwritten;//每次写后剩余要写的字节数
		bufp += 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;
		return ret;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	//读取过程不一定要读取maxline个字节
	//只要遇到\n就可以返回
	int ret;
	int nread;
	char* bufp = 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')
			{
				//我们的recv_peek只是偷窥一下数据
				//并没有一走数据
				//所以这里用readn从缓冲区中移除已偷窥的数据
				ret = readn(sockfd, bufp, i+1);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//没有遇到\n
		if(nread > nleft)
			exit(EXIT_FAILURE);
		//把读到的数据nread个字节从缓冲区中移走
		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("read failure");
		if(ret == 0)
		{
			printf("client close\n");
			break;
		}
		fputs(recvbuf, stdout);
		writen(conn, recvbuf, strlen(recvbuf));
	}
}

void handle_sigchld(int sig)
{
/*	wait(NULL);*/
	while(waitpid(-1, NULL, WNOHANG) > 0)
		;
}

int main(void)
{
	//避免僵尸进程
/*	signal(SIGCHLD, SIG_IGN);//忽略
 */
	signal(SIGCHLD, handle_sigchld);

	//创建一个套接字
	int listenfd;
	if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		//初始化地址
	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_failure");

	//接下来进行绑定,将该套接字与一个本地地址进行绑定
	//需要将IPv4地址结构强制转换为通用地址结构
	if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
		ERR_EXIT("bind_failure");//绑定失败
	
	//接下来是监听,将socket从close状态转为监听状态才能够接受连接
	if(listen(listenfd, SOMAXCONN) < 0)
		ERR_EXIT("listen_failure"); 

	//定义一个对方的地址
	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_failure");
		//输出客户端的地址和端口		
		printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
		//一旦获得一个连接,就创建一个进程
		//处理多个客户端,达到并发的目的
		pid = fork();
		if(pid == -1)
			ERR_EXIT("fork_failure");
		if(pid == 0)
		{
			//让子进程处理已有的通信过程
			//不再需要监听套接口
			close(listenfd);
			echo_srv(conn);
			//一旦do_service函数返回,那么该进程就没有存在的价值了
			exit(EXIT_SUCCESS);//此时,为客户端开辟的进程也销毁了
		}
		else
			//父进程进行accept
			//不再需要连接套接口了,即conn(父子进程共享文件描述符)
			close(conn);
			
	}

	//实现一个回射客户/服务器模型
	//即客户端从标准输入获取数据,发送给服务器端,服务器端再回射过去
*/

	//改为用select实现
	int client[FD_SETSIZE];//select最多能处理的事件个数
	int i;
	for(i = 0; i < FD_SETSIZE; ++i)
		client[i] = -1;//初始化,-1表示空闲状态
	int maxi = 0; //最大不空闲位置

	int nready;
	int maxfd = listenfd;//3
	fd_set rset;//定义一个读集合
	fd_set allset;
	FD_ZERO(&rset);
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);//先把监听套接口加进去
	while(1)
	{
		rset = allset;
		nready = select(maxfd+1, &rset, 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 = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
			if(conn == -1)
				ERR_EXIT("accept error");
			for(i = 0; i < FD_SETSIZE; ++i)
			{
				if(client[i] < 0)
				{
					//找一个空闲位置把该套接口放进去
					client[i] = conn;
					if(maxi < i)
						maxi = i;
					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)); 

			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 error");
				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;
}
客户端:echocli.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>

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

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;
			return -1;
		}
		if(nread == 0)//对等方关闭
			return count-nleft;
		nleft -= nread;
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, 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;
			return -1;
		}
		if(nwritten == 0)
			continue;
		nleft -= nwritten;
		bufp += 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;
		//偷窥到数据就直接返回
		return ret;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	char* bufp = buf;
	int nleft = maxline;
	int nread;
	int ret;
	while(1)
	{
		ret = recv_peek(sockfd, bufp, nleft);
		if(ret < 0)
			return ret;
		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);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//没有遇到\n
		if(nread > 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, 1024);
		if(ret == -1)
			ERR_EXIT("read error");
		else if(ret == 0)
		{
			printf("peer close\n");
			break;
		}
		//显示出来
		fputs(recvbuf, stdout);
		//这里需要清空缓冲区
		memset(sendbuf, 0, sizeof(sendbuf));
		memset(recvbuf, 0, sizeof(recvbuf));			
	}
	close(sock);
*/
	//该为select用法
	fd_set rset;
	FD_ZERO(&rset);

	//检测标准输入是否产生了可读事件
	int nready;
	int maxfd;
	int fd_stdin = fileno(stdin);//标准输入
	char recvbuf[1024] = {0};
	char sendbuf[1024] = {0};

	//有两个文件描述符,fd_stdin和sock
	if(fd_stdin > sock)
		maxfd = fd_stdin;
	else
		maxfd = sock;
	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 error");
		if(nready == 0)
			continue;
		//如果检测到了事件,那么rset就会发生改变
		//里面会包含哪些套接口发生了事件
		if(FD_ISSET(sock, &rset))
		{
			//套接口产生了可读
			//按行读取
			int ret = readline(sock, recvbuf, sizeof(recvbuf));
			if(ret == -1)
				ERR_EXIT("readline error");
			else if(ret == 0)
			{
				printf("server close\n");
				break;
			}
			//显示出来
			fputs(recvbuf, stdout);
			memset(recvbuf, 0, sizeof(recvbuf));
		}
		if(FD_ISSET(fd_stdin, &rset))
		{
			//标准输入产生事件,输入缓冲区有内容,用fgets去清空
			if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
				break;
			write(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);
	int sock;//创建一个套接字
	if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		
	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");
	
	//客户端不需要绑定(bind),也不需要监听(listen)
	//直接连接过去就可以
	if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
		ERR_EXIT("connect_failure");

	//连接成功,查看本地的端口和地址
	struct sockaddr_in localaddr;
	socklen_t addrlen = sizeof(localaddr);
	if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
		ERR_EXIT("getsockname error");
	
	printf("IP=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));

	echo_cli(sock);
	
	return 0;
}

说明:FD_SETSIZE是系统指定的select所能监听的事件的最大值,一般为1024。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值