深度理解Unix/linux系列中Select()模型[中英对照]

翻译文章,为了自己更好地理解。英文原文贴在http://www.doserver.net/post/linux-unix-select-model.php ,吉林大学胡章优博士的个人主页。


THE WORLD OF SELECT()
So just why am I so hyped on select()?

One traditional way to write network servers is to have the main server block on accept(), waiting for a connection. Once a connection comes in, the server fork()s, the child process handles the connection and the main server is able to service new incoming requests.

With select(), instead of having a process for each request, there is usually only one process that "multi-plexes" all requests, servicing each request as much as it can.

So one main advantage of using select() is that your server will only require a single process to handle all requests. Thus, your server will not need shared memory or synchronization primitives for different 'tasks' to communicate.

One major disadvantage of using select(), is that your server cannot act like there's only one client, like with a fork()'ing solution. For example, with a fork()'ing solution, after the server fork()s, the child process works with the client as if there was only one client in the universe -- the child does not have to worry about new incoming connections or the existence of other sockets. With select(), the programming isn't as transparent.


select()的世界--为什么我回对select()如此狂热?

        在编写网络服务器程序时,一种传统的方法是将主要的服务代码放在accept()区块里面,等待连接到来。一旦有连接到达,服务器将调用fork()函数生成一个子进程处理本次连接,随后主服务就可以接受新的连接请求。

        有了select()之后,服务器就不必为每个连接创建一个进程了,而是可以在同一进程中处理全部连接请求,实现多路复用,并且竭尽所能地为每个连接服务。

        这样看来select()的最主要优点在于服务器只需要向系统申请一个进程就可以处理全部连接请求,以使得你的服务器摆脱内存共享或者不同任务间的同步原语通信。

        然而,select()也有缺点,最主要的一项是服务器不能在“只有一个客户端”的假设中运行,像fork()那样解决问题。例如说,服务器调用fork()后子进程处理客户端时就好像全天下只存在这一个客户端,它不必担心后来的客户端连接请求或者现有的套接字。select()可不会像这样透明化对待每个套接字。


Okay, so how do you use select()?

select() works by blocking until something happens on a file descriptor (aka a socket). What's 'something'? Data coming in or being able to write to a file descriptor -- you tell select() what you want to be woken up by. How do you tell it? You fill up a fd_set structure with some macros.

Most select()-based servers look pretty much the same:


那么,select()怎么使用呢?

        select()的工作方式是阻塞直到文件描述符(也就是socket)上指定的事件触发。哪些事件呢?数据送达或者已准备好写入文件描述符,这些都有你来告诉select()想被什么事件触发。怎么告诉它?用宏指令填写fd_set结构体。

        大多数基于select()函数的服务器有十分相似的结构:

Fill up a fd_set structure with the file descriptors you want to know when data comes in on.
用当有数据到达时你感兴趣的文件描述符填写fd_set
Fill up a fd_set structure with the file descriptors you want to know when you can write on.
用能写入时你感兴趣的文件描述符填写fd_set

Call select() and block until something happens.调用select()并挂直到指定事件发生Once select() returns, check to see if any of your file descriptors was the reason you woke up. If so, 'service' that file descriptor in whatever particular way your server needs to (i.e. read
 in a request for a Web page).一旦select()有返回值,检查文件描述符,是否指定事件触发。 如果没错,那么就开始使用各种方法为此文件描述符服务。Repeat this process forever. 重复以上过程。

Quit with the pseudo-code, show me some real code!Okay, let's take a look at a sample server included with Vic Metcalfe's Socket Programming FAQ (my comments are in red):

        看完了伪代码,下面我们要看看真家伙!

        好的,让我们看一个 Vic Metcalfe's Socket Programming FAQ上的简单的服务器程序源代码(我的注释以红色标记)

/*  
   PLEASE READ THE FILE NB-APOLOGY!!!!  There are some things you should 
   know about this source before you read it.  Thanks. 
   请阅读文档NB-APOLOGY (http://www.lowtek.com/sockets/NB-APOLOGY.txt),里面有一些东西是你在阅读代码之前需要了解的。谢谢!
    
   Quang Ngo alerted me to a bug where the variable listnum in deal_with_data() 
   wasn't being passed in by parameter, thus it was always garbage. I have 
   quick-fixed this in the code below. - Spencer (October 12, 1999) 
   Quang Ngo筒子告诫我说 deal_with_data()函数里面的listnum变量没有被传入参数,因此将永远成为垃圾。我立即修复了这个错误。-Spencer(1999.10.12)
 
   Non blocking server demo  
   By Vic Metcalfe (vic@acm.org) 
   For the unix-socket-faq 

   非阻塞服务器演示程序
   作者:Vic Metcalfe (vic@acm.org)
   为unix-socket-faq而写
 */  
  
