TCP/IP ネットワーク プログラミング 第 12 章: I/O 多重化

I/O多重化に基づくサーバー側

サーバー側マルチプロセスのデメリットと解決策

並行サーバーを構築するには、クライアント接続要求があるたびに新しいプロセスが作成されます。これは確かに実際に使用されているソリューションですが、プロセスを作成する際に多大な代償を払う必要があるため、完璧ではありません。これには多くのコンピューティングとメモリ空間が必要であり、各プロセスが独立したメモリ空間を持っているため、相互間のデータ交換にも比較的複雑な方法が必要です (IPC は比較的複雑な通信方法です)。また、IPC が必要になると、プログラミングの難易度が上がることも感じられるはずです。「解決策は何でしょうか? プロセスを作成せずに複数のクライアントに同時にサービスを提供することは可能でしょうか?」


もちろん!この節で説明する I/O 多重化もそのような技術です。このようなアプローチがあると聞いて皆さんは興奮していますか? ただし、このモデルにあまり依存しないでください。この解決策はすべての状況に適用できるわけではなく、対象サーバーの特性に応じて異なる実装方法を採用する必要があります。まずは「多重化」の意味を理解しましょう。

再利用を理解する

ネットワーク プログラミングでは、多重化とは、物理通信リンク (ネットワーク伝送媒体など) 上で複数の独立したデータ ストリームを同時に伝送することを指します。複数のデータ ストリームを 1 つのストリームに結合し、受信側でそれらを分解することで、ネットワーク リソースの効率的な使用が向上します。

多重化技術はいくつかの方法で実装できます。

  1. 時分割多重 (TDM): 時間がいくつかの間隔に分割され、各間隔が送信用の異なるデータ ストリームに割り当てられます。送信側は一定の規則に従って時間間隔ごとにデータを送信し、受信側はその間隔に従ってデータを抽出して復元します。

  2. 周波数分割多重 (FDM): 周波数範囲は複数の狭帯域チャネルに分割され、それぞれがデータ ストリームの送信専用になります。データ ストリームは変調された後、さまざまな周波数で送信され、受信側で信号を復調して元のデータを取得します。

  3. コード分​​割多重 (CDM): 各データ ストリームを区別するために、異なるコード シーケンスが使用されます。送信側は特定のコード シーケンスを使用してデータを拡散し、受信側は同じコード シーケンスを逆拡散に使用して、データ ストリームを分離します。

これらの多重化技術は、特に、同じ物理通信リンク上で複数のデータ ストリームの送信を実現し、ネットワークの帯域幅利用率と送信効率を向上させることを目的としています。ネットワーク プログラミングでは、さまざまな多重化技術を使用して、複数のクライアント要求を同時に処理したり、単一の接続で複数のデータ ストリームを送信したりできます。

サーバー側での多重化技術の適用

ネットワークプログラミングにおけるIO多重化(IO Multiplexing)において、複数のIOイベントを効率的に処理するための仕組みです。従来の IO モデルでは、各 IO 操作によってスレッドがブロックされるため、プログラムは 1 つの IO を処理するときに他の IO イベントを同時に処理できなくなり、リソースが無駄になります。

IO 多重化では、特定のシステム コール関数 (select、poll、epoll など) を使用して複数の IO イベントを監視し、複数の IO 操作を 1 つのスレッドで管理および処理することで、複数の IO イベントを同時に処理する機能を実現します。その基本原理は、監視する必要がある IO イベントをイベント コレクションに追加し、システム コールを通じていずれかのイベントが準備完了になるまでブロックして待機することです。準備完了のイベントが存在すると、プログラムは対応する操作を実行できます。

IO 多重化の主な利点は次のとおりです。

  1. 高いリソース使用率: IO 多重化を使用すると、各 IO 操作がスレッドをブロックするのを防ぐことができるため、スレッドの数が減り、リソースの使用効率が向上します。

  2. 高速応答: IO 多重化により複数の IO イベントを同時に監視でき、イベントの準備が整うとすぐに処理されるため、イベント応答の遅延が大幅に短縮されます。

  3. シンプルなプログラミング: マルチスレッドまたはマルチプロセス モデルと比較して、IO 多重化を使用するとコードが簡素化され、開発とメンテナンスの困難さが軽減されます。

