🚸

ソケット通信とは?【C言語】

2023/03/19に公開

ソケット通信とはソケットを用いた通信のこと。

「通信」とは非常に身近なものであり、今読んでいただいているこのページも通信によって閲覧している。

さまざまな物が通信でつながることで、便利な機能がたくさん実現されており、これらの通信には「ソケット通信」で行われているケースが多い。

ソケットを使用することで、異なるマシン間(もしくは同一のマシン上)の異なるプロセス間で通信を可能にする。

そもそもソケットとは?

ソケットっていうのは通信端点のこと。

いいかえると、アプリとアプリ(プログラムとプログラム)の通信の出入り口。

ソケット通信を行うには、まずはアプリやプログラム内で「ソケット」を作成。

そして、それを他のアプリやプログラムのソケットに接続してデータのやり取りを行う。

なので、まずはこのソケットを作成する必要がある。

C言語ではこのソケットを、socket 関数により作成することが可能だ。

ソケットを利用し、サーバーとクライアント間でデータのやり取りを行うことができる。

つぎに、この「サーバーとクライアント」について見ていく。

サーバーとクライアン

サーバーとは、クライアントにサービスを提供するソフトウェアやコンピュータ。

そしてクライアントとは、サーバーからサービスを受けるソフトウェアやコンピュータ。

例えばウェブブラウザを使って、ウェブページの閲覧する場合、以下のようになる。

・クライアント:ウェブページを閲覧しようとしている PC やスマホ
・サーバー:ウェブページのデータを保存しているウェブサーバー

一般的に、サーバーとクライアントは別々のPCに分かれている。

しかし、自作のソケット通信を自分のPCで試す場合は、自分のPCにサーバーとクライアント両方の役割を持たせて動作させることができる。

それでは、さっそくソケット通信に入っていく。

ソケット通信のおおまかな流れ

①サーバー・クライアント間で接続を確立

ソケット作成 (socket)

まずは通信を行うためのソケット作成が必要。

注意が必要なのは、サーバーとクライアント両方にソケットが必要なので、サーバークライアント両方でソケットを作成する必要があること。

ソケットの作成は socket関数を使用する。

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

//domain:プロトコルファミリー(アドレスファミリー)を指定
//type:ソケットのタイプを指定
//protocol:使用するプロトコルを指定
//example
sock = socket(AF_INET, SOCK_STREAM, 0);

サーバー側でソケットの関連付け (bind)

ソケット作成後に行うのが bindとなり、ソケットに IP アドレスやポート番号の設定をおこなう。

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//sockfd:ソケット
//addr:ソケットに割り当てるアドレスやポート番号の情報
//addrlen:addr のサイズ(バイト数)

サーバー側で接続を待つ (listen)

クライアントから接続要求がくるまで、サーバー側で「接続待ち」をおこない、待機させる。

この接続待ちには listen 関数を使用する。

#include <sys/socket.h>

int listen(int sockfd, int backlog);

//sockfd:接続を待つソケット
//backlog:接続要求を保持する数 -> accept していない接続要求を、最大いくつまで溜めておくかを指定する。これにより、サーバーは接続要求が来たタイミングではなく、サーバー自身のタイミングが良いときに接続を受けつけ、実際のデータのやり取りを行うようなことが可能。

クライアント側で接続を要求する (connect)

つぎに、クライアントがサーバーに対して「接続要求」を行う。

この接続要求には connect関数を使用する。

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//sockfd:接続を行うソケット
//addr:接続先(アドレスやポート番号など)の情報
//addrlen:addr のサイズ(バイト数)

サーバーが接続を受け付ける (accept)

最後に、サーバーは受け取った接続要求を受けつける。

ここで、サーバーとクライアントの間で接続が確立される。

この接続の受けつけは accept 関数を使用。

accept 関数は、接続要求がくるまでは関数が終了しない。

つまり、クライアントから connectが実行されなければ、関数の中でプログラムが待たされることになる。

また、accept 関数はソケットを作成し、戻り値がその作成したソケットの識別子となる。

作成されたソケットは、接続要求を受け付けたクライアントと「接続済のソケット」である。

一方、accept 関数の第1引数 sockfdで指定したソケットは、いまだ listen で接続待ちの状態になっているソケット。

結果、接続済のクライアントとデータのやり取りを行うには、accept 関数の戻り値(接続済のソケット)を用いて、データのやり取りを行うことになる。

#include <sys/socket.h>

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

//sockfd:接続待ちを行なっているソケット
//addr:接続先の情報へのポインタ
//addrlen:addr のサイズ(バイト数)へのポインタ

②サーバー・クライアント間でデータのやり取り

ソケット同士の接続が確立後、つぎはソケットを用いて通信の目的となるデータのやり取りを行う。

データの送信(send)

send 関数は、接続先に対してデータを送信する関数です。

#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

//sockfd:接続済のソケット
//buf:送信するデータへのポインタ
//len:送信するデータのサイズ(バイト数)
//flags:送信時の動作の詳細設定

send 関数の戻り値は、実際に接続先に送信したデータのバイト数となる。

データの受信(recv)

recv 関数は、接続先が送信したデータを受信する関数。

recv 関数はデータが到着するまで待ち続ける。

#include <sys/socket.h>

ssize_t recv(int sockfd, const void *buf, size_t len, int flags);

//sockfd:接続済のソケット
//buf:受信データを格納するバッファのアドレス
//len:buf のサイズ(バイト数)
//flags:受信時の動作の詳細設定

recv 関数の戻り値は、実際に接続先から受信したデータのバイト数。

ポイント

サーバーとクライアントでタイミングを合わせることが必要。

サーバーとクライアントのどちらが、どのタイミングで、どんなデータを送信(または受信)するかを定める必要がある。

