首页 > 代码库 > UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本

UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie


这是一个简单的回射服务器程序。它将客户发送的数据读入缓冲区并回射其中内容



下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本、select 版本、poll 版本、多线程版本


fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求
/**
 * TCP/IPv4 协议相关
 * **/
#include	"unp.h"


int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;


    //1.创建套接字,捆绑服务器的众所周知端口
	listenfd = Socket(AF_INET, SOCK_STREAM, 0);


	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);


	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));


	Listen(listenfd, LISTENQ);


    //2.等待完成客户连接
	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);


        //3.并发服务器
		if ( (childpid = Fork()) == 0) {	/* 子进程 */
			Close(listenfd);	/* 关闭监听套接字*/
			str_echo(connfd);	/* 处理客户请求*/
			exit(0);
		}
		Close(connfd);			/* 父进程关闭连接套接字 */
	}
}


#include	"unp.h"


void
str_echo(int sockfd)
{
	ssize_t		n;
	char		buf[MAXLINE];


again:
	// 读入缓冲区并回射其中内容
	while ( (n = read(sockfd, buf, MAXLINE)) > 0)
		Writen(sockfd, buf, n);


	if (n < 0 && errno == EINTR)
		goto again;
	else if (n < 0)
		err_sys("str_echo: read error");
}
 


问题:僵死子进程
改善:通过捕获 SIGCHLD 信号并在信号的调用  waitpid 函数来回收子进程的资源。
之所以用 waitpid 而不用 wait 是因为 Unix 信号是不排队的,而 waitpid 因为可以设置不阻塞,所以可以在循环中
被调用,这样可以处理同时到达的多个 SIGCHLD 信号。


/**
* TCP/IPv4 协议相关 收拾终止了的子进程
**/
#include	"unp.h"


int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;
	void				sig_chld(int);


	//1.创建套接字,绑定众所周知的端口
	listenfd = Socket(AF_INET, SOCK_STREAM, 0);


	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);


	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));


	Listen(listenfd, LISTENQ);


	//2.当fork 子进程时,必须捕获 SIGCHLD 信号, sig_chld 为信号处理函数
	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */


	//3.等待客户连接
	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		//4.当捕获信号时,必须处理被中断的系统调用
		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("accept error");
		}


		//5.并发服务器
		if ( (childpid = Fork()) == 0) {	/* child process */
			Close(listenfd);	/* close listening socket */
			str_echo(connfd);	/* process the request */
			exit(0);
		}
		Close(connfd);			/* parent closes connected socket */
	}
}
#include	"unp.h"


void
sig_chld(int signo)
{
	pid_t	pid;
	int		stat;


	while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
		printf("child %d terminated\n", pid);
	return;
}



select 版本:使用 select 来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程

/**
* TCP 使用 select 
**/
#include	"unp.h"


int
main(int argc, char **argv)
{
	int					i, maxi, maxfd, listenfd, connfd, sockfd;
	int					nready, client[FD_SETSIZE];
	ssize_t				n;
	fd_set				rset, allset;
	char				buf[MAXLINE];
	socklen_t			clilen;
	struct sockaddr_in	cliaddr, servaddr;


	//1.创建套接字,绑定众所周知的端口
	listenfd = Socket(AF_INET, SOCK_STREAM, 0);


	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);


	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));


	Listen(listenfd, LISTENQ);


	//2.初始化
	maxfd = listenfd;			/* initialize */
	maxi = -1;					/* index into client[] array */
	for (i = 0; i < FD_SETSIZE; i++)
		client[i] = -1;			/* -1 indicates available entry */
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);
/* end fig01 */


