有关并发
Apr 1, 2014
Technology
app2e中有几个很好的关于并发实现的例子,这里加以详细解析。 ###简单的echo服务器 所谓echo服务器就是将客户端的输入简单的通过socket回送回来。代码实现如下:
#include <csapp.h>
void echo(int connfd);
int main(int argc, char **argv)
{
int listenfd, connfd, port, clientlen;
struct sockaddr_in clientaddr;
struct hostent *hp;
char *haddrp;
if(argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
return 1;
}
port = atoi(argv[1]);
listenfd = Open_listenfd(port);
while(1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
/* determin the domain name and IP address of the client */
hp = Gethostbyaddr((const char*)&clientaddr.sin_addr.s_addr,
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
haddrp = inet_ntoa(clientaddr.sin_addr);
printf("server conected to %s (%s)\n", hp->h_name, haddrp);
echo(connfd);
Close(connfd);
}
return 0;
}
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", n);
Rio_writen(connfd, buf, n);
}
}
从代码来看,这是一个很典型的socket通信的例子。连接一旦建立成功,server段会打印出client端的IP地址,并一直在echo程序中晃荡。因为echo()中有while()函数会一直等着从connfd文件描述符读入输入行。当得到来自socket fd的输入时,会打印出接收到的字符个数,并将其写入到socket文件描述符中,由此client段会得到回显字符。 ###利用进程实现并发 上面的简单echo服务器是没法接受一个以上的连接的。因此我们写出echo服务器的第二版,利用子进程实现echo服务器。
/*
* echoserverp.c - A concurrent echo server based on processes
*/
/* $begin echoserverpmain */
#include "csapp.h"
void echo(int connfd);
void sigchld_handler(int sig) //line:conc:echoserverp:handlerstart
{
while (waitpid(-1, 0, WNOHANG) > 0)
;
return;
} //line:conc:echoserverp:handlerend
int main(int argc, char **argv)
{
int listenfd, connfd, port;
socklen_t clientlen=sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
Signal(SIGCHLD, sigchld_handler);
listenfd = Open_listenfd(port);
while (1) {
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
if (Fork() == 0) {
Close(listenfd); /* Child closes its listening socket */
echo(connfd); /* Child services client */ //line:conc:echoserverp:echofun
Close(connfd); /* Child closes connection with client */ //line:conc:echoserverp:childclose
exit(0); /* Child exits */
}
Close(connfd); /* Parent closes connected socket (important!) */ //line:conc:echoserverp:parentclose
}
}
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", n);
Rio_writen(connfd, buf, n);
}
}
/* $end echoserverpmain */
编译和运行命令如下:
$ gcc -o echoserverp echoserverp.c -lcsapp -lpthread
$ ./echoserverp 3344
$ ./echoclient localhost 3344
在多个终端上执行完./echoclient localhost 3344后,我们可以用ps -ef | grep echoserverp来检查当前系统中的进程个数:
$ ps -ef | grep echoserverp
Trusty 30404 8497 0 17:19 pts/9 00:00:00 ./echoserverp 3344
Trusty 30651 30404 0 17:19 pts/9 00:00:00 ./echoserverp 3344
Trusty 31174 30404 0 17:20 pts/9 00:00:00 ./echoserverp 3344
这里看到,在有3个client端连接时,存在3个echoserverp运行实例。
实现的关键在于:
- 使用信号, SIGCHLD用于回收僵死进程。
- Fork()函数创建子进程。
- 创建完子进程后,父进程需要关闭已经建立的socket连接。而子进程则需要关闭它的监听描述符。
优缺点比较: 父子进程共享文件表,但是不共享用户地址空间。使得一个进程不可能不小心覆盖到另一个进程的虚拟存储器。但是独立的地址空间使得进程共享状态信息变得困难,它们需要用IPC来显示通信。而且进程通常比较慢,因为进程控制和IPC的开销很高。IPC,进程间通信。 ###基于I/O多路复用的并发编程
/* $begin select */
#include "csapp.h"
void echo(int connfd);
void command(void);
int main(int argc, char **argv)
{
int listenfd, connfd, port;
socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
fd_set read_set, ready_set;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
listenfd = Open_listenfd(port); //line:conc:select:openlistenfd
FD_ZERO(&read_set); /* Clear read set */ //line:conc:select:clearreadset
FD_SET(STDIN_FILENO, &read_set); /* Add stdin to read set */ //line:conc:select:addstdin
FD_SET(listenfd, &read_set); /* Add listenfd to read set */ //line:conc:select:addlistenfd
while (1) {
ready_set = read_set;
Select(listenfd+1, &ready_set, NULL, NULL, NULL); //line:conc:select:select
if (FD_ISSET(STDIN_FILENO, &ready_set)) //line:conc:select:stdinready
command(); /* Read command line from stdin */
if (FD_ISSET(listenfd, &ready_set)) { //line:conc:select:listenfdready
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
echo(connfd); /* Echo client input until EOF */
Close(connfd);
}
}
}
void command(void) {
char buf[MAXLINE];
if (!Fgets(buf, MAXLINE, stdin))
exit(0); /* EOF */
printf("%s", buf); /* Process the input command */
}
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", n);
Rio_writen(connfd, buf, n);
}
}
/* $end select */
这个例子测试时需要注意的是,当客户端有连接时,终端输入将失效。一个更好的解决方案是使用更细粒度的多路复用,服务器每次循环回送一个文本行。