C/400

101. select 関数の本当の意味は ?

select 関数をマニュアルで調べると「複数ソケットのイベントの待機」と書かれており、
続いて「select 関数を使用すれば、アプリケーションによる入力の多重化を実現できる」と
書かれているが、ハッキリ言って意味不明であろう。
意味不明と解釈した人は、それで正しいのかも知れない。
一体、select 関数は、どのような目的のために使用するのだろうか ? と思った人も少なくはないはずだ。
ここでは select 関数の本質をわかりやすく説明する。

まず最初に「イベントの待機」ではなく、現実の業務で表現すると「着信の待機」である。
「入力の多重化」は、もうひとつ意味不明である。
「入力の多重化」ではなく「受信の条件を増やす」ことである。
「着信の待機」「受信の条件を増やす=タイマーを設定する」 の2つが select の機能である。

着信の待機

select 関数は listen や rcvmsg 関数のように受信するのを待ち続けることができる。

受信の条件を増やす: 例えばタイマーを設定する

無接続型の受信 : recvfrom 関数では、永遠に受信を待機することになるが
適用業務によっては,無接続型の受信であってもタイマーを指定して
ある一定の時間だけは待機するものの、一定時間の後には受信を終了させたい場合がある。

※ 無接続型受信とは

「無接続型受信」も IBM マニュアルには解説されていないので、ここで説明しておこう。
一般にTCP/IP通信とは、お互いに通信を開始してから送受信を行なうものであるが
場合によっては、まだ接続されていない相手からの受信を待機したいケースも出てくる。

例えば、あるジョブでは別のジョブからのデータが転送されてくるのを
ひたすら待機するものとする。
これは接続を待つのではなくデータを待つような場合である。
このようなケースでは、まだ接続されていない状態で受信を待機するのである。
このような受信待機を 「無接続型受信」 と呼ぶ。

recv 関数のように接続型受信であれば、接続されていることが前提であるので
非接続の状態で受信を recv 関数で待機しようとしても、直ちにエラーとなって終了してしまう。
これに対して 無接続型の recvfrom であれば接続されるまで待つことができる。
従って他のジョブからの転送を待機するのであれば recvfrom によって
接続されることも含めて待機することができる recvfrom の待機が適切である。
ところが recvfrom は永続待機であるので、受信が必ず期待できる場合は
良いのだが、時には受信が起こらないケースもあるのであれば
ジョブは終了することなく、その時点で永続待機の状態に陥ってしまう。
このような事態を避けるためにはタイマー機能が必要となる。
そこで登場するのが select 関数であり、recvfrom 関数の直前に select 関数を入れるのである。
select 関数であればタイマー機能を持っているので、一定の時間のあいだだけ
待機しておいて、時間中に受信があっても select は着信を検知するだけで
データは読まないのでデータの読み取りは recvfrom に任せることができる。
タイムアウトすれば、時間切れとして終了すればよい。

select関数、recvfrom関数の組み合わせ

つまり、無接続受信である recvfrom の機能にタイマー機能を select 関数によって
加えることができるのである。
このことを「入力の多重化」と表現しているのだが、「入力の多重化」では
とてもこのような機能までを推測することはできない。
select は後に続く受信の機能を拡張することができる、と言ったほうがわかりやすくなる。

もっと理解を深めるためにサンプル・ソースを以下に紹介する。