/* include fig02 */
	for ( ; ; ) {
		//3.阻塞于 select
		//select 等待某个事件发生:或是新客户连接的建立,或是数据、FIN或 RST的到达
		rset = allset;		/* structure assignment */
		nready = Select(maxfd+1, &rset, NULL, NULL, NULL);


		//4.accept 新的连接
		if (FD_ISSET(listenfd, &rset)) {	/* new client connection */
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef	NOTDEF
			printf("new client: %s, port %d\n",
					Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
					ntohs(cliaddr.sin_port));
#endif


			//用 client 数组中第一个未用项记录这个已连接描述符
			for (i = 0; i < FD_SETSIZE; i++)
				if (client[i] < 0) {
					client[i] = connfd;	/* save descriptor */
					break;
				}
			if (i == FD_SETSIZE)
				err_quit("too many clients");


			//在 allset 描述符中打开已连接套接字 connfd 的对应位
			FD_SET(connfd, &allset);	/* add new descriptor to set */
			
			if (connfd > maxfd)
				maxfd = connfd;			/* for select */
			if (i > maxi)
				maxi = i;				/* max index in client[] array */
			//nready 是 select 函数的返回值,表示跨所有描述符集的已就绪的总位数
			//就绪描述符数目减 1,若其值变为 0,就可以避免进入下一个 for 循环
			if (--nready <= 0)
				continue;				/* no more readable descriptors */
		}


		//5.检查现有连接
		//对于每个现在的客户连接,测试其描述符是否在 select 返回的描述符集中 --> ? select 有返回描述符集吗 ?
		//如果是,就从客户读入一行文本并回射给它。
		//如果关闭了连接,那么 read 将返回 0,要相应地更新数据结构
		for (i = 0; i <= maxi; i++) {	/* check all clients for data */
			if ( (sockfd = client[i]) < 0)
				continue;
			if (FD_ISSET(sockfd, &rset)) {
				if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
						/*4connection closed by client */
					Close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				} else
					Writen(sockfd, buf, n);


				if (--nready <= 0)
					break;				/* no more readable descriptors */
			}
		}
	}
}
/* end fig02 */


poll 版本:在 select 版本中,必须分配一个 client 数组以及一个名为 rset 的描述符集。改用 poll 后,我们只需分配 一个 pollfd 结构的数组来
维护客户信息,而不必分配另一个数组。


/**
 * TCP/IPv4 协议相关,使用poll,单个进程处理所有客户
 * **/
/* include fig01 */
#include	"unp.h"
#include	<limits.h>		/* for OPEN_MAX */


int
main(int argc, char **argv)
{
	int					i, maxi, listenfd, connfd, sockfd;
	int					nready;
	ssize_t				n;
	char				buf[MAXLINE];
	socklen_t			clilen;
    //1.分配 pollfd 结构数组
	struct pollfd		client[OPEN_MAX];
	struct sockaddr_in	cliaddr, servaddr;


	listenfd = Socket(AF_INET, SOCK_STREAM, 0);


	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);


	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));


	Listen(listenfd, LISTENQ);


    //2.初始化
    //把 client 数组的第一项用于监听套接字,并设置 events为 POLLRDNORM
    //把其余各项的成员转为 -1
	client[0].fd = listenfd;
	client[0].events = POLLRDNORM;
	for (i = 1; i < OPEN_MAX; i++)
		client[i].fd = -1;		/* -1 indicates available entry */
	maxi = 0;					/* max index into client[] array */
/* end fig01 */


/* include fig02 */
	for ( ; ; ) {
        //3.调用 poll,检查新的连接
		nready = Poll(client, maxi+1, INFTIM);


        //检查 client[0],即监听套接字上是否有新客户连接
		if (client[0].revents & POLLRDNORM) {	/* new client connection */
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef	NOTDEF
			printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif


            //保存已连接套接字
			for (i = 1; i < OPEN_MAX; i++)
				if (client[i].fd < 0) {
					client[i].fd = connfd;	/* save descriptor */
					break;
				}
			if (i == OPEN_MAX)
				err_quit("too many clients");


			client[i].events = POLLRDNORM;
			if (i > maxi)
				maxi = i;				/* max index in client[] array */


			if (--nready <= 0)
				continue;				/* no more readable descriptors */
		}


        //4.检查某个现有连接上的数据
        // i 从 1,开始,因为 client[0]是监听套接字
		for (i = 1; i <= maxi; i++) {	/* check all clients for data */
			if ( (sockfd = client[i].fd) < 0)
				continue;
			if (client[i].revents & (POLLRDNORM | POLLERR)) {
                //读取数据并回射
				if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
					if (errno == ECONNRESET) {
							/*4connection reset by client */
#ifdef	NOTDEF
						printf("client[%d] aborted connection\n", i);
#endif
						Close(sockfd);
						client[i].fd = -1;
					} else
						err_sys("read error");
                //终止连接
				} else if (n == 0) {
						/*4connection closed by client */
#ifdef	NOTDEF
					printf("client[%d] closed connection\n", i);
#endif
					Close(sockfd);
					client[i].fd = -1;
				} else
					Writen(sockfd, buf, n);


				if (--nready <= 0)
					break;				/* no more readable descriptors */
			}
		}
	}
}
/* end fig02 */


