UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式

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

下面我会介绍同一个使用 TCP 协议的客户端程序的几个不同版本,分别是停等版本、select 加阻塞式 I/O 版本、

非阻塞式 I/O 版本、fork 版本、线程化版本。它们都由同一个 main 函数调用来实现同一个功能,即回射程序客户端。

它从标准输入读入一行文本,写到服务器上,读取服务器对该行的回射,并把回射行写到标准输出上。

其中,非阻塞式 I/O 版本是所有版本中执行速度最快的,但它的代码比较复杂。

创建一个新线程通常比使用 fork 派生一个新进程快得多,

下面代码中的线程化版本执行速度略快于 fork版本,稍慢于非阻塞式 I/O。一般编程推荐使用线程版本。

下面是《Unix 网络编程:卷1》中给出的各个版本的性能测试结果:

354.0秒,停等版本

12.3秒, select加阻塞式I/O版本

6.9秒,  非阻塞式I/O版本

8.7秒, fork 版本

8.5秒,  线程化版本

下面是 main 函数的代码

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: tcpcli <IPaddress>");

	//1.创建 TCP 套接字
	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	//2.指定服务器的 IP 地址和端口
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	//3.建立与服务器的连接
	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

	//4.str_cli 函数完成剩余部分的客户处理工作
	str_cli(stdin, sockfd);		

	exit(0);
}

最初代码(停等版本):

#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	char	sendline[MAXLINE], recvline[MAXLINE];

	//1.从 fp 读入一行,存放到 sendline
	while (Fgets(sendline, MAXLINE, fp) != NULL) {
		//2.将 sendline 的内容写到 sockfd 连接的服务器
		Writen(sockfd, sendline, strlen(sendline));

		//3.从服务器读入回射行,存放到 recvline
		if (Readline(sockfd, recvline, MAXLINE) == 0)
			err_quit("str_cli: server terminated prematurely");

		//4.将 recvline 的内容写到标准输出
		Fputs(recvline, stdout);
	}
}

问题1:当套接字上发生某些事件时,客户可能阻塞于 fgets 调用

改善1:使用 select 重写 str_cli 函数,阻塞于 select 调用,或是等待标准输入可读,

或是等待套接字可读。这样服务器一终止,客户就能马上得到通知。

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

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1; //  maxfdpl 参数指定待测定的描述符个数
	fd_set		rset;   //可读描述符集合
	char		sendline[MAXLINE], recvline[MAXLINE];

	//1.调用 select
	FD_ZERO(&rset); //初始化 rset
	for ( ; ; ) {
		FD_SET(fileno(fp), &rset); // 打开标准I/O文件指针 fp 对应的位
		FD_SET(sockfd, &rset); 	  // 打开套接字 sockfd 对应的位
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		//调用 select 阻塞到某个描述符就绪为止
		Select(maxfdp1, &rset, NULL, NULL, NULL);

		//2.处理可读套接字
		if (FD_ISSET(sockfd, &rset)) {
			//使用 readline 读入回射文本,再用 fputs 输出到 stdout
			if (Readline(sockfd, recvline, MAXLINE) == 0)
				err_quit("str_cli: server terminated prematurely");
			Fputs(recvline, stdout);
		}

		//3.处理可读输入
		if (FD_ISSET(fileno(fp), &rset)) {
			//使用 fgets 读入一行文本,再用 writen 把它写到套接字中
			if (Fgets(sendline, MAXLINE, fp) == NULL)
				return;		/* all done */
			Writen(sockfd, sendline, strlen(sendline));
		}
	}
}

改善1的问题:批量方式运行用 select 编写的回射客户程序,发现即使已经遇到了用户输入结尾,

仍可能有数据处于运往或来自服务器的管道中。

改善2(select加阻塞式I/O版本):使用 shutdown 函数利用上 TCP 的半关闭特性

下面代码使用了 select 和 shutdown,前者只要服务器关闭它那一端的连接就会通知客户,

后者允许客户正确地处理批量输入。这个版本还废弃了以文本行为为中心的代码,发布针对缓冲区操作。