全体として、IO 多重化は複数の IO イベントを効率的に処理するためのメカニズムであり、スレッド数を削減し、リソース使用率と応答速度を向上させることができ、ネットワーク プログラミングで一般的に使用されるテクノロジの 1 つです。

IO 多重化サーバー側を理解するために別の例を挙げますと、教室に 10 人の生徒と 1 人の教師がいますが、この子供たちは暇人ではなく、授業中に質問をし続けます。学校は生徒一人に対して一人の教師を割り当てざるを得ません。つまり、現在教室には 10 人の教師がいます。その後、転校生がいる限り、先生が 1 名追加されます。転校生も質問が好きなためです。この話では、生徒をクライアント、教師をクライアントとのデータ交換のサーバー側プロセスとみなすと、教室の動作モードはマルチプロセスサーバーサイド方式となります。


ある日、学校に超能力を持つ教師がやって来た。この教師は、生徒を待たせることなく、すべての生徒の質問にすぐに対応します。そこで、教師の効率を高めるために、学校は他の教師を他のクラスに異動させた。現在、生徒は質問する前に挙手する必要があり、教師は生徒の質問を確認してから質問に答えます。つまり、現在の教室は IO 多重化モードで動作しています。
ちょっと変わった例ですが、教師は手を挙げた生徒がいるかどうかを確認する必要があるのと同様に、IO多重化サーバー側の処理も挙手したソケット(受信データ)を確認し、挙手したソケットからデータを受信する必要があることから、IO多重化の仕組みが理解できると思います。

select関数を理解してサーバーサイドを実装する

select 関数の使用は、サーバー側で多重化を実現する最も代表的な方法です。Windowsプラットフォーム上にも同名の関数が存在し、同様の機能を提供する
ため、移植性に優れています。

select関数の関数と呼び出し順序

sclect 機能を使用すると、複数のファイルディスクリプタをまとめて一元的に監視することができます。
□データを受信するソケットはありますか?
□データ送信をブロックする必要のないソケットは何ですか?
□どのソケットが異常ですか?

select関数の使い方は一般的な関数とは大きく異なり、正確に言うと使い方が難しいです。ただし、サーバー側でIO多重化を実現するには、select関数をマスターしてソケットプログラミングに適用する必要があります。「セレクト機能こそがIO多重化の中身である」と言っても過言ではありません。次にselect関数の呼び出し方法と順番を紹介します。

ステップ 1:
ファイル記述子の設定
、監視範囲の指定
、タイムアウトの設定
ステップ 2:
select 関数の呼び出し
ステップ 3:
呼び出し結果の表示

select 関数を呼び出す前にいくつかの準備作業が必要であり、呼び出し後に結果を確認する必要があることがわかります。次に、上記の順序で一つずつ説明していきます。

ファイル記述子の設定

複数のファイル記述子を同時に監視するには、select 関数を使用します。もちろん、監視ファイルディスクリプタを監視ソケットとみなすこともできますが、このとき、まず監視対象となるファイルディスクリプタを集める必要があります。集中する場合には、監視項目(受信、送信、異常)による区別、すなわち上記3つの監視項目に応じて3つのカテゴリーに分けることも必要である。


これは、図に示すように fd_set 配列変数を使用して実行します。配列は、0 と 1 を含むビットの配列です。

 図の左端のビットは、ファイル記述子 0 (それが配置されている場所) を表します。このビットが 1 に設定されている場合、ファイルディスクリプタが
監視対象であることを示します。では、図中のどのファイルディスクリプタが監視対象となるのでしょうか?明らかに、ファイル記述子 1 と 3 です。
「ファイル記述子の番号によって、値を直接 fd_set 変数に登録する必要がありますか?」
もちろん、そうではありません。fd_set 変数の演算はビット単位で行われるため、
変数を直接操作するのは面倒です。自分で行う必要がありますか? 実際、fd_set変数への値の登録・変更操作は以下のマクロで完了します。

□FD_ZERO(fd_set *fdset): fd_set変数の全ビットを0に初期化します。
□ FD_SET(int fd, fd_set *fdset): パラメータ fdset が指す変数にファイルディスクリプタ情報を登録します。