TESTSEL: タイマー機能によって無接続型 recvfrom で待機するTCP/IP受信のソース
0001.00 #include <stdio.h>                                                            
0002.00 #include <stdlib.h>                                                           
0003.00 #include <string.h>                                                           
0004.00 #include <errno.h>                                                            
0005.00 #include <sys/socket.h>                                                       
0006.00 #include <sys/types.h>                                                        
0007.00 #include <netinet/in.h>                                                       
0008.00 #include <netinet/tcp.h>                                                      
0009.00 #include <arpa/inet.h>                                                        
0010.00 #include <sys/time.h>                                                         
0011.00                                                                               
0012.00 #define TRUE         0                                                        
0013.00 #define FALSE       -1                                                        
0014.00 #define TCP_LEN      1492    /* TCP/IP 送受信長 */                            
0015.00 int    m_timeout  = 0;      /* IE 切断後の再 READ のタイムアウト秒 */         
0016.00                                                                               
0017.00 void main(void){                                                              
0018.00    int sockfd, on = 1, rc, port = 3010;                                       
0019.00    struct sockaddr_in iaddr;                                                  
0020.00    int    iaddrlen = sizeof(iaddr);                                           
0021.00    struct timeval timeout;                                                    
0022.00    fd_set read_fd;                                                            
0023.00    char buff[TCP_LEN+1];                                                      
0024.00                                                                                
0025.00    printf("** TESTSEL: select のテスト・サンプル  **\n");                      
0026.00    getchar();                                                                  
0027.00    m_timeout = 30; /* 30 秒のタイムアウト */                                   
0028.00    sockfd = socket(AF_INET, SOCK_DGRAM, 0);                                    
0029.00    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));      
0030.00    memset(&iaddr, 0, sizeof(struct sockaddr_in));                              
0031.00    iaddr.sin_port = htons(port);                                               
0032.00    iaddr.sin_family  = AF_INET;                                                
0033.00    iaddr.sin_addr.s_addr = htonl(INADDR_ANY);                                  
0034.00    bind(sockfd, (struct sockaddr*)&iaddr, sizeof(iaddr));                      
0035.00    FD_ZERO(&read_fd);                                                          
0036.00    FD_SET(sockfd, &read_fd);                                                   
0037.00    timeout.tv_sec = m_timeout;                                                 
0038.00    timeout.tv_usec = 0;                                                        
0039.00    printf("** select 待機中 **\n");                                            
0040.00    rc = select(sockfd+1, &read_fd, NULL, NULL, &timeout);                      
0041.00    if(rc < 0){/* select 失敗 */                                                
0042.00      fprintf(stderr, "%d:SELECT 失敗 :%s", __LINE__, strerror(errno));         
0043.00      close(sockfd);                                                            
0044.00      getchar();                                                                
0045.00      exit(-1);                                                                 
0046.00    }/* select 失敗 */                                                          
0047.00    if(FD_ISSET(sockfd, &read_fd)){                                             
0048.00    }                                                                              
0049.00    else{/* NODATA-TIMEOUT */                                                      
0050.00      printf("%d 秒の受信待機はタイムアウトで終了しました。 \n", m_timeout);       
0051.00      close(sockfd);                                                               
0052.00      getchar();                                                                   
0053.00      exit(-1);                                                                    
0054.00    }/* NODATA-TIMEOUT */                                                          
0055.00    printf("** recvfrom 開始 **\n");                                               
0056.00    rc = recvfrom(sockfd, buff, TCP_LEN, 0, (struct sockaddr*)&iaddr,              
0057.00                      &iaddrlen);                                                  
0058.00    if(rc < 0){/* recvfrom error */                                                
0059.00      fprintf(stderr, "%d:recvfrom error:%s\n", __LINE__,strerror(errno));         
0060.00    }/* recvfrom error */                                                          
0061.00    else{/* 正常に受信 */                                                          
0062.00    }/* 正常に受信 */                                                              
0063.00    printf("%d: %d バイトを受信しました。 \n", __LINE__, rc);                      
0064.00    close(sockfd);                                                                 
0065.00    getchar();                                                                     
0066.00    exit(0);                                                                       
0067.00                                                                                   
0068.00 }                                                                                 
【解説】
0031.00    iaddr.sin_port = htons(port);                                               
0032.00    iaddr.sin_family  = AF_INET;                                                
0033.00    iaddr.sin_addr.s_addr = htonl(INADDR_ANY);

によって指定したアドレス : iaddr を作成したソケット: sockfd に

0034.00    bind(sockfd, (struct sockaddr*)&iaddr, sizeof(iaddr));

結びつけておいてから

0035.00    FD_ZERO(&read_fd);

によって read_fd をゼロにセットして

0036.00    FD_SET(sockfd, &read_fd);

によって sockfd と read_fd を関連づけている。

タイマーを

0037.00    timeout.tv_sec = m_timeout;                                                 
0038.00    timeout.tv_usec = 0;       

によって設定しておいてから

0040.00    rc = select(sockfd+1, &read_fd, NULL, NULL, &timeout); 

によって select 関数を使って sockfd + 1 未満のソケット識別子までの着信を監視している。
ここで sockfd ではなく sockfd + 1 を指定しているのは、 sockfd + 1 より小さな値の
sockfd であれば、すべて着信を検査することができるからであることに注意。
FD_SET された複数個の sockfd の着信を同時に検査することができるのである。

結果として

0047.00    if(FD_ISSET(sockfd, &read_fd)){                                             
0048.00    } 

であれば、時間内に着信が検知されたことになる。

0049.00    else{/* NODATA-TIMEOUT */                                                      
0050.00      printf("%d 秒の受信待機はタイムアウトで終了しました。 \n", m_timeout);       
0051.00      close(sockfd);                                                               
0052.00      getchar();                                                                   
0053.00      exit(-1);                                                                    
0054.00    }/* NODATA-TIMEOUT */

はタイムアウトを意味している。

その後に、

0055.00    printf("** recvfrom 開始 **\n");                                               
0056.00    rc = recvfrom(sockfd, buff, TCP_LEN, 0, (struct sockaddr*)&iaddr,              
0057.00                      &iaddrlen);

によって recvfrom による無接続型受信を開始するのであるが、
recvfrom を単独で実行した場合は着信が無ければ単に永続待機になってしまう。
recvfrom にタイマー機能を待たせて受信機能を拡張するためにselect が使われたのである。

【参考】

select + recvfrom による受信待機は Alaska/7.0 のスマート・コネクションと呼ばれる
受信待機の機能に使われている。
スマート・コネクションとは、HTML に埋め込まれた、元の実行ジョブのジョブ番号に
必ず戻って再実行されることが保証される。
Alaskaq/7.0 で例え、他のジョブがブラウザからの要求を受け取った場合でも
select + recvfrom によって待機している元のジョブに要求データは戻されるような仕組みとなっている。
通信は切れているが、必ず元のジョブに復帰する、これが「スマート・コネクション」であり
IE8〜, FireFox, Opera, Chrome などのクロス・ブラウザによる「プロセス共有問題」を
非常にシンプルに解決している。