/**
* TCP 使用 select 并操纵缓冲区
**/
#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	//stdineof 表示标准输入是否结束
	int			maxfdp1, stdineof;
	fd_set		rset;
	char		buf[MAXLINE];
	int		n;

	//1.调用 select
	stdineof = 0;
	FD_ZERO(&rset);
	for ( ; ; ) {
		if (stdineof == 0)
			FD_SET(fileno(fp), &rset);
		FD_SET(sockfd, &rset);
		maxfdp1 = max(fileno(fp), sockfd) + 1;
		Select(maxfdp1, &rset, NULL, NULL, NULL);

		//2.处理可读套接字
		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */
			if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
				if (stdineof == 1) //如果在套接字上读到 EOF,并且已在标准输入上遇到 EOF,那就是正常的终止
					return;
				else //否则,服务器进程过早终止了
					err_quit("str_cli: server terminated prematurely");
			}

			Write(fileno(stdout), buf, n);
		}

		//3.处理可读输入
		if (FD_ISSET(fileno(fp), &rset)) {
			//如果在标准输入上碰到 EOF 时,把 stdineof 置为 1
			//并调用 shutdown 函数向服务器发送 FIN,关闭客户端向服务器的写操作
			if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
				stdineof = 1;
				Shutdown(sockfd, SHUT_WR);
				FD_CLR(fileno(fp), &rset);
				continue;
			}

			Writen(sockfd, buf, n);
		}
	}
}

改善2问题:阻塞式I/O

例如,如果在标准输入有一行文本可读,我们就调用 read 读入它,再调用  writen 把它发送给服务器。

然而如果套接字发送缓冲区已满, writen 调用将会阻塞。在进程阻塞于 writen 调用期间,可能有来自套接字接收缓冲区的数据可供读取。

类似的,如果从套接字有一行输入文本可读,那么一旦标准输出比网络还要慢,进程照样可能阻塞于后续的 write 调用。

改善3(非阻塞式I/O版本):使用非阻塞式 I/O