#include "sockhelp.h"  
#include <ctype.h>  
#include <sys/time.h>  
#include <fcntl.h>  
  
int sock;            /* 我们正在监听的文件描述符 */  
int connectlist[5];  /* 已连接上的socket数组,我们通过它知道在和谁通信 */  
fd_set socks;        /* 我们想通过select()使其生效的socket文件描述符*/  
int highsock;       /* select()函数需要的最大文件描述符参数 */  
  
void setnonblocking(sock)  
int sock;  
{  
  int opts;  
  
  opts = fcntl(sock,F_GETFL);  
  if (opts < 0) {  
    perror("fcntl(F_GETFL)");  
    exit(EXIT_FAILURE);  
  }  
  opts = (opts | O_NONBLOCK);  
  if (fcntl(sock,F_SETFL,opts) < 0) {  
    perror("fcntl(F_SETFL)");  
    exit(EXIT_FAILURE);  
  }  
  return;  
}

void build_select_list() {  
  int listnum;       /* connectlist中正在for循环中的那一个文件描述符 */  
      
  /* 首先将fd_set与select()放在一块儿,那里会包含sock变量以应对新来的连接,将所有已连接的sockets相加 */  
        
        
  /* FD_ZERO()函数清空fd_set调用的套接字,这样其中将不含有任何文件描述符 */  
        
  FD_ZERO(&socks);  
        
  /* FD_SET()函数将“sock”与fd_set相加,当有新的连接请求到达时select()将返回一个参数(意味着你必须accept()等等)*/  
        
  FD_SET(sock,&socks);  
        
  /* 循环遍历所有可能存在的连接并且把他们都加到fd_set上面*/  
        
  for (listnum = 0; listnum < 5; listnum++) {  
    if (connectlist[listnum] != 0) {  
      FD_SET(connectlist[listnum],&socks);  
      if (connectlist[listnum] > highsock)  
        highsock = connectlist[listnum];  
    }  
  }  
}  