このようなデータのやり取りの仕方は「プロトコル」によって定められている。

たとえば、ウェブページを要求するさいには HTTP というプロトコルに従ってデータのやり取りを行う必要がある。

HTTP の P は、プロトコルの頭文字でもある。

③サーバー・クライアント間の接続を閉じる(close)

データのやり取りが終了したら、最後に接続を閉じる。

これによりサーバーとクライアント間の接続が解消される。

接続を閉じる処理は close関数を使用。

#include <unistd.h> 

int close(int fd);

//fd: ソケットの識別子

サンプルコード

server.c

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

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 1024

int communicate(int c_sock)
{
    int recv_size;
    int send_size;
    char recv_buf[BUF_SIZE];
    char send_buf;

    while (1)
    {
        // クライアントから文字列を受信
        recv_size = recv(c_sock, recv_buf, BUF_SIZE, 0);
        if (recv_size == -1)
        {
            printf("Error: recv()\n");
            break;
        }
        if (recv_size == 0)
        {
            // 受信サイズが0の場合は相手が接続閉じていると判断
            printf("Connection ended\n");
            break;
        }
        // 受信した文字列を表示 
        printf("%s", recv_buf);
        // 文字列が"exit"ならクライアントとの接続終了 
        if (strcmp(recv_buf, "exit\n") == 0)
        {
            // 接続終了を表す0を送信
            send_buf = 0;
            send_size = send(c_sock, &send_buf, 1, 0);
            if (send_size == -1)
            {
                printf("Error: send()\n");
                break;
            }
            printf("A client exited\n");
            break;
        }
        else
        {
            // "finish"以外の場合はクライアントとの接続を継続
            send_buf = 1;
            send_size = send(c_sock, &send_buf, 1, 0);
            if (send_size == -1)
            {
                printf("Error: send()\n");
                break;
            }
        }
    }
    return (0);
}

int main(void)
{
    int w_addr;
    int c_sock;
    struct sockaddr_in a_addr;

    // ソケットを作成
    w_addr = socket(AF_INET, SOCK_STREAM, 0);
    if (w_addr == -1) {
        printf("Error: socket()\n");
        return (-1);
    }
    // 構造体を全て0にセット
    bzero(&a_addr, sizeof(struct sockaddr_in));
    // サーバーのIPアドレスとポートの情報を設定
    a_addr.sin_family = AF_INET;
    a_addr.sin_port = htons((unsigned short)SERVER_PORT);
    a_addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    // ソケットに情報を設定
    if (bind(w_addr, (const struct sockaddr *)&a_addr, sizeof(a_addr)) == -1)
    // if (bind(w_addr, (const struct sockaddr *)&a_addr, sizeof(a_addr)) == -1)
    {
        printf("Error: bind()\n");
        close(w_addr);
        return (-1);
    }
    // ソケットを接続待ちに設定 
    if (listen(w_addr, 3) == -1)
    {
        printf("Error: listen()\n");
        close(w_addr);
        return (-1);
    }
    while (1)
    {
        // 接続要求の受け付け(接続要求くるまで待ち)
        printf("Waiting for a client's connection...\n");
        c_sock = accept(w_addr, NULL, NULL);
        if (c_sock == -1)
        {
            printf("Error: accept()\n");
            close(w_addr);
            return (-1);
        }
        printf("Connected!!\n");
        // 接続済のソケットでデータのやり取り 
        communicate(c_sock);
        // ソケット通信をクローズ 
        close(c_sock);
        // 次の接続要求の受け付けに移る
    }
    // 接続待ちソケットをクローズ
    close(w_addr);
    return (0);
}

client.c

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

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 1024

int communication(int sock)

{
    int send_size;
    int recv_size;
    char send_buf[BUF_SIZE];
    char recv_buf;

    while (1)
    {
        // サーバーに送る文字列を取得
        fgets(send_buf, BUF_SIZE, stdin);
        // 文字列を送信
        send_size = send(sock, send_buf, strlen(send_buf) + 1, 0);
        if (send_size == -1) 
        {
            printf("Error: send()\n");
            break;
        }

        // サーバーからの応答を受信
        recv_size = recv(sock, &recv_buf, 1, 0);
        if (recv_size == -1) 
        {
            printf("Error: recv()\n");
            break;
        }
        if (recv_size == 0) 
        {
            // 受信サイズが0の場合は相手が接続閉じていると判断
            printf("Server is closed\n");
            break;
        }

        // 応答が0の場合はデータ送信終了
        if (recv_buf == 0) 
        {
            printf("Exited from the connection to a server\n");
            break;
        }
    }
    return 0;
}

int main(void) 
{
    int sock;
    struct sockaddr_in addr;

    // ソケットを作成
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1) 
    {
        printf("Error: socket()\n");
        return -1;
    }

    // 構造体を全て0にセット
    bzero(&addr, sizeof(struct sockaddr_in));
    // サーバーのIPアドレスとポートの情報を設定
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    // サーバーに接続要求送信
    printf("Start connect...\n");
    if (connect(sock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) == -1) 
    {
        printf("Error: connect()\n");
        close(sock);
        return (-1);
    }
    printf("Connection is establised!\n");
    printf("Ready to send messages\n");
    // 接続済のソケットでデータのやり取り
    communication(sock);
    // ソケット通信をクローズ
    close(sock);
    return (0);
}

コンパイル

gcc -Wall -Werror -Wextra server.c -o server.exe
gcc -Wall -Werror -Wextra client.c -o client.exe

動作環境

Windows11 (version 22H2)
WSL2 (5.10.102.1-microsoft-standard-WSL2)
Ubuntu (20.04.5 LTS (Focal Fossa))

Discussion