□ FD_CLR(int fd, fd_set *fdset): パラメータ fdset が指す変数からファイル記述子情報をクリアします。

□ FD_ISSET(int fd, fd_set *fdset): パラメータ fdset が指す変数にファイル記述子の情報が含まれている場合、"true" を返します。
上記関数では、select 関数の呼び出し結果を確認するために FD_ISSET を使用しています。

検査(監視)範囲とタイムアウトを設定する

まずはselect関数について簡単に紹介します。

#include<sys/select.h>
#include<sys/time.h>

int select(int maxfd,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval *timeout);//成功时返回大于0的值,失败时返回-1
      maxfd     //监视对象文件描述符数量。
      readset   //将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值。 
      writeset  //将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值。  
      exceptset //将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值。
      timeout   //调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
      返回值    //发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数

前述したように、selcct関数を使用して3つの監視項目の変化を確認します。監視項目に応じて fd_set 変数を 3 つ宣言し、それぞれにファイルディスクリプタ情報を登録し、変数のアドレス値を上記関数の第 2 引数~第 4 引数に渡します。ただし、その前に (select 関数を呼び出す前に)、次の 2 つのことを決定する必要があります。
「ファイルディスクリプタの監視(チェック)範囲は何ですか?」
「select関数のタイムアウト時間はどのように設定するのですか?」
まず、ファイルディスクリプタの監視範囲はselect関数の第1パラメータに関係します。実際、select 関数では、最初のパラメーターを通じてモニター オブジェクト ファイル記述子の数を渡す必要があります。したがって、変数fd_setに登録されているファイルディスクリプタの数を取得する必要があります。ただし、新しいファイル記述子が作成されるたびに、その値は 1 ずつ増加するため、最大のファイル記述子の値に 1 を加算して、それを select 関数に渡すだけで済みます。1 を追加するのは、ファイル記述子の値が 0 から始まるためです。
第 2 に、select 関数のタイムアウト期間は select 関数の最後のパラメータに関連しており、timeval 構造は次のように定義されます。

struct timeval{
     long tv_sec;   //seconds
     long tv_usec;  //microseconds
}

本来、select 関数は、監視対象のファイル記述子が変更された場合にのみ返されます。変化がない場合はブロッキング状態になります。これを防ぐためにタイムアウトが指定されています。上記の構造体変数を宣言することで、tv_secメンバに秒を、tv_usecメンバにマイクロ秒を入れて、構造体のアドレス値をselect関数の最後のパラメータに渡します。このとき、ファイルディスクリプタに変更がなくても、指定した時間が経過していれば関数からリターンできます。ただし、この場合、select 関数は 0 を返します。したがって、戻り値によって戻りの理由がわかります。タイムアウトを設定したくない場合は、NULL パラメータを渡します。

select関数呼び出し後の結果を確認する

関数呼び出し後の結果を確認することも同様に重要です。select 関数の戻り値について説明しましたが、0 より大きい整数が返された場合、対応するファイル記述子の数が変更されたことを意味します。では、この変更はどのように変わるのでしょうか? select 関数の呼び出しが完了すると、それに渡される fd_set 変数が変更されます。変更されたファイル記述子に対応するビットを除き、1 だったすべてのビットが 0 になります。したがって、値が1のままである位置のファイルディスクリプタが変更されたと考えられる。

関数呼び出しの例を選択します

#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/select.h>
#define BUF_SIZE 30

int main(int argc,char *argv){
    fd_set reads,temps;
    int result,str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;

    FD_ZERO(&reads);
    FD_SET(0,&reads);

    while(1){
       temps=reads;
       timeout.tv_sec=5;
       timeout.tv_usec=0;
       result=select(1,&temp,0,0,&timeout);
       if(result==-1){
            puts("select() error!");
            break;
       }
       else if(result==0){
            puts("Time-out!");
       }
       else{
            if(FD_ISSET(0,&temps)){
               str_len=read(0,buf,BUF_SIZE);
               buf[str_len]=0;
               printf("message from console: %s",buf);
            }
       }
    }
return 0;
}

上記に関して注意すべき点は次の 2 点です。

