C言語のselect関数を7ステップで詳細解説

C言語のselect関数の詳細解説を初心者向けに行うイメージ図C言語
この記事は約15分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C言語のselect関数の詳細な使い方や活用法について、初心者でも理解できるように解説します。

特に、初心者が混乱しがちな部分を明確に説明していきます。

●select関数とは

○select関数の概要

select関数はC言語でよく使われる関数の一つです。

主に、複数の入出力ストリームの監視を行うために使用されます。

具体的には、複数のソケット(ネットワーク通信のエンドポイント)を監視し、読み取り可能、書き込み可能、またはエラー状態になったソケットを知ることができます。

○select関数の使い道

select関数は、特にネットワーク通信を行うプログラムで多く使用されます。

一つのプロセスで複数のソケットを同時に監視するために使用されるほか、非同期IOの実装や、マルチクライアントのサーバー構築などにも利用されます。

●select関数の使い方

○基本的な使い方

select関数の基本的な使い方について見ていきましょう。

select関数の基本的な使い方を表すコードを紹介します。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
    fd_set fds;
    struct timeval tv;

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

    tv.tv_sec = 5;
    tv.tv_usec = 0;

    select(1, &fds, NULL, NULL, &tv);

    if (FD_ISSET(0, &fds)) {
        printf("Data is available now.\n");
    } else {
        printf("No data within five seconds.\n");
    }

    return 0;
}

このコードでは、select関数を使って標準入力(ファイルディスクリプタ0)の監視を行っています。

ここでは5秒間だけ監視を行い、その間に標準入力があれば”Data is available now.”を、なければ”No data within five seconds.”を出力します。

○詳細な使い方

さらに詳しくselect関数の使い方を理解するため、より複雑な例を見てみましょう。

下記のコードは、複数のソケットの監視を行い、ソケットが読み取り可能になったらその情報を出力する例です。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
    fd_set readfds;
    int sockfd1, sockfd2, maxfd;

    // 前提:sockfd1, sockfd2は事前に作成され、接続されているとします。

    FD_ZERO(&readfds);
    FD_SET(sockfd1, &readfds);
    FD_SET(sockfd2, &readfds);

    maxfd = (sockfd1 > sockfd2 ? sockfd1 : sockfd2) + 1;

    select(maxfd, &readfds, NULL, NULL, NULL);

    if (FD_ISSET(sockfd1, &readfds)) {
        printf("Socket 1 is ready to be read.\n");
    }
    if (FD_ISSET(sockfd2, &readfds)) {
        printf("Socket 2 is ready to be read.\n");
    }

    return 0;
}

このコードでは、まずfd_set型の変数readfdsを初期化し、その後、監視したいソケットのファイルディスクリプタsockfd1とsockfd2をセットしています。

その後、select関数を呼び出し、どちらのソケットが読み取り可能になったかを確認しています。

このコードを実行すると、sockfd1やsockfd2が読み取り可能になった場合にそれぞれ”Socket 1 is ready to be read.”や”Socket 2 is ready to be read.”が出力されます。

●select関数の応用例

C言語のselect関数は多くの応用可能な場面がありますが、その中でも特に代表的なものとして、非同期通信とマルチクライアントサーバの実装が挙げられます。

これらの応用例について詳しく見ていきましょう。

○応用例1:非同期通信

非同期通信とは、サーバとクライアント間でデータの送受信を行う際に、お互いの処理を待たずに通信を行う方式のことを指します。

この方式を採用することで、レスポンス待ちの時間を他の処理に充てることができ、効率的な通信が可能になります。

select関数を用いた非同期通信のサンプルコードを紹介します。

このコードでは、select関数を用いて複数のソケットからの入力を同時に監視しています。

#include<stdio.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>

int main(void) {
    fd_set fds;
    struct timeval tv;
    int ret;

    //ファイルディスクリプタのセットをクリア
    FD_ZERO(&fds);
    //0番のファイルディスクリプタをセットに追加
    FD_SET(0, &fds);

    //タイムアウトを5秒に設定
    tv.tv_sec = 5;
    tv.tv_usec = 0;

    ret = select(1, &fds, NULL, NULL, &tv);
    if (ret == -1) {
        perror("select");
        return 1;
    } else if (!ret) {
        printf("%d seconds elapsed.\n", 5);
        return 0;
    }

    if (FD_ISSET(0, &fds)) {
        char buf[256];
        fgets(buf, sizeof(buf), stdin);
        printf("input: %s", buf);
    }

    return 0;
}

