11. TCP 同時ネットワーク プログラミング

この記事では、IO 多重化の epoll 実装に焦点を当てて、TCP 同時ネットワークのプログラミングを主に紹介します。

1. TCP/IPネットワーク通信処理

完全な TCP/IP ネットワーク通信プロセスを完了するには、一連の関数を使用する必要があります。これらの機能には、バインド、リッスン、受け入れ、受信/送信などが含まれます。これらがどのように連携するかは次のとおりです。

  1. ソケット (ソケット) の作成: ソケット関数を使用してソケットを作成し、プロトコル ファミリとソケット タイプを指定します。
  2. バインド アドレス (バインド): ローカル アドレスをソケットにバインドし、クライアントがこのアドレスを介してサーバーにアクセスできるようにします。
  3. 接続要求を待機する (listen): ソケットを待機状態に設定し、待機中の接続の最大数 (バックログ) を指定します。
  4. 接続要求の受け入れ (accept): クライアントが接続要求を開始すると、accept 関数を使用してクライアントとの通信用の新しいソケットを作成します。
  5. データの読み取りおよび書き込み (受信/送信): 新しく作成されたソケットを使用して、クライアントからのデータの読み取りやクライアントへのデータの送信などのデータ送信を行います。
  6. 接続を閉じる (close): 通信が終了したら、close 関数を使用してソケットを閉じ、リソースを解放する必要があります。

ステップ 4 のリクエストには、1 スレッド 1 リクエストと epoll メソッドの 2 つの処理方法があります。

2. IO多重化のepoll実装

epoll は、Linux オペレーティング システムにおける高性能 I/O 多重化のメカニズムです。複数のファイル記述子を同時に監視でき、いずれかのファイル記述子で読み取りや書き込みなどのイベントが発生すると、対応するコールバック関数がトリガーされて処理されます。
epoll に基づく TCP サーバー プログラムの流れは次のとおりです。

  1. リスニングソケットを作成し、socket() 関数を使用してソケットを作成し、関連パラメータ (アドレスの再利用など) を設定します。

  2. リスニングソケットをローカル IP アドレスとポート番号にバインドし、bind() 関数を使用してソケットを指定されたアドレスにバインドします。

  3. 接続リクエストのリスニングを開始し、 listen() 関数を使用してソケットをパッシブ リスニング状態としてマークし、同時に処理できる接続の最大数を設定します。

  4. epoll インスタンスを作成し、epoll_create() 関数を使用して epoll インスタンスを作成し、監視するイベント タイプを設定します。

  5. リスニング・ソケットを epoll インスタンスに追加し、 epoll_ctl() 関数を使用して、監視対象のファイル記述子と対応するイベントを epoll インスタンスに追加します。イベント タイプは通常、読み取り可能なイベントの場合は EPOLLIN、エラー イベントの場合は EPOLLERR です。

  6. メインループに入ってクライアント要求を処理し、epoll_wait() を使用してカーネルが Ready イベントを通知するのを待ち、Ready ファイル記述子のリストを取得します。次に、ファイル記述子のリストを調べ、各ファイル記述子に対応するイベント タイプに従って処理します。新しい接続リクエストの場合は、accept() を呼び出して接続を受信し、epoll リスニング キューに追加します。それ以外の場合は、データを直接読み取るか、接続を閉じます。

  7. リスニングソケットと接続されているクライアントソケットを閉じ、リソースをクリーンアップしてプログラムを終了します。

つまり、epoll で実装された TCP サーバー プログラムでは、epoll インスタンスを使用することで、複数のクライアント接続要求を同時に処理し、新しいデータが到着した場合に、対応する処理に間に合うようにプログラムに通知することができます。

