【Network Advanced】5つのIOネットワークモデル (1)

1. ブロック I/O

Linux では、デフォルトですべてのソケットがブロックされます。一般的な読み取り操作の流れは次のとおりです。
ここに画像の説明を挿入

ユーザー プロセスが read システム コールを呼び出すと、カーネルはデータを準備する I/O の最初のフェーズを開始します。ネットワーク I/O の場合、通常、データが最初に到着していない (たとえば、完全なパケットが受信されていない) ため、カーネルは十分なデータが到着するまで待機する必要があります。ユーザー プロセス側では、プロセス全体がブロックされます。カーネルがデータの準備が整うまで待機すると、データがカーネル空間からユーザー空間にコピーされ、カーネルが結果を返し、ユーザー プロセスのブロックが解除され、再び実行が開始されます。

したがって、ブロッキング I/O は、I/O 実行の両方のフェーズ (データの待機とデータのコピー) でブロックされるという特徴があります。

ほとんどのプログラマーは、ネットワーク プログラミングに触れると、最初に listen()、send()、recv() などのブロッキング インターフェイスを理解します。サーバー/クライアント モデルは、これらのインターフェイスを使用して便利に構築できます。簡単な Q&A サーバーの例を次に示します。

ここに画像の説明を挿入

ほとんどのソケット インターフェイスはブロックしています。いわゆるブロッキング インターフェースとは、システム コール (通常は I/O インターフェース) が呼び出し結果を返さずに現在のスレッドをブロックしたままにし、システム コールが結果を取得するか、タイムアウト エラーが発生した場合にのみ戻ることを意味します。

実際、特に指定がない限り、ほとんどすべての I/O インターフェイス (ソケット インターフェイスを含む) がブロックされています。つまり、send() の呼び出し中にスレッドがブロックされ、この間、スレッドは計算を実行したり、ネットワーク リクエストに応答したりできなくなります。

簡単な改善は、サーバー側でマルチスレッド (またはマルチプロセッシング) を使用することです。マルチスレッド (またはマルチプロセッシング) の目的は、各接続に独立したスレッド (またはプロセス) を持たせることで、1 つの接続のブロックが他の接続に影響を与えないようにすることです。マルチプロセスを使用するかマルチスレッドを使用するかにかかわらず、特定のモードはありません。従来、プロセスのオーバーヘッドはスレッドのオーバーヘッドよりもはるかに大きいため、多数のクライアントに同時にサービスを提供する必要がある場合、マルチプロセスは推奨されません。単一のサービス実行本体がより多くを消費する必要がある場合CPU リソース、たとえば、大規模または長期のデータ操作またはファイル アクセス、プロセスはより安全です。一般に、新しいスレッドは pthread_create() で作成するか、新しいプロセスは fork() で作成できます。

上記のサーバー/クライアント モデルに対してより高い要件が提示されていると仮定すると、つまり、サーバーは複数のクライアントに対して同時に 1 問 1 回答のサービスを提供できるため、次のモデルが使用可能になります。

ここに画像の説明を挿入

上記のスレッド/時間の例では、メイン スレッドはクライアントからの接続要求を継続的に待機しています。つながりがある場合は、新しいスレッドを作成し、新しいスレッドで前の例と同じ質疑応答サービスを提供します。

多くの初心者は、ソケットが複数回受け入れられる理由を理解していない可能性があります。実際、ソケットの設計者は、accept() が新しいソケットを返すことができるように、この関数をマルチクライアント シナリオ専用に設計した可能性があります。以下は、accept インターフェイスのプロトタイプです。

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

入力パラメータ s は、socket()、bind()、および listen() から継承されたソケット ハンドル値です。bind() および listen() の実行後、オペレーティング システムは、指定されたポートですべての接続要求のリッスンを開始します。要求がある場合は、接続要求を要求キューに追加します。accept() インターフェイスを呼び出すと、ソケット s の要求キューから最初の接続情報が抽出され、s と同じタイプの新しいソケットが作成され、ハンドルが返されます。新しいソケット ハンドルは、後続の read() および recv() の入力パラメータとして使用されます。現在、リクエスト キューにリクエストがない場合、リクエストがキューに入るまで、accept() はブロック状態になります。

上記のマルチスレッド サーバー モデルは、複数のクライアントに質問応答サービスを提供するという要件を完全に解決しているように見えますが、そうではありません。数百または数千の接続要求に同時に応答したい場合、マルチスレッドであろうとマルチプロセスであろうと、システム リソースを深刻に占有し、システムの外部世界への応答効率を低下させ、スレッドとプロセス自体が応答する可能性が高くなります。仮死状態に入る。

