TCP测试用客户程序

1 #include "unp.h" 2 3 #define MAXN 16384 /* max # bytes to request from server */ 4 5 int 6 main(int argc, char **argv) 7 { 8 int i, j, fd, nchildren, nloops, nbytes; 9 pid_t pid; 10 ssize_t n; 11 char request[MAXLINE], reply[MAXN]; 12 13 if (argc != 6) 14 err_quit("usage: client <hostname or IPaddr> <port> <#children> " 15 "<#loops/child> <#bytes/request>"); 16 17 nchildren = atoi(argv[3]); 18 nloops = atoi(argv[4]); 19 nbytes = atoi(argv[5]); 20 snprintf(request, sizeof(request), "%d ", nbytes); /* newline at end */ 21 22 for (i = 0; i < nchildren; i++) { 23 if ( (pid = Fork()) == 0) { /* child */ 24 for (j = 0; j < nloops; j++) { 25 fd = Tcp_connect(argv[1], argv[2]); 26 27 Write(fd, request, strlen(request)); 28 29 if ( (n = Readn(fd, reply, nbytes)) != nbytes) 30 err_quit("server returned %d bytes", n); 31 32 Close(fd); /* TIME_WAIT on client, not server */ 33 } 34 printf("child %d done ", i); 35 exit(0); 36 } 37 /* parent loops around to fork() again */ 38 } 39 40 while (wait(NULL) > 0) /* now parent waits for all children */ 41 ; 42 if (errno != ECHILD) 43 err_sys("wait error"); 44 45 exit(0); 46 }
每次运行本客户程序时,我们指定了
1.服务器的主机名或IP地址
2.服务器的端口
3.由客户fork的子进程数(以允许客户并发地向同一个服务器发起多个连接)
4.每个子进程发送给服务器的请求数
5.每个请求要求服务器反送的数据字节数
大写字母开头的函数(如Write是对原始write进行过错误处理的包裹函数)
tcp_connect函数以及后面的tcp_listen函数可以查看第十一章的笔记 http://www.cnblogs.com/runnyu/p/4663514.html
TCP迭代服务器程序
第一章我们给出的服务器程序就是迭代服务器。
迭代TCP服务器总是在完全处理某个客户的请求后才转向下一个客户的。
下面给出的迭代TCP服务器只是下一节并发服务器程序的少许修改,部分函数代码在下一节将演示。

1 /* include serv00 */ 2 #include "unp.h" 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 void sig_int(int), web_child(int); 9 socklen_t clilen, addrlen; 10 struct sockaddr *cliaddr; 11 12 if (argc == 2) 13 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 14 else if (argc == 3) 15 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 16 else 17 err_quit("usage: serv00 [ <host> ] <port#>"); 18 cliaddr = Malloc(addrlen); 19 20 Signal(SIGINT, sig_int); 21 22 for ( ; ; ) { 23 clilen = addrlen; 24 connfd = Accept(listenfd, cliaddr, &clilen); 25 26 web_child(connfd); /* process the request */ 27 28 Close(connfd); /* parent closes connected socket */ 29 } 30 } 31 /* end serv00 */
运行结果
在8888端口运行该服务器程序
运行测试用客户程序
终止服务器程序。由于服务器是迭代的,这就让我们测量出服务器处理如此数目客户所需CPU时间的一个基准值,从其他服务器的实测CPU时间中减去该值就能得到他们的进程控制时间
TCP并发服务器程序:每个客户一个子进程
传统上并发服务器调用fork派生一个子进程来处理每个客户。这使得服务器能够同时为多个客户服务,每个进程一个客户。
并发服务器的问题在于为每个客户现场fork一个子进程比较耗费CPU时间。
下面是一个并发服务器程序的例子,绝大多数服务器程序也按照这个范式编写
main函数