さらに、epoll は、ET (エッジ トリガー) と LT (レベル トリガー) の 2 つの動作モードも提供します。

  1. 水平トリガー モード
    水平トリガー モードでは、ファイル記述子上のイベントが処理されていない場合、epoll はファイル記述子上にまだ処理すべきイベントがあることをアプリケーションに通知し続けます。この場合、アプリケーションが時間内に応答してデータを読み取れなかった場合、epoll はファイル記述子に読み取るデータがあることをアプリケーションに通知し続けます。
  2. エッジ トリガー モード
    エッジ トリガー モードでは、ファイル記述子で新しいイベント (データの読み取りや接続の確立など) が発生するたびに、epoll がアプリケーションに通知します。ただし、通知後、アプリケーションがすぐに応答してすべてのデータを読み取らない場合、epoll はそのファイル記述子に読み取る新しいデータがあることを再度通知しません。

一般に、エッジ トリガー モードはレベル トリガー モードより効率的であり、繰り返しの監視によって引き起こされる CPU 使用率の上昇の問題を回避できます。ただし、エッジ トリガー モードを使用してすべてのデータをタイムリーに読み取り、各イベントが適切に処理されるようにする場合は注意が必要です。

また、select と比較すると、どちらも Linux における I/O 多重化メカニズムですが、いくつかの重要な違いがあります。

  • 監視されるファイル記述子の数の制限は異なります。Linux では、select 関数でサポートされるファイル記述子の最大数はデフォルトで 1024 ですが、epoll にはこの制限がなく、数千のファイル記述子を監視できます。

  • ファイル記述子セットのコピー方法が異なります。select を使用する場合、監視対象のすべてのファイル記述子を呼び出しごとにユーザー空間からカーネル空間にコピーし、その結果をカーネル空間からコピーして戻す必要があります。結果が返された後、ユーザー空間に保存されます。これにより、パフォーマンスに大きなオーバーヘッドが生じます。epollを使用する場合、監視対象のファイル記述子をカーネルイベントテーブルに追加するだけで登録が完了し、readyイベントが発生した場合にはアプリケーションに直接処理を通知します。

  • 非ブロッキング ソケットの処理方法は異なります。選択を使用する場合、非ブロッキング ソケットの場合は、手動で非ブロッキング モードに設定し、読み取りおよび書き込み操作の準備ができているかどうかをポーリングする必要があります。ただし、epoll は EPOLLET フラグを設定することでエッジ トリガー モードを実装しており、非ブロッキング ソケットの場合は、EAGAIN エラー コードが返されるまで待つだけで、すでに非ブロッキング状態であることがわかります。

一般に、epoll には select と比較して、効率性、柔軟性、拡張が容易であるという利点があり、多数の同時接続を処理する際のパフォーマンスとスケーラビリティが優れています。


#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

// #include <winsock2.h>
// #include <mswsock.h>
// #include <windows.h>
// #include <sys/types.h>  
// #include <unistd.h>
// #include <fcntl.h>

#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <fcntl.h> 
#include <unistd.h> 
#include <sys/epoll.h>


#define BUFFER_LENGTH       1024
#define EPOLL_SIZE          1024

void *client_routine(void *arg){
    
    
    int clientfd=*(int *)arg;

    while (1){
    
    
        char buffer[BUFFER_LENGTH]={
    
    0};
        int len=recv(clientfd,buffer,BUFFER_LENGTH,0);

        if (len < 0){
    
    //非阻塞状态下读到空数据
            close(clientfd);
            break;
        }
        else if(len == 0) {
    
    //断开连接
            close(clientfd);
            break;
        }
        else{
    
    
            printf("Recv: %s, %d btye(s)\n",buffer,len);
        }
    }
}

int main(int argc,char *argv[]){
    
    
    if (argc < 2) {
    
    
        printf("Param Error \n");
        return -1;
    }
    int port=atoi(argv[1]);//atoi将一个字符串转换为对应的整数值

    int sockfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in addr;
    memset(&addr,0,sizeof(struct sockaddr_in));
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=INADDR_ANY;

    if (bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in))<0){
    
    
        perror("bind");
        return -2;
    }

    if(listen(sockfd,5)<0){
    
    
        perror("listen");
        return -3;
    }