/* include nonb1 */
#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	int			maxfdp1, val, stdineof;
	ssize_t		n, nwritten;
	fd_set		rset, wset;
	char		to[MAXLINE], fr[MAXLINE];
	//to 容纳从标准输入到服务器去的数据
	//from 容纳自服务器到标准输出来的数据
	char		*toiptr, *tooptr, *friptr, *froptr;

	//1.把描述符设置为非阻塞
	val = Fcntl(sockfd, F_GETFL, 0);
	Fcntl(sockfd, F_SETFL, val | O_NONBLOCK); //套接字描述符

	val = Fcntl(STDIN_FILENO, F_GETFL, 0);
	Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK); //标准输入

	val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
	Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK); //标准输出

	//2.初始化缓冲区指针
	toiptr = tooptr = to;
	friptr = froptr = fr;
	stdineof = 0;
	maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;

	//3.主循环:调用 select
	//一个 select 调用后对所关注各个条件进行单独测试
	for ( ; ; ) {
		//指定所关注的描述符
		FD_ZERO(&rset);
		FD_ZERO(&wset);
		if (stdineof == 0 && toiptr < &to[MAXLINE])
			FD_SET(STDIN_FILENO, &rset);	/* 打开读描述符集中对应标准输入的位 */
		if (friptr < &fr[MAXLINE])
			FD_SET(sockfd, &rset);			/* 打开读描述符集中对应套接字的位 */
		if (tooptr != toiptr)
			FD_SET(sockfd, &wset);			/* 打开写描述符集中对应套接字的位 */
		if (froptr != friptr)
			FD_SET(STDOUT_FILENO, &wset);	/* 打开写描述符集中对应标准输出的位 */
		//调用 select
		Select(maxfdp1, &rset, &wset, NULL, NULL);
/* end nonb1 */
/* include nonb2 */
		//4.对所关注各个条件进行单独测试
		//从标准输入 read
		if (FD_ISSET(STDIN_FILENO, &rset)) {
			//read 返回错误。 EWOULDBLOCK 是正常的错误,表明这个描述符还处在阻塞中,应该忽略该错误
			if ( (n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("read error on stdin");

			}
			//read 返回 EOF,标准输入处理结束
			else if (n == 0) {
#ifdef	VOL2
				fprintf(stderr, "%s: EOF on stdin\n", gf_time());
#endif
				stdineof = 1;			/* 标志标准输入结束 */
				if (tooptr == toiptr)   // to 缓冲区中已经没有数据要发送
					Shutdown(sockfd, SHUT_WR);/* 关闭客户端向服务器的写操作 */

			}
			//read 返回数据
			else {
#ifdef	VOL2
				fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);
#endif
				toiptr += n;			//增加 toiptr
				FD_SET(sockfd, &wset);	//打开写描述符集中对应套接字的位
			}
		}

		//从套接字 read
		if (FD_ISSET(sockfd, &rset)) {
			//read 返回错误。 EWOULDBLOCK 是正常的错误,表明这个描述符还处在阻塞中,应该忽略该错误
			if ( (n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("read error on socket");

			}
			//read 返回 EOF,服务器返回数据结束
			else if (n == 0) {
#ifdef	VOL2
				fprintf(stderr, "%s: EOF on socket\n", gf_time());
#endif
				if (stdineof) //如果遇到来自服务器的 EOF ,并且已经在标准输入上遇到 EOF ,则正常结束;否则就是服务器过早终止了
					return;		/* normal termination */
				else
					err_quit("str_cli: server terminated prematurely");

			}
			//read 返回数据
			else {
#ifdef	VOL2
				fprintf(stderr, "%s: read %d bytes from socket\n",
								gf_time(), n);
#endif
				friptr += n;		//增加 friptr
				FD_SET(STDOUT_FILENO, &wset);	// 打开写描述符集中对应标准输出的位
			}
		}
/* end nonb2 */
/* include nonb3 */
		//write 到标准输出
		if (FD_ISSET(STDOUT_FILENO, &wset) && ( (n = friptr - froptr) > 0)) {
			//write 返回错误。
			if ( (nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("write error to stdout");

			}
			//write 成功
			else {
#ifdef	VOL2
				fprintf(stderr, "%s: wrote %d bytes to stdout\n",
								gf_time(), nwritten);
#endif
				froptr += nwritten;		//增加写出字节数
				if (froptr == friptr)
					froptr = friptr = fr;	//如果输出指针追上输入指针, 这两个指针不同时恢复为指向缓冲区开始处
			}
		}

		//write 到套接字
		if (FD_ISSET(sockfd, &wset) && ( (n = toiptr - tooptr) > 0)) {
			//write 失败
			if ( (nwritten = write(sockfd, tooptr, n)) < 0) {
				if (errno != EWOULDBLOCK)
					err_sys("write error to socket");

			}
			//write 成功
			else {
#ifdef	VOL2
				fprintf(stderr, "%s: wrote %d bytes to socket\n",
								gf_time(), nwritten);
#endif
				tooptr += nwritten;
				if (tooptr == toiptr) { //
					toiptr = tooptr = to;
					if (stdineof) //如果标准输入已经读完,并且已经缓冲区的数据都已经发送给服务器,就关闭写套接字
						Shutdown(sockfd, SHUT_WR);	/* send FIN */
				}
			}
		}
	}
}
/* end nonb3 */

改善3问题:非阻塞式版本比较复杂

改善4:需要使用非阻塞I/O时,更简单的方法是把应用程序任务划分到多个进程(使用 fork) 或多个线程

fork 版本:

/**
* TCP 使用两个进程 (fork)
**/
#include	"unp.h"

void
str_cli(FILE *fp, int sockfd)
{
	pid_t	pid;
	char	sendline[MAXLINE], recvline[MAXLINE];

	//子进程:从服务器读取数据并输出到 stdout
	if ( (pid = Fork()) == 0) {
		while (Readline(sockfd, recvline, MAXLINE) > 0)
			Fputs(recvline, stdout);

		kill(getppid(), SIGTERM);	/* 子进程向父进程发送一个 SIGTERM 信号,终止父进程 */
		exit(0);
	}

	//父进程:从 stdin 读取数据并发送到服务器
	while (Fgets(sendline, MAXLINE, fp) != NULL)
		Writen(sockfd, sendline, strlen(sendline));

	//在 stdin 读取 EOF,输入结束,关闭写套接字
	Shutdown(sockfd, SHUT_WR);	/* EOF on stdin, send FIN */

	//父进程完成数据凰调用 pause 让自己进入睡眠状态
	pause();
	return;
}

线程化版本:

/**
* TCP 使用两个线程
**/
#include	"unpthread.h"

void	*copyto(void *);

static int	sockfd;		/* global for both threads to access */
static FILE	*fp;

void
str_cli(FILE *fp_arg, int sockfd_arg)
{
	char		recvline[MAXLINE];
	pthread_t	tid;

	//1.把参数保存在外部变量中
	sockfd = sockfd_arg;	/* copy arguments to externals */
	fp = fp_arg;

	//2.创建新线程
	Pthread_create(&tid, NULL, copyto, NULL);

	//3.主线程循环:从套接字到标准输出复制
	while (Readline(sockfd, recvline, MAXLINE) > 0)
		Fputs(recvline, stdout);
}

//4.copyto 线程:从标准输入到套接字复制
void *
copyto(void *arg)
{
	char	sendline[MAXLINE];

	while (Fgets(sendline, MAXLINE, fp) != NULL)
		Writen(sockfd, sendline, strlen(sendline));

	Shutdown(sockfd, SHUT_WR);	/* EOF on stdin, send FIN */

	return(NULL);
		/* 4return (i.e., thread terminates) when EOF on stdin */
}

时间: 2024-10-13 02:03:25

UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式的相关文章

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

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 这是一个简单的回射服务器程序.它将客户发送的数据读入缓冲区并回射其中内容 下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本.select 版本.poll 版本.多线程版本 fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求 /** * TCP/IPv4 协议相关 * **/ #include "unp.h" in

UNIX网络编程卷1 回射客户程序 UDP 超时设置

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 最初代码: #include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: udpcli <IPaddress>"); //1.指明服务器的 IP 地址和端口 bze

UNIX网络编程卷1 时间获取程序server TCP 协议相关性

本文为senlie原创.转载请保留此地址:http://blog.csdn.net/zhengsenlie 最初代码: 这是一个简单的时间获取server程序.它和时间获取程序client一道工作. 它是 协议相关,把代码中出现的左边的字符串换为右边的,就变成了IPv6版本号的 IPv4 --> IPv6 sockaddr_in --> sockaddr_in6 AF_INET --> AF_INET6 sin_family --> sin6_family sin_port --&

UNIX网络编程 卷1:套接字联网API

这篇是计算机类的优质预售推荐>>>><UNIX网络编程 卷1:套接字联网API> UNIX和网络专家W. Richard Stevens的传世之作,世界著名网络专家Bill Fenner和Andrew M. Rudoff执笔新版 编辑推荐 这是一部传世之作!顶级网络编程专家Bill Fenner和Andrew M. Rudoff应邀执笔,对W. Richard Stevens的经典作品进行修订.书中吸纳了近几年网络技术的发展,增添了IPv6.SCTP协议和密钥管理套接字

将UNIX网络编程卷2的库函数合并到卷1的库函数中

源起 前面讲述了unix网路编程卷1库函数的配置.但是卷2还有一个配置,而且其中的关于进程间通信的函数在卷1中也没有. 我们使用两个库函数不免有些不方便,现在将卷2中的在卷1中没有的函数都合并到卷1的库函数中. 1.创建unix网络编程卷2——进程间通信configure.h配置文件    cd 目录    ./configure    之后创建了configure.h文件. 2.合并unix网路编程卷1和卷2的configure.h文件    将上面生成的configure.h的头文件的宏定义

UNIX网络编程 卷2:进程间通信

这篇是计算机类的优质预售推荐>>>><UNIX网络编程 卷2:进程间通信(第2版)> UNIX和网络专家W. Richard Stevens的传世之作 编辑推荐 两卷本的<UNIX网络编程>是已故著名技术作家W. Richard Stevens的传世之作.卷2着重讨论怎样让应用程序与在其它机器上的应用程序进行对话. 良好的进程间通信(IPC)机制是提高UNIX程序性能的关键. 本书全面深入地解说了各种进程间通信形式,包括消息传递.同步.共享内存及远程过程调用

[转载] 读《UNIX网络编程 卷1:套接字联网API》

原文: http://cstdlib.com/tech/2014/10/09/read-unix-network-programming-1/ 文章写的很清楚, 适合初学者 最近看了<UNIX网络编程 卷1:套接字联网API>, 英文名叫Unix Network Programming啦,后来上网查了查, 一般都叫UNP逼格会高一点, 就像APUE一样. 他们的作者都是W. Richard Stevens. 另外,他也是TCP/IP Illustrated的作者. 靠,看完作者简介,简直崇拜得

UNIX网络编程卷2 源码编译篇

W. Richard Stevens的主页: 源代码下载   >>  ~/Downloads/unpv22e.tar.gz; 1 tar -xzfv unpv22e.tar.gz 2 cd unpv22e 3 ./configure 4 cd lib 5 make make编译失败,因为需要对两个文件修改,unpv22e/config.h和unpv22e/wrapunix.c. 1 vi config.h 2 3 /*注释掉这三行*/ 4 // #define uint8_t unsigned

《UNIX网络编程 卷1》之&quot;学习环境搭建&quot;(CentOS 7)

<UNIX网络编程 卷1>的源码可以从www.unpbook.com下载得到.解压之后的目录为unpv13e.  1. 编译 进入unpv13e目录,按如下步骤编译: 1 ./configure 2 3 cd lib 4 make // 可能遇到问题:redefinition of ‘struct in_pktinfo’ 5 6 cd ../libfree 7 make 8 9 cd ../libroute 10 make //这一步可能会出错,可忽略,只是表示你的系统不支持 4.4BSD,并