1 /* include serv01 */ 2 #include "unp.h" 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 pid_t childpid; 9 void sig_chld(int), sig_int(int), web_child(int); 10 socklen_t clilen, addrlen; 11 struct sockaddr *cliaddr; 12 13 if (argc == 2) 14 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 15 else if (argc == 3) 16 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 17 else 18 err_quit("usage: serv01 [ <host> ] <port#>"); 19 cliaddr = Malloc(addrlen); 20 21 Signal(SIGCHLD, sig_chld); 22 Signal(SIGINT, sig_int); 23 24 for ( ; ; ) { 25 clilen = addrlen; 26 if ( (connfd = accept(listenfd, cliaddr, &clilen)) < 0) { 27 if (errno == EINTR) 28 continue; /* back to for() */ 29 else 30 err_sys("accept error"); 31 } 32 33 if ( (childpid = Fork()) == 0) { /* child process */ 34 Close(listenfd); /* close listening socket */ 35 web_child(connfd); /* process request */ 36 exit(0); 37 } 38 Close(connfd); /* parent closes connected socket */ 39 } 40 } 41 /* end serv01 */
信号处理函数。在客户运行完毕之后我们键入Ctrl+C进入SIGINT信号处理函数,以显示服务器程序所需的CPU时间

1 void 2 sig_int(int signo) 3 { 4 void pr_cpu_time(void); 5 6 pr_cpu_time(); 7 exit(0); 8 }
SIGINT信号处理函数调用的pr_cup_time函数

1 #include "unp.h" 2 #include <sys/resource.h> 3 4 #ifndef HAVE_GETRUSAGE_PROTO 5 int getrusage(int, struct rusage *); 6 #endif 7 8 void 9 pr_cpu_time(void) 10 { 11 double user, sys; 12 struct rusage myusage, childusage; 13 14 if (getrusage(RUSAGE_SELF, &myusage) < 0) 15 err_sys("getrusage error"); 16 if (getrusage(RUSAGE_CHILDREN, &childusage) < 0) 17 err_sys("getrusage error"); 18 19 user = (double) myusage.ru_utime.tv_sec + 20 myusage.ru_utime.tv_usec/1000000.0; 21 user += (double) childusage.ru_utime.tv_sec + 22 childusage.ru_utime.tv_usec/1000000.0; 23 sys = (double) myusage.ru_stime.tv_sec + 24 myusage.ru_stime.tv_usec/1000000.0; 25 sys += (double) childusage.ru_stime.tv_sec + 26 childusage.ru_stime.tv_usec/1000000.0; 27 28 printf(" user time = %g, sys time = %g ", user, sys); 29 }
下面是处理每个客户请求的web_child函数

1 #include "unp.h" 2 3 #define MAXN 16384 /* max # bytes client can request */ 4 5 void 6 web_child(int sockfd) 7 { 8 int ntowrite; 9 ssize_t nread; 10 char line[MAXLINE], result[MAXN]; 11 12 for ( ; ; ) { 13 if ( (nread = Readline(sockfd, line, MAXLINE)) == 0) 14 return; /* connection closed by other end */ 15 16 /* 4line from client specifies #bytes to write back */ 17 ntowrite = atol(line); 18 if ((ntowrite <= 0) || (ntowrite > MAXN)) 19 err_quit("client request for %d bytes", ntowrite); 20 21 Writen(sockfd, result, ntowrite); 22 } 23 }
TCP预先派生子进程服务器程序:accept无上锁保护
传统意义的并发服务器像上一节那样为每个客户现场派生一个子进程,本节将使用成为预先派生子进程的技术:
在启动阶段预先派生一定数量的子进程,当各个客户连接到达时,这些子进程立即就能为它们服务。
这种技术的优点在于无须引入父进程执行fork的开销就能处理新到的客户。缺点是父进程必须在服务器启动阶段猜测需要预先派生多少子进程。
下面给出我们预先派生子进程服务器程序第一个main函数