このコードでは、まずfd_set型の変数fdsを初期化し、0番のファイルディスクリプタをセットに追加しています。

次に、タイムアウトを5秒に設定してselect関数を呼び出しています。

この状態で何らかの入力があればその内容を表示し、なければタイムアウトして終了します。

このコードを実行すると、5秒間ユーザーからの入力を待つようになります。

何かしらの文字列を入力しエンターを押すと、その文字列が出力されます。

5秒間何も入力がなければ、タイムアウトメッセージが表示されてプログラムは終了します。

○応用例2:マルチクライアントサーバ

マルチクライアントサーバは、一つのサーバが複数のクライアントからの接続を同時に受け入れることができるようにするための方式です。

この方式を採用することで、大量のクライアントからの接続要求に対応することが可能になります。

select関数を用いたマルチクライアントサーバのサンプルコードを紹介します。

このコードでは、select関数を用いて複数のクライアントからの接続要求を同時に監視しています。

ただし、簡易化のためエラーハンドリングは省略しています。

#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<sys/time.h>
#include<sys/types.h>
#include<string.h>

#define PORT 50000
#define BACKLOG 5
#define BUF_SIZE 256
#define MAX_FD 10

int main(void) {
    int listener;
    int client[MAX_FD];
    fd_set fds;
    int i;
    struct sockaddr_in addr;
    char buf[BUF_SIZE];

    //ソケットを作成
    listener = socket(PF_INET, SOCK_STREAM, 0);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(listener, (struct sockaddr *)&addr, sizeof(addr));

    //ソケットを待ち受け状態にする
    listen(listener, BACKLOG);

    for (i = 0; i < MAX_FD; i++)
        client[i] = -1;  // -1 indicates available entry

    FD_ZERO(&fds);
    FD_SET(listener, &fds);

    while(1) {
        fd_set rfds = fds;
        int n = select(MAX_FD+1, &rfds, NULL, NULL, NULL);

        if (FD_ISSET(listener, &rfds)) {
            struct sockaddr_in client_addr;
            socklen_t len = sizeof(struct sockaddr_in);
            int sock = accept(listener, (struct sockaddr *)&client_addr, &len);
            for (i = 0; i < MAX_FD; i++) {
                if (client[i] < 0) {
                    client[i] = sock;
                    FD_SET(client[i], &fds);
                    break;
                }
            }
        }

        for (i = 0; i < MAX_FD; i++) {
            if (client[i] > 0 && FD_ISSET(client[i], &rfds)) {
                memset(buf, 0, sizeof(buf));
                recv(client[i], buf, sizeof(buf), 0);
                printf("client[%d]: %s", i, buf);
            }
        }
    }

    return 0;
}

このコードでは、まずリスナーソケットを作成し、指定したポートで待ち受け状態にします。

その後、無限ループの中でselect関数を用いて新たな接続要求と既存のクライアントからのメッセージの受信を同時に監視しています。

新たな接続があれば、それを受け入れて接続用のソケットを作成し、FD_SETを使って監視対象のファイルディスクリプタセットに追加します。

既存のクライアントからメッセージがあればそれを表示します。

このコードを実行すると、指定したポートでクライアントからの接続を待ち続けるサーバが起動します。

クライアントから接続要求があり、メッセージが送られてくると、そのメッセージがサーバ側のコンソールに表示されます。

●select関数の注意点と対処法

プログラミング言語Cにおいて、select関数は非常に強力で、多くの応用例がありますが、その使用には注意が必要です。

ここではその主な注意点とその対処法について説明します。

○注意点

  1. select関数の引数として使用するファイルディスクリプタセットは、select関数の呼び出し後に変更されてしまうことです。
    つまり、select関数を繰り返し使用する際には、毎回ファイルディスクリプタセットを初期化し直す必要があります。
  2. select関数は指定したファイルディスクリプタのいずれかが読み取り、書き込み、または例外のいずれかの状態になると直ちに戻る仕様となっています。
    しかし、その時点でどのファイルディスクリプタが状態の変更を引き起こしたのか特定するためには、全てのファイルディスクリプタを調べる必要があります。
  3. select関数はファイルディスクリプタの最大数に制限があります
    UNIX系のシステムではこの最大数は通常1024となっており、これを超えるとエラーとなってしまいます。

