Chinaunix首页 | 论坛 | 博客
  • 博客访问: 303706
  • 博文数量: 21
  • 博客积分: 250
  • 博客等级: 二等列兵
  • 技术积分: 484
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-06 23:10
个人简介

程序猿

文章分类

全部博文(21)

文章存档

2016年(17)

2014年(3)

2013年(1)

分类: 网络与安全

2016-02-05 21:58:28

服务器并发处理客户端有两种常见的方式:
1. fork方式
每连上一个客户端就fork一个进程,由子进程和客户端通信。
2. IO复用方式
IO复用也被称为事件驱动,包括使用select,poll,epoll等函数。

我们分别来介绍一下。

fork方式

使用fork方式需要注意几点:
1. 创建新进程后,子进程会继承父进程所有已打开的文件描述符,所以子进程需要关闭不使用的文件描述符。
2. 子进程退出后,会向父进程发送SIGCHLD信号,如果父进程未处理,子进程会变成僵尸进程。
来重点看一下这个问题。
服务器主要代码如下:
  1. int main(void)
  2. {
  3.     int listenfd;
  4.     if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  5.         ERR_EXIT("socket error");

  6.     struct sockaddr_in servaddr;
  7.     memset(&servaddr, 0, sizeof(servaddr));
  8.     servaddr.sin_family = AF_INET;
  9.     servaddr.sin_port = htons(5188);
  10.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  11.     int on = 1;
  12.     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
  13.         ERR_EXIT("setsockopt error");

  14.     if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
  15.         ERR_EXIT("bind error");

  16.     if (listen(listenfd, 10) < 0)
  17.         ERR_EXIT("listen error");

  18.     struct sockaddr_in peeraddr;
  19.     socklen_t peerlen = sizeof(peeraddr);
  20.     int conn;

  21.     pid_t pid;

  22.     while (1) {
  23.         if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
  24.             ERR_EXIT("accept error");
  25.         printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

  26.         pid = fork();
  27.         if (pid == -1)
  28.             ERR_EXIT("fork error");
  29.         if (pid == 0) {
  30.             printf("client close \n");
  31.             close(listenfd);
  32.             do_service(conn);
  33.             exit(EXIT_SUCCESS);
  34.         } else {
  35.             close(conn);
  36.         }
  37.     }

  38.     return 0;
  39. }

  40. void do_service(int conn)
  41. {
  42.     char recvbuf[1024];
  43.     int size = 1024;
  44.     int total = 0;
  45.     while (1) {
  46.         memset(recvbuf, 0, size);
  47.         int ret = read(conn, recvbuf, size);
  48.         if (ret == 0) {
  49.             printf("client close\n");
  50.             break;
  51.         } else if (ret == -1)
  52.             ERR_EXIT("read error");
  53.         printf("len = %d\n", ret);
  54.         total += ret;
  55.         if (total >= size)
  56.             break;
  57.     }
  58.     write(conn, "ack\n", 4);
  59. }
运行服务器和客户端,客户端退出后,运行:
  1. # ps -aux|grep fork_server
  2. root 16664 0.0 0.0 4192 356 pts/7 S+ 15:20 0:00 ./fork_server
  3. root 16666 0.0 0.0 0 0 pts/7 Z+ 15:20 0:00 [fork_server] <defunct>
  4. root 16675 0.0 0.0 11740 940 pts/8 S+ 15:21 0:00 grep --color=auto fork_server
我们发现,有一个fork_server进程变成了僵尸进程(ps命令僵尸进程的标志为Z)。

这是因为,子进程退出时会向父进程发送SIGCHLD信号,父进程默认情况下不会处理该信号,子进程就成了僵尸进程。
要解决这个问题,最简单的方法是忽略SIGCHLD信号。
  1. signal(SIGCHLD, SIG_IGN);
把这句加到main()函数开头,再次运行程序,查看状态:
  1. # ps -aux|grep fork_server
  2. root 16698 0.0 0.0 4196 352 pts/7 S+ 15:22 0:00 ./fork_server
  3. root 16704 0.0 0.0 11740 940 pts/8 S+ 15:22 0:00 grep --color=auto fork_server
这次不存在僵尸进程了。

如果不想忽略该信号,则需要在信号处理函数中调用wait()之类的函数给子进程“收尸”。注意,信号不能排队,如果多个子进程同时退出,父进程只会收到一个SIGCHLD信号。而wait()函数只能等待一个子进程退出,这就会造成有些进程无法被“收尸”。所以必须使用waitpid()函数循环等待子进程,并判断返回值来决定是否处理完全部子进程。示例代码如下:
  1. void handler(int sig)
  2. {
  3.     while (waitpid(-1, NULL, WNOHANG) > 0);
  4. }

IO复用方式

我们以select()函数为例。
  1. int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);

先简单介绍一下函数参数,参数中,readfds,writefds,errorfds分别为需要监听读、写、错误写状态的描述符集合,maxfd设置为集合中描述符最大值加1。timeout为超时时间,设置NULL表示阻塞,设置0表示立刻返回,其他值就是等待对应的时间,没有任何监听状态改变则返回。