1 /* include serv02 */ 2 #include "unp.h" 3 4 static int nchildren; 5 static pid_t *pids; 6 7 int 8 main(int argc, char **argv) 9 { 10 int listenfd, i; 11 socklen_t addrlen; 12 void sig_int(int); 13 pid_t child_make(int, int, int); 14 15 if (argc == 3) 16 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 17 else if (argc == 4) 18 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 19 else 20 err_quit("usage: serv02 [ <host> ] <port#> <#children>"); 21 nchildren = atoi(argv[argc-1]); 22 pids = Calloc(nchildren, sizeof(pid_t)); 23 24 for (i = 0; i < nchildren; i++) 25 pids[i] = child_make(i, listenfd, addrlen); /* parent returns */ 26 27 Signal(SIGINT, sig_int); 28 29 for ( ; ; ) 30 pause(); /* everything done by children */ 31 } 32 /* end serv02 */
增设一个命令行参数供用户指定预先派生的子进程个数。分配一个存放各个子进程ID的数组。
下面给出SIGINT信号处理函数。在调用pr_cpu_time之前不需终止所有子进程来获得子进程的资源利用统计

1 void 2 sig_int(int signo) 3 { 4 int i; 5 void pr_cpu_time(void); 6 7 /* 4terminate all children */ 8 for (i = 0; i < nchildren; i++) 9 kill(pids[i], SIGTERM); 10 while (wait(NULL) > 0) /* wait for all children */ 11 ; 12 if (errno != ECHILD) 13 err_sys("wait error"); 14 15 pr_cpu_time(); 16 exit(0); 17 }
下面给出child_make函数,它由main调用以派生各个子进程

1 pid_t 2 child_make(int i, int listenfd, int addrlen) 3 { 4 pid_t pid; 5 void child_main(int, int, int); 6 7 if ( (pid = Fork()) > 0) 8 return(pid); /* parent */ 9 10 child_main(i, listenfd, addrlen); /* never returns */ 11 }
调用fork派生子进程后只有父进程返回。子进程调用下面给出的child_main函数,它是个无限循环

1 void 2 child_main(int i, int listenfd, int addrlen) 3 { 4 int connfd; 5 void web_child(int); 6 socklen_t clilen; 7 struct sockaddr *cliaddr; 8 9 cliaddr = Malloc(addrlen); 10 11 printf("child %ld starting ", (long) getpid()); 12 for ( ; ; ) { 13 clilen = addrlen; 14 connfd = Accept(listenfd, cliaddr, &clilen); 15 16 web_child(connfd); /* process the request */ 17 Close(connfd); 18 } 19 }
每个子进程调用accept返回一个已连接套接字,然后调用web_child处理客户请求,最后关闭连接。子进程一直循环,知道被父进程终止。
这个程序有一个性能上的问题:
服务器再启动阶段派生N个子进程,它们各自调用accept并因而均被内核投入睡眠,
当第一个客户连接到达时,所有N歌子进程均被唤醒,然后只有最先运行的子进程获得能够客户连接,
这就是有时候成为惊群(thundering herd)的问题。只是每当一个连接准备好被接收时唤醒太多进程的做法会导致性能受损
TCP预先派生子进程服务器程序:accept使用文件上锁保护
在不同的系统上accept函数的实现会有所不同。
事实上如果我们在基于SVR4的Solaris 2.5内核上运行上一节的服务器程序,那么客户开始连接到该服务器后不久,某个子进程的accept就会返回EPROTO错误
解决办法是让应用程序在调用accept前后安置某种形式的锁(lock),这样任意时刻只有一个子进程阻塞在accept调用中,其他子进程则阻塞在试图获取用于保护accept的锁上
我们有多种方法可用于提供上锁功能。本节我们使用以fcntl函数呈现的POSIX文件上锁功能
main函数唯一改动是在派生子进程的循环之前增加一个对我们的my_lock_init函数的调用
my_lock_init("/tmp/lock.XXXXXX"); /* one lock file for all children */ for (i = 0; i < nchildren; i++) pids[i] = child_make(i, listenfd, addrlen); /* parent returns */
child_main的唯一改动是在调用accept之前获取文件锁,在accept返回之后释放文件锁
for ( ; ; ) { clilen = addrlen; my_lock_wait(); connfd = Accept(listenfd, cliaddr, &clilen); my_lock_release(); web_child(connfd); /* process the request */ Close(connfd); }
下面给出使用POSIX文件上锁功能的my_lock_init函数

