Linux TCP 服务器编程(四):I/O复用

时间:2022-08-19 17:57:49

【版权声明:转载请保留出处:blog.csdn.net/gentleliu。邮箱:shallnew*163.com】

前面我们的服务器使用多进程来实现并发,但是这样会有一个问题,就是在同时有很多客户端连接时,服务器在每一个客户连接时就会新建一个进程,最后会产生很多进程,这样会占用很多系统资源。下面我们来实现一个单进程服务器处理多个客户端连接的程序。这涉及到I/O复用。进程需要一种预先告知内核的能力,当内核发现进程指定的一个或多个I/O条件就绪,他就会通知进程,这个能力称为I/O 复用。

我们先说select系统调用,select系统调用是用来让我们的程序监视多个文件描述符状态变化的。程序会停在select这里等待,直到被监视的文件句柄有某一个或多个发生了状态改变。
select函数原型如下:

  #include <sys/select.h>
  #include <sys/time.h>
  int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);

参数nfds指定被监视描述符个数,值为被监视的最大描述符加1,也就是说描述符0,1,2,...,nfds-1都将被监视。

中间三个参数指定我们要让内核测试读,写,异常条件的描述符。对这三个变量的赋值要使用到线面4个宏:

  void FD_ZERO(fd_set *fdset)//将该描述符集清
  void FD_SET(int fd, fd_set *fdset);//将要监视fd,加入描述符集合
  void FD_CLR(int fd, fd_set *fdset);//从描述符集合中删去被监视的fd,不再监视该描述符。
  int FD_ISSET(int fd, fd_set *fdset);//检测被监视的描述符fd,看是否一已经就绪。

现在我们主要用到第一个参数,主要监视是否有可读的描述符。

最后一个timeout参数是等待描述符集合中任意一个描述符就绪可花的时间,在这段时间内,如果描述符没有就绪select就直接返回。参数为空指针时表示select一直等下去,知道某个描述符就绪。当指定该值之后就将等待该值长度的时间,但是如果将该值设置为0时,将不会等待,这是就成了轮询了。