使用该函数时,把需要监听的socket描述符添加到集合中,一旦集合中有描述符发生变化,select就会返回,集合中未发生改变的描述符清0。上层需要查询描述符集合,值为1的描述符就是状态发生改变的描述符。

代码如下:
  1. int main(void)
  2. {
  3.     int listenfd;
  4.     if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
  5.         ERR_EXIT("socket error");

  6.     struct sockaddr_in servaddr;
  7.     memset(&servaddr, 0, sizeof(servaddr));
  8.     servaddr.sin_family = AF_INET;
  9.     servaddr.sin_port = htons(5188);
  10.     servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

  11.     int on = 1;
  12.     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
  13.         ERR_EXIT("setsockopt error");

  14.     if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
  15.         ERR_EXIT("bind error");

  16.     if (listen(listenfd, SOMAXCONN) < 0)
  17.         ERR_EXIT("listen error");

  18.     struct sockaddr_in peeraddr;
  19.     socklen_t peerlen = sizeof(peeraddr);

  20.     int conn;
  21.     int i;
  22.     int client[FD_SETSIZE];
  23.     int maxi = 0;
  24.     for (i = 0; i < FD_SETSIZE; i++)
  25.         client[i] = -1;

  26.     int nready;
  27.     int maxfd = listenfd;
  28.     fd_set rset;
  29.     fd_set allset;
  30.     FD_ZERO(&rset);
  31.     FD_ZERO(&allset);
  32.     FD_SET(listenfd, &allset);

  33.     while (1) {
  34.         rset = allset;
  35.         nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
  36.         if (nready == -1) {
  37.             if (errno == EINTR)
  38.                 continue;
  39.             ERR_EXIT("select error");
  40.         }

  41.         if (nready == 0)
  42.             continue;

  43.         if (FD_ISSET(listenfd, &rset)) {
  44.             conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
  45.             if (conn == -1)
  46.                 ERR_EXIT("accept error");

  47.             for (i = 0; i < FD_SETSIZE; i++) {
  48.                 if (client[i] < 0) {
  49.                     client[i] = conn;
  50.                     if (i > maxi)
  51.                         maxi = i;
  52.                     break;
  53.                 }
  54.             }

  55.             if (i == FD_SETSIZE) {
  56.                 fprintf(stderr, "too many clients\n");
  57.                 exit(EXIT_FAILURE);
  58.             }

  59.             printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
  60.                     ntohs(peeraddr.sin_port));

  61.             FD_SET(conn, &allset);
  62.             if (conn > maxfd)
  63.                 maxfd = conn;

  64.             if (--nready <= 0)
  65.                 continue;
  66.         }

  67.         for (i = 0; i <= maxi; i++) {
  68.             conn = client[i];
  69.             if (conn == -1)
  70.                 continue;

  71.             if (FD_ISSET(conn, &rset)) {
  72.                 printf("client close \n");
  73.                 do_service(conn);
  74.                 close(conn);
  75.                 FD_CLR(conn, &allset);
  76.                 client[i] = -1;

  77.                 if (--nready <= 0)
  78.                     break;
  79.             }
  80.         }
  81.     }

  82.     return 0;
  83. }

  84. void do_service(int conn)
  85. {
  86.     char recvbuf[1024];
  87.     int size = 1024;
  88.     int total = 0;
  89.     while (1) {
  90.         memset(recvbuf, 0, size);
  91.         int ret = read(conn, recvbuf, size);
  92.         if (ret == 0) {
  93.             printf("client close\n");
  94.             break;
  95.         } else if (ret == -1)
  96.             ERR_EXIT("read error");
  97.         printf("len = %d\n", ret);
  98.         total += ret;
  99.         if (total >= size)
  100.             break;
  101.     }
  102.     write(conn, "ack\n", 4);
  103. }

使用select需要注意一些问题。
1. select返回后,还是不能使用阻塞IO操作。man 2 select 能看到这样一段话:
Under Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless a subsequent read blocks.  This could for example happen when data has arrived but upon examination has wrong checksum and is discarded.  There may be other circumstances in which a file descriptor is spuriously reported as ready.  Thus it may be safer to use O_NONBLOCK on sockets that should not block.
也就是说,select返回并不一定代表能读写socket,所以socket还是需要设置为非阻塞的。

2. select的最大并发数受以下影响:
a. 一个进程能打开的最大文件数,可以通过ulimit -n查看;
b. 一个系统能打开的最大文件数,可以通过cat /proc/sys/fs/file-max 查看;
c. FD_SETSIZE(fd_set)的限制,需要重新编译内核 ,但是会导致内核效率减小。poll和epoll没有该限制。


文中涉及到的代码可以在下载执行。


参考资料:
http://blog.csdn.net/jnu_simba/article/details/9034407

阅读(3609) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~