1 #include "unp.h" 2 3 static struct flock lock_it, unlock_it; 4 static int lock_fd = -1; 5 /* fcntl() will fail if my_lock_init() not called */ 6 7 void 8 my_lock_init(char *pathname) 9 { 10 char lock_file[1024]; 11 12 /* 4must copy caller's string, in case it's a constant */ 13 strncpy(lock_file, pathname, sizeof(lock_file)); 14 lock_fd = Mkstemp(lock_file); 15 16 Unlink(lock_file); /* but lock_fd remains open */ 17 18 lock_it.l_type = F_WRLCK; 19 lock_it.l_whence = SEEK_SET; 20 lock_it.l_start = 0; 21 lock_it.l_len = 0; 22 23 unlock_it.l_type = F_UNLCK; 24 unlock_it.l_whence = SEEK_SET; 25 unlock_it.l_start = 0; 26 unlock_it.l_len = 0; 27 }
mkstemp函数在系统中以唯一的文件名创建一个文件并打开。关于fcntl记录锁可以查看apue的笔记 http://www.cnblogs.com/runnyu/p/4645754.html
下面给出用于上锁和解锁文件的两个函数

void my_lock_wait() { int rc; while ( (rc = fcntl(lock_fd, F_SETLKW, &lock_it)) < 0) { if (errno == EINTR) continue; else err_sys("fcntl error for my_lock_wait"); } } void my_lock_release() { if (fcntl(lock_fd, F_SETLKW, &unlock_it) < 0) err_sys("fcntl error for my_lock_release"); }
TCP预先派生子进程服务器程序:accept使用线程上锁保护
上一节使用的POSIX文件上锁方法可移植到所有POSIX兼容系统,不过它涉及文件系统操作,可能比较耗时。
本节我们改用线程上锁保护accept,这种方法不能适用于同一进程内各线程之间的上锁,而且适用于不同进程之间的上锁。
我们需要改动的只是3个上锁好熟。在不同进程之间使用线程上锁要求:
1.互斥锁变量必须存放在由所有进程共享的内存区中
2.必须告知线程函数库这是在不同进程之间共享的互斥锁
关于互斥量可以查看apue的笔记 http://www.cnblogs.com/runnyu/p/4643363.html 关于互斥量属性可以查看 http://www.cnblogs.com/runnyu/p/4643764.html
我们有多种方法可用于不同进程之间共享内存空间。在本节例子中我们使用mmap函数以及/dev/zero设备。
下面是新版本的my_lock_init函数

1 #include "unpthread.h" 2 #include <sys/mman.h> 3 4 static pthread_mutex_t *mptr; /* actual mutex will be in shared memory */ 5 6 void 7 my_lock_init(char *pathname) 8 { 9 int fd; 10 pthread_mutexattr_t mattr; 11 12 fd = Open("/dev/zero", O_RDWR, 0); 13 14 mptr = Mmap(0, sizeof(pthread_mutex_t), PROT_READ | PROT_WRITE, 15 MAP_SHARED, fd, 0); 16 Close(fd); 17 18 Pthread_mutexattr_init(&mattr); 19 Pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); 20 Pthread_mutex_init(mptr, &mattr); 21 }
下面是新版本的my_lock_wait和my_lock_relesease函数

1 void 2 my_lock_wait() 3 { 4 Pthread_mutex_lock(mptr); 5 } 6 7 void 8 my_lock_release() 9 { 10 Pthread_mutex_unlock(mptr); 11 }
TCP预先派生子进程服务器程序:传递描述符
这个版本是只让父进程调用accept,然后把所有接受的已连接套接字“传递”给某个子进程。
TCP并发服务器程序:每个客户一个线程
在本节中我们的服务器程序将为每个客户创建一个线程来取代为每个客户派生一个子进程
main函数

