一:概述
在简单的回射服务例子中,客户端和服务器的交互步骤如下:
客户从标准输入中读入一行文本,并写给服务器;
服务器从网络输入读入这行文本,并回射给客户;
客户从网络输入读入这行回射文本,并显示在标准输出上。下图描述了整个过程:
二:多进程的str_cli
其中客户端的str_cli函数处理客户端上的主要逻辑:从标准输入读入一行文本,写到服务器上,然后读会服务器对改行的回射,写到标准输出。
str_cli的非阻塞版木比较复杂--约有135行代码(P342),与之相比,使用select和阻塞式IO的版木有40行代码(P137),而最初的停-等版本(P100)则只有区区20行代码。
代码长度从20行倍增到40行的努力是值得的,因为在批最模式下执行速度几乎提高了30倍,而且在阻塞的描述符上使用select不太复杂。然而考虑到结果代码的复杂性,把应用程序编写成使用非阻塞式IO的努力足否照样值得?答案是否定的。每当我们发现需要使用非阻塞式IO时,更简单的办法通常是把应用程序仟务划分到多个迸程〔使用Fork〕或多个线程。
下面是使用多进程的版本:
#include "unp.h"
void str_cli(FILE *fp, int sockfd)
{
pid_t pid;
char sendline[MAXLINE], recvline[MAXLINE];
if ( (pid = Fork()) == 0) { /* child: server -> stdout */
while (Readline(sockfd, recvline, MAXLINE) > 0)
Fputs(recvline, stdout);
kill(getppid(), SIGTERM); /* in case parent still running */
exit(0);
}
/* parent: stdin -> server */
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
pause();
return;
}
图中明确地指出:所用TCP连接是全双工的,而且父子进程共享同一个套接字:父进程往该套接字中写,子进程从该套接字中读。尽管套接字只有一个,其接收缓冲区和发送缓冲区也分别只有一个,然而这个套接字却有两个描述符在引用它:一个在父进程中,另一个在子进程中。
三:多线程的echo服务器
#include "unpthread.h"
static void *doit(void*); /* each thread executes thisfunction */
int main(int argc, char **argv)
{
int listenfd, connfd;
pthread_t tid;
socklen_t addrlen, len;
struct sockaddr *cliaddr;
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 [
cliaddr = malloc(addrlen);
for (; ; ) {
len = addrlen;
connfd = Accept(listenfd, cliaddr, &len);
Pthread_create(&tid, NULL, &doit, (void *) connfd);
}
}
static void * doit(void * arg)
{
Pthread_detach(pthread_self());
str_echo((int) arg); /* same function as before */
Close((int) arg); /* done with connected socket*/
return (NULL);
}
注意:主线程不关闭已连接套接字,而在调用fork的并发服务器程序中却总是反着做。这是因为同一进程内的所有线程共享全部描述符。要是主线程调用close,它就会终止相应的连接。创建新线程并不影响己打开描述符的引用计数,这一点不同于fork。
摘自《Unix网络编程卷一:套接字联网API》