多线程版本:这个版本的代码为每个客户使用一个线程,而不是子进程,同时它使用了 tcp_listen 函数来使得该程序与协议无关。


/**
 * TCP 每个用户一个线程
 * **/
#include	"unpthread.h"


static void	*doit(void *);		/* each thread executes this function */


int
main(int argc, char **argv)
{
	int				listenfd, connfd;
	pthread_t		tid;
	socklen_t		addrlen, len;
	struct sockaddr	*cliaddr;


    //1.创建监听套接字
	if (argc == 2)
		listenfd = Tcp_listen(NULL, argv[1], &addrlen);
	else if (argc == 3)
		listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
	else
		err_quit("usage: tcpserv01 [ <host> ] <service or port>");


	cliaddr = Malloc(addrlen);


    //2.创建线程
	for ( ; ; ) {
		len = addrlen;
		connfd = Accept(listenfd, cliaddr, &len);
        //accept 返回之后,改为调用 pthread_create 取代调用 fork
		Pthread_create(&tid, NULL, &doit, (void *) connfd);
	}
}


//3.线程函数
static void *
doit(void *arg)
{
    //pthread_detach 使自身脱离主线程,这样主线程不用等待它
	Pthread_detach(pthread_self());
	str_echo((int) arg);	/* same function as before */
    //关闭已连接套接字
	Close((int) arg);		/* done with connected socket */
	return(NULL);
}


问题:上面的代码把整数变量 connfd 类型强制转换成 void 指针,这并不具备兼容性。
改善:可以把一个整数指针类型强制转换为 void *,然后把这个 void * 指针类型强制转换回原来的整数指针。
但如果主线程中只有一个整数变量 connfd 的话,每次调用 accept 该变量都会被覆写为另一个新值。
因此还应该专门为每一个线程 malloc 一个整数变量的空间,用来存放该线程的处理的已连接套接字描述符,
同时还要在线程中 free 掉该空间。


/**
 * TCP 每个客户一个线程,可移植的参数传递
 * **/
#include	"unpthread.h"


static void	*doit(void *);		/* each thread executes this function */


int
main(int argc, char **argv)
{
	int				listenfd, *iptr;
	thread_t		tid;
	socklen_t		addrlen, len;
	struct sockaddr	*cliaddr;


    //1.创建监听套接字
	if (argc == 2)
		listenfd = Tcp_listen(NULL, argv[1], &addrlen);
	else if (argc == 3)
		listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
	else
		err_quit("usage: tcpserv01 [ <host> ] <service or port>");


	cliaddr = Malloc(addrlen);


    //2.创建线程
	for ( ; ; ) {
		len = addrlen;
		iptr = Malloc(sizeof(int));
		*iptr = Accept(listenfd, cliaddr, &len);
        Pthread_create(&tid, NULL, &doit, iptr);
	}
}


//3.线程函数
static void *
doit(void *arg)
{
	int		connfd;


	connfd = *((int *) arg);
	free(arg);


	Pthread_detach(pthread_self());
	str_echo(connfd);		/* same function as before */
	Close(connfd);			/* done with connected socket */
	return(NULL);
}


UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本