1 #include "unpthread.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int listenfd, connfd; 7 void sig_int(int); 8 void *doit(void *); 9 pthread_t tid; 10 socklen_t clilen, addrlen; 11 struct sockaddr *cliaddr; 12 13 if (argc == 2) 14 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 15 else if (argc == 3) 16 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 17 else 18 err_quit("usage: serv06 [ <host> ] <port#>"); 19 cliaddr = Malloc(addrlen); 20 21 Signal(SIGINT, sig_int); 22 23 for ( ; ; ) { 24 clilen = addrlen; 25 connfd = Accept(listenfd, cliaddr, &clilen); 26 27 Pthread_create(&tid, NULL, &doit, (void *) connfd); 28 } 29 } 30 31 void * 32 doit(void *arg) 33 { 34 void web_child(int); 35 36 Pthread_detach(pthread_self()); 37 web_child((int) arg); 38 Close((int) arg); 39 return(NULL); 40 }
主线程大部分时间阻塞在一个accept调用之中,每当它返回一个客户连接时,就调用pthread_create创建一个新线程。新线程执行的函数是doit。
doit函数先让自己脱离,使得主线程不必等待它,然后调用web_child函数。该函数返回后关闭已连接套接字。
TCP预先创建线程服务器程序:每个线程各自accept
跟预先派生子进程服务器程序一样,我们可以在服务器启动阶段预先创建一个线程池以取代为每个客户现场创建一个线程的做法来提高性能。
本服务器的基本设计是预先创建一个线程池,并让每个线程各自调用accept,使用互斥锁以保证任何时刻只有一个线程调用accept。
我们在pthread07.h中定义了用于维护关于每个线程若干信息的Thread结构,并声明了一些全局变量
typedef struct { pthread_t thread_tid; /* thread ID */ long thread_count; /* # connections handled */ } Thread; Thread *tptr; /* array of Thread structures; calloc'ed */ int listenfd, nthreads; socklen_t addrlen; pthread_mutex_t mlock;
main函数

1 /* include serv07 */ 2 #include "unpthread.h" 3 #include "pthread07.h" 4 5 pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; 6 7 int 8 main(int argc, char **argv) 9 { 10 int i; 11 void sig_int(int), thread_make(int); 12 13 if (argc == 3) 14 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 15 else if (argc == 4) 16 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 17 else 18 err_quit("usage: serv07 [ <host> ] <port#> <#threads>"); 19 nthreads = atoi(argv[argc-1]); 20 tptr = Calloc(nthreads, sizeof(Thread)); 21 22 for (i = 0; i < nthreads; i++) 23 thread_make(i); /* only main thread returns */ 24 25 Signal(SIGINT, sig_int); 26 27 for ( ; ; ) 28 pause(); /* everything done by threads */ 29 } 30 /* end serv07 */ 31 32 void 33 sig_int(int signo) 34 { 35 int i; 36 void pr_cpu_time(void); 37 38 pr_cpu_time(); 39 40 for (i = 0; i < nthreads; i++) 41 printf("thread %d, %ld connections ", i, tptr[i].thread_count); 42 43 exit(0); 44 }
thread_make和thread_main函数