多くのプログラマーは、「スレッド プール」または「接続プール」の使用を検討するかもしれません。「スレッド プール」の目的は、スレッドの作成と破棄の頻度を減らし、適切な数のスレッドを維持し、アイドル状態のスレッドが新しい実行タスクを実行できるようにすることです。「接続プール」は、接続キャッシュ プールを維持し、既存の接続を可能な限り再利用して、接続の作成と終了の頻度を減らします。これらの 2 つのテクノロジは、システムのオーバーヘッドを非常にうまく削減することができ、Websphere、Tomcat、さまざまなデータベースなど、多くの大規模システムで広く使用されています。ただし、「スレッド プール」および「接続プール」テクノロジは、I/O インターフェイスの頻繁な呼び出しによるリソースの占有をある程度軽減することしかできません。また、いわゆる「プール」には必ず上限があり、要求が上限を大幅に超えると、「プール」によって形成されるシステムの外界に対する応答は、プールがない場合と比べてあまり良くありません。したがって、「プール」を使用する場合、直面する応答の規模を考慮し、応答の規模に応じて「プール」を調整する必要があります。

上記の例で同時に発生する可能性のある数千または数万のクライアント要求に対して、「スレッド プール」または「接続プール」はプレッシャーの一部を軽減する可能性がありますが、すべての問題を解決できるわけではありません。つまり、マルチスレッド モデルは小規模なサービス リクエストを便利かつ効率的に解決できますが、大規模なサービス リクエストに直面すると、マルチスレッド モデルにもボトルネックが発生します。現時点では、ノンブロッキング インターフェイスを使用してこの問題を解決できます。

ノンブロッキング I/O (ノンブロッキング I/O) または非同期 I/O (非同期 I/O) は、多数の同時接続を処理するもう 1 つの方法です。このモードでは、I/O 操作は現在のスレッドをブロックせず、すぐに戻ります。I/O 操作が完了していない場合、システム コールは特別なエラー コードを返し、呼び出し元に後で再試行するように指示します。これにより、サーバーは、I/O 操作が完了するのを待っている間に他のクライアント要求を処理できます。

ノンブロッキング I/O を実現する 1 つの方法は、イベント駆動型プログラミング (イベント駆動型プログラミング) を使用することです。このモードでは、サーバーは、新しい接続要求やデータの到着など、さまざまな I/O イベントをリッスンするためのイベント ループ (イベント ループ) を維持します。イベントが発生すると、イベント ループは対応するコールバック関数を呼び出してイベントを処理します。このようにして、サーバーは複数のクライアントからの要求を 1 つのスレッドで処理できるため、マルチスレッドまたはマルチプロセッシングによって生じるオーバーヘッドを回避できます。

Linux には、select、poll、epoll など、イベント駆動型プログラミングを実装するためのさまざまなメカニズムがあります。これらのメカニズムにより、プログラマは 1 つのスレッドで複数のファイル記述子 (ソケットなど) のステータスを監視し、特定のファイル記述子の準備ができたとき (読み取り可能、書き込み可能など) に処理することができます。これらのメカニズムの主な違いは、パフォーマンスとスケーラビリティにあります。その中でも epoll は、多数の同時接続を処理する際のパフォーマンスが優れています。

つまり、ノンブロッキング I/O とイベント ドリブン プログラミングは、大規模な同時接続シナリオに効果的なソリューションを提供します。select、poll、epoll などのメカニズムを使用することで、高性能でスケーラブルなサーバーを実現でき、マルチスレッドやマルチプロセスによって引き起こされるリソースのオーバーヘッドと管理の複雑さを回避できます。


C 言語ソケットの例:

この例は、クライアントからの接続を受け入れ、クライアントから送信されたメッセージを受信し、メッセージをそのままクライアントに返す TCP サーバーです。

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    
    
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUF_SIZE];
    socklen_t client_addr_size;

    // 创建服务器socket
    server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_socket == -1) {
    
    
        perror("socket creation failed");
        exit(1);
    }

    // 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);

    // 绑定socket到服务器地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    
    
        perror("bind failed");
        exit(1);
    }

    // 监听socket
    if (listen(server_socket, 5) == -1) {
    
    
        perror("listen failed");
        exit(1);
    }

    client_addr_size = sizeof(client_addr);
    while (1) {
    
    
        // 接受客户端连接(阻塞式)
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_size);
        if (client_socket == -1) {
    
    
            perror("accept failed");
            exit(1);
        }

        // 接收客户端消息并将其返回(阻塞式)
        int read_size;
        while ((read_size = read(client_socket, buffer, BUF_SIZE)) != 0) {
    
    
            printf("从客户端收到的消息:%s\n", buffer);
            write(client_socket, buffer, read_size);
            memset(buffer, 0, sizeof(buffer));
        }

        close(client_socket);
    }

    close(server_socket);
    return 0;
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    
    
    int client_socket;
    struct sockaddr_in server_addr;
    char buffer[BUF_SIZE];

    // 创建客户端socket
    client_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (client_socket == -1) {
    
    
        perror("socket creation failed");
        exit(1);
    }

    // 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(SERVER_PORT);

    // 连接到服务器(阻塞式)
    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    
    
        perror("connect failed");
        exit(1);
    }

    while (1) {
    
    
        printf("请输入消息:");
        fgets(buffer, BUF_SIZE, stdin);
        buffer[strlen(buffer) - 1] = '\0';
        // 向服务器发送消息
        write(client_socket, buffer, strlen(buffer));

        // 接收服务器响应(阻塞式)
        int read_size = read(client_socket, buffer, BUF_SIZE - 1);
        if (read_size == -1) {
    
    
            perror("read failed");
            exit(1);
        }

        buffer[read_size] = '\0';
        printf("从服务器接收到的消息:%s\n", buffer);
    }

    close(client_socket);
    return 0;
}

ここに画像の説明を挿入

2. ノンブロッキング I/O

Linux では、ソケットを設定することで、ソケットをノンブロッキングにすることができます。ノンブロッキング ソケットで読み取り操作を実行する場合のプロセスは次のとおりです。

ここに画像の説明を挿入

上記のプロセスからわかるように、ユーザー プロセスが読み取り操作を開始するときに、カーネル内のデータの準備ができていない場合、ユーザー プロセスはブロックされず、すぐにエラーが返されます。ユーザー プロセスの観点からは、読み取り操作を開始した後、待機する必要はありませんが、結果はすぐに取得されます。ユーザープロセスは、結果がエラーであると判断した場合、データの準備ができていないことを認識しているため、再度読み取り操作を開始できます。カーネル内のデータの準備が整い、ユーザー プロセスから別のシステム コールを受け取ると、すぐにデータをユーザー メモリにコピーして戻ります。したがって、ノンブロッキング I/O では、ユーザー プロセスは、データの準備ができているかどうかを常にアクティブにカーネルに問い合わせる必要があります。

非ブロッキング状態では、recv() インターフェイスは呼び出された直後に戻り、戻り値は異なる意味を持ちます。たとえば、この場合:

  • recv() の戻り値は 0 よりも大きく、データが受信されたことを示し、戻り値は受信したバイト数です。
  • recv() の戻り値は 0 で、接続が正常に切断されたことを示します
  • recv() の戻り値は -1 で、errno は EAGAIN に等しく、recv 操作が完了していないことを示します。
  • recv() の戻り値は -1 であり、errno は EAGAIN と等しくありません。これは、recv 操作でシステム エラー errno が発生したことを示しています。

非ブロッキング インターフェイスとブロッキング インターフェイスの大きな違いは、呼び出しの直後に戻ることです。ファイル記述子 fd は、次の関数を使用して非ブロック状態に設定できます。

fcntl(fd, F_SETFL, O_NONBLOCK);

以下は、1 つのスレッドのみを使用するモデルを示していますが、データが複数の接続で到着するかどうかを同時に検出し、データを受信できます。

ここに画像の説明を挿入

サーバー スレッドが recv() インターフェイスをループで呼び出して、すべての接続のデータ受信作業を 1 つのスレッドで実現できることがわかります。ただし、このモデルはお勧めしません。ループ内で recv() を呼び出すと、CPU 使用率が大幅に増加するため、さらに、このスキームでは、recv() は「操作が完了したかどうか」を検出するために使用されます。実際、オペレーティング システムはより効率的な検出を提供します」 select () 多重化モードなどの「操作が完了しました」インターフェイスは、一度に複数の接続がアクティブになっているかどうかを確認できます。select() などの多重化テクノロジを使用すると、同時接続をより効率的に処理し、CPU 使用率を削減し、サーバーのパフォーマンスを向上させることができます。