○対処法

  1. ファイルディスクリプタセットがselect関数の呼び出し後に変更されてしまう問題に対処するためには、select関数を呼び出す前に毎回ファイルディスクリプタセットを初期化するようにしましょう。
  2. どのファイルディスクリプタが状態の変更を引き起こしたか特定するためには、select関数の戻り値とFD_ISSETマクロを使用して全てのファイルディスクリプタを順に調べるようにします。
  3. select関数のファイルディスクリプタの最大数に制限がある問題に対処するには、より新しい関数であるpoll関数やepoll関数(Linux専用)を使用することを検討することが一つの解決策となります。
    これらの関数はファイルディスクリプタの最大数の制限がないため、大量のファイルディスクリプタを扱う必要がある場合に適しています。

以上のように、select関数の使用にはいくつかの注意点がありますが、それらの注意点を理解し、適切な対処法を用いることで、select関数を有効に活用することが可能となります。

これらの知識を持つことで、あなたのC言語プログラミングのスキルはさらに向上するでしょう。

次に、これまで学んだ知識を活用して、select関数をカスタマイズした例を見てみましょう。

●select関数のカスタマイズ

C言語のselect関数は、その柔軟性からさまざまな形にカスタマイズすることができます。

select関数を使用してノンブロッキングな通信を行うカスタマイズ例を紹介します。

○カスタマイズの方法

ノンブロッキングな通信とは、通信を行うプロセスが相手方の応答を待つことなく、次の処理に移行する方式のことを言います。

これにより、レスポンス待ちでプロセスがブロックされることなく、他の処理を並行して行うことが可能となります。

下記のサンプルコードでは、select関数とタイムアウトを用いてノンブロッキングな通信を実現しています。

具体的には、select関数のタイムアウトを0秒に設定することで、ファイルディスクリプタの状態変化を待つことなく直ちに処理を進めています。

これにより、通信が可能な状態になった場合だけデータの読み書きを行い、そうでない場合はすぐに次の処理に移る、というノンブロッキングな通信を実現しています。

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    fd_set rfds;
    struct timeval tv;
    int retval;

    /* ファイルディスクリプタ0(標準入力)を監視対象にする */
    FD_ZERO(&rfds);
    FD_SET(0, &rfds);

    /* タイムアウトを0秒に設定 */
    tv.tv_sec = 0;
    tv.tv_usec = 0;

    retval = select(1, &rfds, NULL, NULL, &tv);
    if (retval == -1)
        perror("select()");
    else if (retval)
        printf("データが利用可能です。\n");
    else
        printf("データは利用可能ではありません。\n");

    return 0;
}

このコードは、標準入力からのデータの有無をノンブロッキングで確認する例です。

タイムアウトを0秒に設定しているため、select関数は待つことなくすぐに戻り、データが利用可能かどうかを表示します。

このコードを実行すると、標準入力に何も入力しなければ「データは利用可能ではありません」と表示され、何か入力を行いエンターキーを押すと「データが利用可能です」と表示されます。

このように、select関数の使い方や特性を理解すれば、様々なシチュエーションに合わせて自由にカスタマイズすることが可能です。

さまざまな要件に対応する力が、C言語プログラミングの醍醐味とも言えるでしょう。

以上が、C言語のselect関数の詳細な解説となります。

本記事を通じて、select関数の基本的な使い方から応用例、注意点とその対処法、そしてカスタマイズ方法までを理解し、活用していただければ幸いです。

まとめ

この記事では、C言語のselect関数の詳細について、基本的な使い方からサンプルコード、応用例、注意点と対処法、カスタマイズ方法までを順を追って解説しました。

初心者の方でも理解できるように丁寧に説明してきましたので、ぜひこれを参考に、select関数の活用を試みてみてください。

これからもプログラミングのスキルを磨き続けるために、新しい関数の学習を止めず、さまざまな関数を試してみてください。

それにより、より効率的なコードを書く能力が身につきます。

最後まで読んでいただき、ありがとうございました。