1 #include "unpthread.h" 2 #include "pthread07.h" 3 4 void 5 thread_make(int i) 6 { 7 void *thread_main(void *); 8 9 Pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void *) i); 10 return; /* main thread returns */ 11 } 12 13 void * 14 thread_main(void *arg) 15 { 16 int connfd; 17 void web_child(int); 18 socklen_t clilen; 19 struct sockaddr *cliaddr; 20 21 cliaddr = Malloc(addrlen); 22 23 printf("thread %d starting ", (int) arg); 24 for ( ; ; ) { 25 clilen = addrlen; 26 Pthread_mutex_lock(&mlock); 27 connfd = Accept(listenfd, cliaddr, &clilen); 28 Pthread_mutex_unlock(&mlock); 29 tptr[(int) arg].thread_count++; 30 31 web_child(connfd); /* process request */ 32 Close(connfd); 33 } 34 }
TCP预先创建线程服务器程序:主线程统一accept
与上一节服务器程序比较,本节的服务器程序只让主线程调用accept并把每个客户连接传递给线程池中某个可用线程。
我们可以使用描述符进行传递,因为所有线程和所有描述符都在同一个进程之内,接收线程只需知道这个已连接套接字描述符的值。
在pthread08.h头文件中定义了Thread结构和声明了一些全局变量
typedef struct { pthread_t thread_tid; /* thread ID */ long thread_count; /* # connections handled */ } Thread; Thread *tptr; /* array of Thread structures; calloc'ed */ #define MAXNCLI 32 int clifd[MAXNCLI], iget, iput; pthread_mutex_t clifd_mutex; pthread_cond_t clifd_cond;
我们定义了一个clifd数组,由主线程往中存入已接受的已连接套接字描述符,并由线程池中的可用线程从中取出一个以服务响应的客户
iput是主线程将往该数组中存入的下一个元素的下标,iget是线程池中某个线程将从该数组中取出的下一个元素的下标。我们使用互斥锁和条件变量把这些数据保护起来
main函数

1 /* include serv08 */ 2 #include "unpthread.h" 3 #include "pthread08.h" 4 5 static int nthreads; 6 pthread_mutex_t clifd_mutex = PTHREAD_MUTEX_INITIALIZER; 7 pthread_cond_t clifd_cond = PTHREAD_COND_INITIALIZER; 8 9 int 10 main(int argc, char **argv) 11 { 12 int i, listenfd, connfd; 13 void sig_int(int), thread_make(int); 14 socklen_t addrlen, clilen; 15 struct sockaddr *cliaddr; 16 17 if (argc == 3) 18 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 19 else if (argc == 4) 20 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 21 else 22 err_quit("usage: serv08 [ <host> ] <port#> <#threads>"); 23 cliaddr = Malloc(addrlen); 24 25 nthreads = atoi(argv[argc-1]); 26 tptr = Calloc(nthreads, sizeof(Thread)); 27 iget = iput = 0; 28 29 /* 4create all the threads */ 30 for (i = 0; i < nthreads; i++) 31 thread_make(i); /* only main thread returns */ 32 33 Signal(SIGINT, sig_int); 34 35 for ( ; ; ) { 36 clilen = addrlen; 37 connfd = Accept(listenfd, cliaddr, &clilen); 38 39 Pthread_mutex_lock(&clifd_mutex); 40 clifd[iput] = connfd; 41 if (++iput == MAXNCLI) 42 iput = 0; 43 if (iput == iget) 44 err_quit("iput = iget = %d", iput); 45 Pthread_cond_signal(&clifd_cond); 46 Pthread_mutex_unlock(&clifd_mutex); 47 } 48 } 49 /* end serv08 */ 50 51 void 52 sig_int(int signo) 53 { 54 int i; 55 void pr_cpu_time(void); 56 57 pr_cpu_time(); 58 59 for (i = 0; i < nthreads; i++) 60 printf("thread %d, %ld connections ", i, tptr[i].thread_count); 61 62 exit(0); 63 }
下面是thread_make和thread_main函数

1 #include "unpthread.h" 2 #include "pthread08.h" 3 4 void 5 thread_make(int i) 6 { 7 void *thread_main(void *); 8 9 Pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void *) i); 10 return; /* main thread returns */ 11 } 12 13 void * 14 thread_main(void *arg) 15 { 16 int connfd; 17 void web_child(int); 18 19 printf("thread %d starting ", (int) arg); 20 for ( ; ; ) { 21 Pthread_mutex_lock(&clifd_mutex); 22 while (iget == iput) 23 Pthread_cond_wait(&clifd_cond, &clifd_mutex); 24 connfd = clifd[iget]; /* connected socket to service */ 25 if (++iget == MAXNCLI) 26 iget = 0; 27 Pthread_mutex_unlock(&clifd_mutex); 28 tptr[(int) arg].thread_count++; 29 30 web_child(connfd); /* process request */ 31 Close(connfd); 32 } 33 }