C 言語ソケットの例:

fcntl()関数を使用して、ソケットを非ブロッキング モードに設定し、select()関数を使用した読み取り/書き込み操作で非同期にすることができます。以下もTCPサーバーで、クライアントからの接続を受け付け、クライアントから送信されたメッセージを受け取り、メッセージをそのままクライアントに返します。

server.c

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

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    
    
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUF_SIZE];
    socklen_t client_addr_size;
    fd_set read_fds, temp_fds;
    int max_fd;

    // 创建服务器socket
    server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_socket == -1) {
    
    
        perror("socket creation failed");
        exit(1);
    }

    // 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);

    // 绑定socket到服务器地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    
    
        perror("bind failed");
        exit(1);
    }

    // 监听socket
    if (listen(server_socket, 5) == -1) {
    
    
        perror("listen failed");
        exit(1);
    }

    // 设置服务器socket为非阻塞模式
    int flags = fcntl(server_socket, F_GETFL, 0);
    fcntl(server_socket, F_SETFL, flags | O_NONBLOCK);

    FD_ZERO(&read_fds);
    FD_SET(server_socket, &read_fds);
    max_fd = server_socket;

    client_addr_size = sizeof(client_addr);
    while (1) {
    
    
        temp_fds = read_fds;

        // 使用select()函数监控文件描述符集合
        if (select(max_fd + 1, &temp_fds, NULL, NULL, NULL) == -1) {
    
    
            perror("select failed");
            exit(1);
        }

        for (int i = 0; i <= max_fd; i++) {
    
    
            if (FD_ISSET(i, &temp_fds)) {
    
    
                if (i == server_socket) {
    
    
                    // 接受客户端连接(非阻塞式)
                    client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_size);
                    if (client_socket == -1) {
    
    
                        perror("accept failed");
                        exit(1);
                    }

                    // 将客户端socket添加到文件描述符集合
                    FD_SET(client_socket, &read_fds);
                    if (client_socket > max_fd) {
    
    
                        max_fd = client_socket;
                    }
                } else {
    
    
                    // 接收客户端消息并将其返回(非阻塞式)
                    int read_size = read(i, buffer, BUF_SIZE - 1);
                    if (read_size <= 0) {
    
    
                        close(i);
                        FD_CLR(i, &read_fds);
                    } else {
    
    
                        write(i, buffer, read_size);
                    }
                }
            }
        }
    }

    close(server_socket);
    return 0;
}

client.c

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

#define BUF_SIZE 1024
#define SERVER_PORT 8080

int main() {
    
    
    int client_socket;
    struct sockaddr_in server_addr;
    char buffer[BUF_SIZE];
    fd_set read_fds, temp_fds;
    int max_fd;

    // 创建客户端socket
    client_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (client_socket == -1) {
    
    
        perror("socket creation failed");
        exit(1);
    }

    // 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(SERVER_PORT);

    // 连接到服务器(阻塞式)
    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
    
    
        perror("connect failed");
        exit(1);
    }

    // 设置客户端socket为非阻塞模式
    int flags = fcntl(client_socket, F_GETFL, 0);
    fcntl(client_socket, F_SETFL, flags | O_NONBLOCK);

    FD_ZERO(&read_fds);
    FD_SET(client_socket, &read_fds);
    FD_SET(STDIN_FILENO, &read_fds);
    max_fd = client_socket;

    while (1) {
    
    
        temp_fds = read_fds;

        // 使用select()函数监控文件描述符集合
        if (select(max_fd + 1, &temp_fds, NULL, NULL, NULL) == -1) {
    
    
            perror("select failed");
            exit(1);
        }

        if (FD_ISSET(STDIN_FILENO, &temp_fds)) {
    
    
            printf("请输入消息:");
            fgets(buffer, BUF_SIZE, stdin);
            buffer[strlen(buffer) - 1] = '\0';

            // 向服务器发送消息
            write(client_socket, buffer, strlen(buffer));
        }

        if (FD_ISSET(client_socket, &temp_fds)) {
    
    
            // 接收服务器响应(非阻塞式)
            int read_size = read(client_socket, buffer, BUF_SIZE - 1);
            if (read_size <= 0) {
    
    
                close(client_socket);
                exit(1);
            }

            buffer[read_size] = '\0';
            printf("从服务器接收到的消息:%s\n", buffer);
        }
    }

    close(client_socket);
    return 0;
}

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_52665939/article/details/130341622