#if 0
    // 一请求一线程
    while (1){
    
      
        struct sockaddr_in client_addr;
        memset(&client_addr,0,sizeof(struct sockaddr_in));
        socklen_t client_len =sizeof(client_addr);

        /*调用 accept() 函数后,它会一直阻塞等待直到有新的客户端连接请求到达为止。
        当有新的连接请求到达时,它会返回一个新产生的套接字文件描述符,并且将该连接对应的客户端地址信息存储在 addr 指向的结构体中*/
        int clientfd=accept(sockfd,(struct sockaddr *)&client_addr,&client_len);

        pthread_t thread_id;
        pthread_create(&thread_id,NULL,client_routine,&clientfd);

    }

#else     
    /*使用epoll的基本流程如下:
        1,创建一个epoll实例,可以通过调用 epoll_create() 函数来创建。
        2,向 epoll 实例中添加需要监控的文件描述符及其事件类型,可以通过调用 epoll_ctl() 函数进行操作。
        3,调用 epoll_wait() 函数等待监控对象上发生事件,并处理活跃的文件描述符及其事件类型。
        4,处理完活跃文件描述符的相关操作后,返回到第三步继续等待新的事件发生。
    */
    int epfd=epoll_create(1);   
    struct epoll_event events[EPOLL_SIZE] = {
    
    0};    //创建一个结构体数组 events 用于存储 epoll_wait() 返回的事件列表。
    struct epoll_event ev;  
    ev.events=EPOLLIN;  //创建一个新的 epoll_event 结构体 ev 并设置其关注的事件类型为 EPOLLIN (表示等待读事件)
    ev.data.fd = sockfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);   //使用 epoll_ctl() 函数将 sockfd 文件描述符加入到 epfd 实例中,并关联上面创建的 ev 结构体。

    while (1){
    
    
        int nready=epoll_wait(epfd,events,EPOLL_SIZE,5); //
        if (nready == -1) continue; //表示5秒内,没有事件,继续监听

        int i=0;
        for (i=0;i<nready;i++){
    
    
            /*判断当前事件所对应的文件描述符是否为监听套接字 sockfd。如果是,则说明有新的客户端连接请求到来了,
            需要通过 accept() 函数获取新产生的客户端连接并添加到 epoll 实例中;
            否则,说明是已经建立好连接的客户端发送了数据,需要通过 recv() 函数接收数据并进行相应处理。*/
            if(events[i].data.fd == sockfd){
    
    
                /*当有新的连接请求到来时(即 sockfd 上有 EPOLLIN 事件),使用 accept() 函数接受连接,
                并将其加入 epoll 实例中关注该套接字上是否有输入事件。*/
                struct sockaddr_in client_addr;
                memset(&client_addr,0,sizeof(struct sockaddr_in));
                socklen_t client_len =sizeof(client_addr);

                int clientfd=accept(sockfd,(struct sockaddr *)&client_addr,&client_len);

                ev.events=EPOLLIN | EPOLLET;  //EPOLLET 则表示将 I/O 事件设置为边缘触发模式。
                ev.data.fd=clientfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else{
    
    
                //当某个客户端套接字上出现可读事件时(即该文件描述符在 events 中对应的元素有 EPOLLIN 标志),则调用 recv() 函数从该套接字中读取数据
                int clientfd=events[i].data.fd;
                
                char buffer[BUFFER_LENGTH]={
    
    0};
                int len=recv(clientfd,buffer,BUFFER_LENGTH,0);

                if (len < 0){
    
    //出现了异常情况或者非阻塞状态下没有更多数据可读
                    //关闭该套接字并将其从 epoll 实例中删除
                    close(clientfd);
                    ev.events=EPOLLIN;  
                    ev.data.fd=clientfd;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev); //从 epoll 实例中删除 clientfd 对应的文件描述符,并且停止监听该套接字上的事件。
                }
                else if(len == 0) {
    
    //对方已经断开连接
                    //关闭该套接字并将其从 epoll 实例中删除
                    close(clientfd);
                    ev.events=EPOLLIN;  
                    ev.data.fd=clientfd;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
                }
                else{
    
    
                    printf("Recv: %s, %d btye(s)\n",buffer,len);
                }

            }
        }
    }


#endif

    return 0;
}



おすすめ

転載: blog.csdn.net/Ricardo2/article/details/130884080