void handle_new_connection() {  
  int listnum;       /* connectlist中正在for循环中的那一个文件描述符 */  
  int connection; /* 新来连接的socket文件描述符*/  
  
  /* 新连接到达!我们要在connectlist中为其寻找一个存放空间 */  
  connection = accept(sock, NULL, NULL);  
  if (connection < 0) {  
    perror("accept");  
    exit(EXIT_FAILURE);  
  }  
  setnonblocking(connection);  
  for (listnum = 0; (listnum < 5) && (connection != -1); listnum ++)  
    if (connectlist[listnum] == 0) {  
      printf("\nConnection accepted:   FD=%d; Slot=%d\n",  
        connection,listnum);  
      connectlist[listnum] = connection;  
      connection = -1;  
    }  
  if (connection != -1) {  
    /* No room left in the queue! */  
    printf("\nNo room left for new client.\n");  
    sock_puts(connection,"Sorry, this server is too busy.  "  
          Try again later!\r\n");  
    close(connection);  
  }  
}

void deal_with_data(  
  int listnum      /* connectlist中正在for循环中的那一个文件描述符 */  
  ) {  
  char buffer[80];     /* socket读入缓冲区*/  
  char *cur_char;      /* 处理时使用的缓冲 */  
  
  if (sock_gets(connectlist[listnum],buffer,80) < 0) {  
    /* 连接已关闭,关闭本端释放connectlist区域*/  
    printf("\nConnection lost: FD=%d;  Slot=%d\n",  
      connectlist[listnum],listnum);  
    close(connectlist[listnum]);  
    connectlist[listnum] = 0;  
  } else {  
    /* 接收到数据,将数据转换为大写然后送回客户端 */  
    printf("\nReceived: %s; ",buffer);  
    cur_char = buffer;  
    while (cur_char[0] != 0) {  
      cur_char[0] = toupper(cur_char[0]);  
      cur_char++;  
    }  
    sock_puts(connectlist[listnum],buffer);  
    sock_puts(connectlist[listnum],"\n");  
    printf("responded: %s\n",buffer);  
  }  
}

void read_socks() {  
  int listnum;       /* connectlist中正在for循环中的那一个文件描述符 */  
  
  /* 任何socket准备好读入后sock将会被置位,首先我们检查监听socket,然后检查connectlist中的socket*/  
    
  /* 如果客户端尝试使用connect()连接至正在监听的socket,select()会认为这个socket是可读的。
     因此,如果正在监听的socket是fd_set的一员,我们要接受这个新连接。*/  
    
  if (FD_ISSET(sock,&socks))  
    handle_new_connection();  
  /*检查connectlist是否有有效数据*/  
    
  /*循环遍历socket,检查是否有事件触发他们,如果有,开始服务*/  
    
  for (listnum = 0; listnum < 5; listnum++) {  
    if (FD_ISSET(connectlist[listnum],&socks))  
      deal_with_data(listnum);  
  } /*for循环(全部按队列进入)*/  
}

int main (argc, argv)   
int argc;  
char *argv[];  
{  
  char *ascport;  /* 端口号的ASCII码*/  
  int port;       /* ACII码转换后的端口号*/  
  struct sockaddr_in server_address; /* 定义绑定信息的结构体*/  
  int reuse_addr = 1;  /* 设置此参数以使得我们可以在之前连接TIME_WAIT状态下重绑定端口*/  
  struct timeval timeout;  /* select的超时参数*/  
  int readsocks;       /* 准备好读入的socket数*/  
      
  /* 确保我们从输入指令获得了一个端口号参数*/  
  if (argc < 2) {  
    printf("Usage: %s PORT\r\n",argv[0]);  
    exit(EXIT_FAILURE);     
  }  
      
      /* 为将要监听的socket获取文件描述符*/  
  sock = socket(AF_INET, SOCK_STREAM, 0);  
  if (sock < 0) {  
    perror("socket");  
    exit(EXIT_FAILURE);  
  }  
  /* 这样我们可以不用考虑TIME_WAIT状态重绑定端口*/  
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(reuse_addr));  
      
  /* 用sernonblocking设置socket为非阻塞*/  
  setnonblocking(sock);  
      
  /* 获取地址信息,并将其绑定至socket*/  
  ascport = argv[1]; /* 从输入参数读入端口号 */  
  port = atoport(ascport); /* 用sockhelp头文件中的函数将端口号转换为int型*/  
  memset((char *) &server_address, 0, sizeof(server_address));  
  server_address.sin_family = AF_INET;  
  server_address.sin_addr.s_addr = htonl(INADDR_ANY);  
  server_address.sin_port = port;  
  if (bind(sock, (struct sockaddr *) &server_address, sizeof(server_address)) < 0 ) {  
    perror("bind");  
    close(sock);  
    exit(EXIT_FAILURE);  
  }  
      
  /* 为新到连接建立队列*/  
  listen(sock,5);  
      
  /* 因为我们开始时仅处理一个socket,因此目前正在监听的socket是最高序号的那个 */  
  highsock = sock;  
  memset((char *) &connectlist, 0, sizeof(connectlist));  
      
  while (1) { /* 永远循环的主服务代码 */  
    build_select_list();  
    timeout.tv_sec = 1;  
    timeout.tv_usec = 0;  
          
    /* select()的第一个参数是最高文件描述符序号+1,其实大多情况下传入FD_SETSIZE即可*/  
            
    /* select()的第二个参数是包含我们将要读取的socket的fd_set地址(包括正在监听的socket)*/  
            
    /* select()的第三个参数是你想要知道是否可以写入的fd_set,此例中并未用到,因此设置为0或NULL。*/  
    /* select()的第四个参数是正在等待的OOB数据(带外数据)sockets,通常,不使用*/
          
    /* select()的最后一个参数是阻塞超时时长。如果希望永远等待直到被事件触发,则传入NULL*/  
          
    readsocks = select(highsock+1, &socks, (fd_set *) 0, (fd_set *) 0, &timeout);  
          
    /* select()返回有事件发生的socket,例如可读的socket*/  
            
     /* 一旦select()有返回参数,原始fd_set将被改变以表示为何select()被触发。
       例如:4号文件描述符本来不在fd_set,当它变为可读状态后,fd_set将包含它*/  
          
    if (readsocks < 0) {  
      perror("select");  
      exit(EXIT_FAILURE);  
    }  
    if (readsocks == 0) {  
      /* 没有数据可读,用“.”号表示*/  
      printf(".");  
      fflush(stdout);  
    } else  
      read_socks();  
  } /* while(1) */  
} /* main */  



Okay, so maybe that wasn't the best example...

好吧,也许这并不是最好的例程

Got some suggests? Corrections? Please let me know.

如果你对以上代码有任何建议或修正意见,请让我知道。

In the mean time, here's some references to other sources of information about select():

同时,这里有一些关于select()模型的参考资料

Socket FAQ Question about select()
Straight from the excellent socket FAQ.

通往极棒的socket FAQ

Unix Programming FAQ Question about select()
Straight from the other excellent FAQ.

通往另一个极棒的socket FAQ

Unix Socket Programming FAQ Examples
The above nbserver.c sample code and more socket stuff.

以上nbserver.c源代码以及更多socket例程

thttpd - tiny/turbo/throttling HTTP server
小巧的单线程, 非fork(), 基于select()的Web服务器.

BOA
Another single-threaded, select() based Web server.

另一个单线程, 基于select()的Web服务器.









  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值