select函数返回就绪描述符的个数。

  现在我们使用select来实现一下单进程处理多个客户端连接的服务器。我们将每一个客户端连接套接字保存起来并加入到被监视描述符集中,将每一个处理完成的客户端套接字从集合中删除。

  服务器断代码如下(参考了unp):

  #include <stdio.h>
  #include <unistd.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <stdlib.h>
  #include <arpa/inet.h>
  #include <netinet/in.h>
  #include <sys/wait.h>
  #include <signal.h>
  #include <sys/time.h>
  
  #include "server.h"
  #include "ly_debug.h"
  
  void accept_handle(int connfd, struct sockaddr_in peeraddr);
  void sig_chld();
  
  int main(int argc, const char *argv[])
  {
   int sockfd, connfd, maxfd, fd;
   struct sockaddr_in seraddr, peeraddr;
   socklen_t addrlen;
   int i, clt[FD_SETSIZE], maxi, ready;
   fd_set allset, rset;
   struct timeval timeout;
   char buf[32];
  
   COMPILE_TIME_PRINT;
  
   if (argc != 2) {
   printf("Usage: %s <port>\n", argv[0]);
   return -1;
   }
   sockfd = socket(AF_INET, SOCK_STREAM, 0);
   if (sockfd < 0) {
   LY_ERR("socket: %s\n", strerror(errno));
   return -1;
   }
  
   seraddr.sin_family = AF_INET;
   seraddr.sin_port = ntohs(atoi(argv[1]));
   seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  
   if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr))) {
   LY_ERR("bind: %s\n", strerror(errno));
   return -1;
   }
  
   if (listen(sockfd, MAX_CONN) < 0) {
   LY_ERR("listen: %s\n", strerror(errno));
   return -1;
   }
  
   timeout.tv_sec = 0;
   timeout.tv_usec = 1000;
  
   maxfd = sockfd;
   for (i = 0; i < FD_SETSIZE; i++) {
   clt[i] = -1;
   }
   maxi = -1;
   FD_ZERO(&allset);
   FD_SET(sockfd, &allset);
   for (;;) {
   rset = allset;
   if ((ready = select(maxfd+1, &rset, NULL, NULL, &timeout)) < 0) {
   LY_ERR("select: %s\n", strerror(errno));
   continue;
   }
   if (0 == ready) {
   continue;
   }
  
   if (FD_ISSET(sockfd, &rset)) {
   LY_DBG("===fd:%d ready=======\n", sockfd);
   connfd = accept(sockfd, (struct sockaddr *)(&peeraddr), &addrlen);
   if (connfd < 0) {
   LY_ERR("accept: %s\n", strerror(errno));
   continue;
   }
  
   LY_IFO("Receive request from %s, port %d\n",
   inet_ntop(AF_INET, &peeraddr.sin_addr, buf, sizeof(buf)),
   ntohs(peeraddr.sin_port));
  
   for (i = 0; i < FD_SETSIZE; i++) {
   if (clt[i] < 0) {
   clt[i] = connfd;
   LY_DBG("add connfd: %d, i=%d\n", connfd, i);
   break;
   }
   }
   if (i == FD_SETSIZE) {
   LY_ERR("Too many clients!\n");
   }
   FD_SET(connfd, &allset);
   if (connfd > maxfd) {
   maxfd = connfd;
   }
   if (maxi < i) {
   maxi = i;
   }
   if (--ready <= 0) {
   continue;
   }
   }
  
   for (i = 0; i <= maxi; i++) {
   fd = clt[i];
   if (fd < 0) {
   continue;
   }
   if (FD_ISSET(fd, &rset)) {
   LY_DBG("===fd:%d ready==maxi:%d==i:%d===\n", fd, maxi, i);
   accept_handle(fd, peeraddr);
   close(fd);
   FD_CLR(fd, &allset);
   LY_DBG("clear connfd: %d\n", fd);
   clt[i] = -1;
   if (--ready <= 0) {
   break;
   }
   }
   }
   }
  
   return 0;
  }
  
  void accept_handle(int connfd, struct sockaddr_in peeraddr)
  {
   char buf[32];
   int val;
  
   memset(buf, 0, sizeof(buf));
   if (recv(connfd, buf, sizeof(buf), 0) < 0) {
   LY_ERR("recv: %s\n", strerror(errno));
   return;
   }
  
   sleep(20);
   val = atoi(buf);
   val *= val;
   snprintf(buf, sizeof(buf), "%d", val);
   if (send(connfd, buf, strlen(buf), 0) < 0) {
   LY_ERR("send: %s\n", strerror(errno));
   return;
   }
  }
如果我们让服务器每处理一个客户端连接是sleep10秒,模拟一下耗时操作,发现在服务器在处理每一个客户端连接时是串行在处理,这样肯定不行的啊。所以服务器在处理多个客户连接时,不能阻塞于某一个客户的处理或与其相关的某个函数的调用,否则会导致服务器被挂起,拒绝为其他所有客户服务,这就是所谓的拒绝服务型攻击,他就是对服务器做某些动作,导致服务器不能为其他合法客户服务。解决办法有:(1)使用非阻塞I/O;(2)为每个客户建立一个单独的线程或进程来处理客户需求(此时并非在连接建立时创建,而是在做实际的处理时才创建);(3)为I/O设置一个超时时间。这些我们会在后面章节进行处理。

    大家在开发服务器过程中,要关闭服务器进程做些调试改动,但是当你下一次再启动时,提示“Address already in use”,过几个分钟再试好像又可以了。这个大家应该都有遇到过,这个是怎么引起的呢,这个涉及到设置套接字选项,下一节我们再详细介绍。这一节到此结束。