1. select 関数の呼び出し後に監視 fd_set の内容が変更されるため、コピーを保存する必要があり、上記のコードは temps に保存されます。

2. タイムアウト時間は、select 関数の呼び出し時にタイムアウト前の残り時間に置き換えられるため、タイムアウト時間の初期値も毎回初期化する必要があります。

サーバー側でIO多重化を実現

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<sys/select.h>

#define BUF_SIZE 100
void error_handling(char *buf);

int main(int argc,char *argv[]){
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    struct timeval timeout;
    fd_set reads,cpy_reads;

    socklen_t addr_sz;
    int fd_max,str_len,fd_num,i;
    char buf[BUF_SIZE];
    if(argc!=2){
         printf("Usage: %s <port>\n",argv[0]);
         exit(1);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0);
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi([argv[1]]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
         error_handling("bind() error");
    if(listen(serv_sock,5)==-1)
         error_handling("listen() error");

    FD_ZERO(&reads);
    FD_SET(serv_sock,&reads);
    fd_max=serv_sock;

    while(1){
         cpy_reads=reads;
         timeout.tv_sec=5;
         timeout.tv_usec=5000;

         if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
              break;
         if(fd_num==0)
              continue;

         for(i=0;i<fd_max+1;++i){
              if(FD_ISSET(i,&cpy_reads)){     
                     if(i==serv_sock){     //连接请求到来
                         addr_sz=sizeof(clnt_addr);
                         clnt_sock=accept(serv_sock,(structsockaddr*)&clnt_addr,addr_sz);
                         FD_SET(clnt_sock,&reads);
                         if(fd_max<clnt_sock)fd=clnt_sock;
                         printf("connected client: %d \n",clnt_sock);
                     }
                     else{
                         str_len=rad(i,buf,BUF_SIZE);
                         if(str_len==0){
                               FD_CLR(i,&reads);
                               close(i);
                               printf("closed client: %d \n",i);
                         }
                         else{
                             write(i,buf,str_len);//回声
                         }
                     }
              }
         }
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *buf){
    fputs(buf,stderr);
    fputc('\n',stderr);
    exit(1);
}

Windows ベースの実装

Windows プラットフォームでの選択関数の呼び出し

Windows にも select 関数が用意されており、すべてのパラメーターは Linux の select 関数とまったく同じです。Windows プラットフォームの select 関数の最初のパラメータは、UNIX 系オペレーティング システム (Linux を含む) との互換性を維持するために追加されているだけで、特別な意味はありません。

#include <winsock2.h>
int select(int nfds, fd_set *treadfds, fd_set *writefds, fd_set *excepfds, const struct
timeval * timeout);//成功时返回0,失败时返回-1。

戻り値とパラメータの順序と意味は、以前の Linux の select 関数と同じなので省略します。
timeval 構造の定義を以下に示します。

typedef struct timeval{
      long tv_sec;
      long tv_usec;
} TIMEVAL;

ご覧のとおり、基本的な構造は Linux での以前の定義と同じですが、Windows では typedef 宣言が使用されます。次に、fd_set 構造体を観察します。Windows で実装する場合はこの点に注意する必要があります。Windows の fd_set は Linux のようなビット配列を使用していないことがわかります。

typedef struct fd_set{
    u_int fd_count;
    SOCKET fd_array[FD_SETSIZE];
} fd_set;

Windows の fd_set は fd_count と fd_array というメンバで構成されており、fd_count はソケット ハンドルの数に使用され、fd_array はソケット ハンドルの保存に使用されます。これを考えれば、この記述の理由が理解できます。Linux のファイル記述子は 0 から増加し始めるため、現在のファイル記述子の数と最後に生成されたファイル記述子の関係を知ることができます。ただし、Windowsのソケットハンドルは0から始まるわけではなく、ハンドルの整数値の間に従うべきルールがないため、ハンドルの配列やハンドル数を記録する変数を直接保存する必要があります。幸いなことに、fd_set 構造を扱う 4 つの FDXXX マクロの名前、機能、使用方法は Linux のものとまったく同じです (省略)。互換性を確保するための Microsoft の配慮かもしれません。

おすすめ

転載: blog.csdn.net/Reol99999/article/details/131748815