【C言語】ソケット通信について解説

ソケット通信解説ページのアイキャッチ

このページではC言語でのソケット通信の仕方について解説していきたいと思います!

今や「通信」は生活になくてはならないものになりましたよね!

ウェブページやスマホのゲーム、SNS なども「通信」があってこそ成り立つ物ばかりです。

このページでは、その通信をC言語で行うための「ソケット通信」について解説していきたいと思います!

このページはC言語向けの解説になりますが、ソケット通信は他の多くのプログラミング言語でも用意されています。

ソケット通信をする際の処理の流れはどの言語でも同じになりますし、使用する関数(メソッド)もほとんど同じですので、他のプログラミング言語を利用されている方でも役に立つ内容になっていると思います!

また、紹介する関数は MacOSX のものを紹介していますが、おそらく Linux でも同じものが使用できると思います。

Windows の場合はちょっと関数名が違ったり、インクルードするファイルが違ったりするので注意が必要ですが、使い方や関数の動作はほぼ同じだと思いますので参考にはなると思います!

ソケット通信とは

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

で、ソケットっていうのは通信端点のことで、アプリとアプリ(プログラムとプログラム)の通信の出入り口のことです。

ソケットの説明図

ソケット通信を行う際には、まずアプリやプログラム内でソケットを作成し、それを他のアプリやプログラムのソケットに接続してデータのやり取りを行います。

ですので、C言語プログラムでソケット通信を行うためにも、まずこのソケットを作成する必要があります。

そして、このソケットを使用して通信を行うようにプログラミングしていくことになります。

C言語ではこのソケットを、後述する socket 関数により作成することができます。

C言語では、このソケットを利用して接続相手に接続したり、データの送受信を行なったりする関数もたくさん用意されています。

サーバーとクライアント

今回作成するプログラムもサーバーとクライアント間でデータのやり取りを行うものなので、ここで「サーバーとクライアント」について説明しておきます。

サーバーとクライアントはそれぞれ下記のようなものになります。

  • サーバー:クライアントにサービスを提供するソフトウェアやコンピュータ
  • クライアント:サーバーからサービスを受けるソフトウェアやコンピュータ

このサーバーとクライアントで通信を行うことでさまざまなサービスや機能が実現されています。

例えばウェブブラウザでのウェブページの閲覧を考えると分かりやすいです。

この場合、ウェブページを閲覧しようとしている PC やスマホなどがクライアントで、ウェブサーバーがサーバーとなります。

クライアントサーバーモデル

クライアントはウェブサーバーに対して、ウェブページを表示するのに必要なデータをサーバーに要求します。

より具体的には、要求するためのデータを送信します。

サーバーへリクエストを送信する様子

サーバーはそのデータを受信し、その応答としてクライアントに対してそのページを表示するのに必要なデータを送信します。

サーバーから応答を受け取る様子

そして、クライアントはそのデータを受信し、受信したデータ用いてウェブブラウザ上にウェブページを表示します。

サーバーから受け取ったデータでページを表示する様子

こんな感じでクライアントがサーバーに対してサービスを要求し、サーバーがその応答としてサービスを提供するのが「サーバーとクライアント」間の通信の流れになります。

で、この場合は「サービス」がウェブページの表示(のためのデータの供給)になります。

サーバーとクライアントがいるってことは、ソケット通信のプログラム動かすためには PC 2台必要ってこと?
普通はサーバーとクライアントは分かれてるけど、ソケット通信のお試しプログラムなんかでは1つの PC でサーバーとクライアント両方の役割を持たせて動作させるのが普通だよ

要は1つの PC でサーバー用のプログラムとクライアント用のプログラムを実行する感じだね

スポンサーリンク

ソケット通信の簡単な流れ

次は、先ほど説明したような通信を行うための処理の流れを簡単に説明をしていきたいと思います。

サーバーとクライアントとでは大きく分けると下記の3つの手順で通信が実現されます。

  1. サーバーとクライアント間で接続を確立する
  2. サーバーとクライアント間でデータのやり取りをする
  3. サーバーとクライアント間の接続を閉じる
MEMO

上記は TCP によるもので、UDP の場合は 1. を行わずにデータのやり取りを行ったりすることもできるようです

通常、通信を行う目的は 2. になると思います。

先程のウェブページ閲覧の例であれば、通信の目的はウェブページを閲覧するための「データのやり取り」になりますよね。

で、その 2. を行うために 1. と 3. を行う必要があります。これらの処理の詳細について解説していきたいと思います。

接続を確立する

データをサーバーとクライアント間で行うためには、まずこの2つの間で接続を行う必要があります。

ソケットを作成する

まず通信を行うためにソケットを作成します。ソケット通信を行うためには、通信の出入り口になるソケットが必要です。

そして、サーバーとクライアント両方にソケットが必要なので、サーバークライアント両方でソケットを作成する必要があります。

このソケットの作成は socket 関数により行うことができます。

ソケットを作成する様子

サーバーが接続を待つ

サーバーが接続を確立するときに次に行うのが「接続待ち」です。

要はクライアントから接続要求がくるまで待機している感じです。

この接続待ちは listen 関数により行うことができます。

サーバーが接続要求待ちを行う様子

クライアントが接続を要求する

クライアントはサーバーに対して「接続要求」を行います。これによりサーバーが接続要求を受け取ります。

この接続要求は connect 関数により行うことができます。

クライアントが接続要求を送る様子

サーバーが接続を受け付ける

そして、サーバーは受け取った接続要求を受け付けます。

この接続の受け付けは accept 関数により行うことができます。

サーバーが接続を受け付ける様子

実際にここでサーバークライアント間で接続が確立されたことになります。

ソケットとソケットの間が線で結ばれるイメージです。

クライアント・サーバー間で接続が確立された様子

データのやり取りを行う

ソケット同士の接続が確立されたら、次はそのソケットを用いて通信の目的となるデータのやり取りを行います。

データのやり取りとは、具体的には次の2つになります。

  • データの送信(send 関数)
  • データの受信(recv 関数)

要は、サーバーもしくはクライアントの一方がデータを送信し、他方がその送信されたデータを受信するという流れですね。

で、これを必要なデータを送信 or 受信するまで繰り返します。

途中で送信する側と受信する側が入れ替わることもあります。

好きなタイミングで送信や受信しちゃっていいの?
特に今回みたいにお試しでソケット通信を試してみる分には好きなようにプログラミングすればいいんだけど、上手く動作させるためにはポイントがあるよ

で、ここでのポイントは「サーバーとクライアントで息を合わせる必要がある」という点になります。

要はサーバーとクライアントとで受信と送信のタイミングを合わせる必要があります。

例えば接続確立後にサーバーとクライアント双方でデータの受信を行おうとしても、データが送信されて来ないのでデータのやり取りが上手く行きません。

そういうことがないように、通信によっては例えば下記のようなことが定められている場合があります。

  • サーバーとクライアントのどちらが
  • どのタイミングで
  • どんなデータを
  • 送信 or 受信するか

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

例えばウェブページを要求するときには HTTP というプロトコルに従ってデータのやり取りを行います(HTTP の P はプロトコルの頭文字です)。

ですので、例えばウェブページを要求するクライアントプログラムを作成する場合は、このプロトコルに従ってデータの送受信しないとウェブサーバーと話が合わなくて上手くデータのやり取りが行えません。

が、今回は単にサーバーとクライアントの通信のサンプルプログラムを作るだけですので、プロトコルは気にせずにプログラミングを行っていきたいと思います(ただしサーバーとクライアントで息を合わせる必要はあります)。

プロトコルってなんか堅苦しいなぁ…

でも、例えばウェブサイトごとに好き勝手データの通信をやってしまうと、ウェブブラウザをサイトごとに適用させなきゃいけないから大変だよね

このプロトコルがあるから、どのウェブサイトも同じデータのやり取りでページ表示ができるんだよ

なるほど…

スポンサーリンク

接続を閉じる

一通りデータのやり取りが終われば、最後に接続を閉じます。これによりサーバーとクライアント間の接続が解消されます。

接続を閉じる処理は close 関数により行うことができます。

接続を閉じる様子

一連の流れを簡単に説明するとこんな感じです。

ソケット通信で利用する関数

では、ここまで解説してきた通信の流れを実現するために必要なソケット通信で利用する関数について解説しておきたいと思います。

ソケットを作成する:socket

ソケット通信を行うためにはソケットが必要で、このソケットを作成するのが socket 関数になります。

socket
#include <sys/socket.h>

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

socket 関数の戻り値は int 型のデータになりますが、これが作成した「ソケットの識別子」となり、後に説明する関数ではこのソケットの識別子を指定して通信を行うことになります。

socket 関数の引数は下記の3つになります。

  • domain:プロトコルファミリー(アドレスファミリー)を指定
  • type:ソケットのタイプを指定
  • protocol:使用するプロトコルを指定

正直私もこの引数については詳しくないです…。

今回は下記の引数設定で socket 関数を実行します。要は IPv4 で TCP プロトコルを使用する設定になります。

socket関数の引数
sock = socket(AF_INET, SOCK_STREAM, 0);

スポンサーリンク

ソケットを関連付ける:bind

特にサーバー側で、ソケット作成後に行うのが bind です。

bind
#include <sys/socket.h>

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

bind はソケットに IP アドレスやポート番号を設定する関数です。

なので、引数には設定先のソケットとソケットに設定する情報(さらにはその情報のサイズ)を指定します。

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

サーバー側では、どの IP アドレス&どのポート番号で接続を待つかを設定することになります。

例えばパソコンはいくつも IP アドレスを持っていたりするので、その中のどの IP アドレスに対しての接続を受け付けるかをソケットに設定する必要があります。

ポート番号も同様ですね。どのポート番号に対する接続を受け付けるかを設定する必要があります。

で、この設定を行うのが bind になります。

接続を待つ:listen

bind 後にサーバーが行うのがこの listen です。

listen を実行することで、ソケットが接続待ち状態になります。

サーバーが接続要求待ちを行う様子

そして、このソケットが接続待ち状態になることで、次に説明するクライアントからの connect による接続要求が可能になります。

クライアントが接続要求を送る様子

listen 関数の定義は下記のようになります。

listen
#include <sys/socket.h>

int listen(int sockfd, int backlog);

listen 関数の引数は下記のようになります。

  • sockfd:接続を待つソケット
  • backlog:接続要求を保持する数

backlog については accept の説明で詳細を説明します。

接続を要求する:connect

この connect はソケット作成後にクライアントが実行する関数です。

connect
#include <sys/socket.h>

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

引数は下記のようになります。

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

connect 関数では「どこに接続するか」の情報を指定する必要があり、それを第2引数 addr に設定する必要があります。

より具体的には、接続先はサーバーなので、その接続先のサーバーの IP アドレスやポート番号を addr で指定します。

スポンサーリンク

接続を受け付ける:accept

で、サーバーが listen で接続待ち状態になり、さらにクライアントが connect を実行することで、サーバーに接続要求が送られてくることになります。

次にサーバーが行うのが接続の受け付けです。この接続の受け付けを行うのが accept 関数です。

listen では接続待ち状態になるだけなので、クライアントから connect が来ても、単純に接続要求がサーバーに溜まるだけです。

で、その接続要求を受け付けるのが accept 関数です。これを実行することで接続が確立され、その後のデータのやり取りを行うことができるようになります。

サーバーが接続を受け付ける様子

listenaccept の関係はキューで考えるとわかりやすいです。

listen は接続要求を溜めておくためのキューを作成する関数です。

listenでキューを作成する様子

で、connect によりクライアントから来た接続要求はこのキューに溜められていきます。

connectでキューに接続情報が溜まる様子

さらに accept ではそのキューからその接続要求を取得し、その接続要求を受け付け、実際のデータのやり取りを行います。

acceptでキューから接続情報を取得して接続を受け付ける様子

サーバーは複数のクライアントを相手にすることも多いので、接続要求が来たときにすぐにデータのやり取りを行ったりすると処理が追いつかないような場合があり得ます。

ですが、listen でキューを作成することで接続要求を溜めておくことができるようになります。

ですので、サーバーは接続要求が来たタイミングではなく、サーバー自身のタイミングが良いとき(例えば他のクライアントとデータのやり取りしていないとき)に接続を受け付け、実際のデータのやり取りを行うようなことが可能になります。

で、このキューのサイズは listen の第2引数で指定する backlog で指定した値となります。

要は、accept していない接続要求を最大いくつまで溜めておくかを指定するのが backlog になります。

この accept 関数の定義は下記のようになります。

accept
#include <sys/socket.h>

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

引数は下記のようになります。

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

accept 関数のポイントは2つです。

1つ目は「接続要求がくるまでは関数が終了しない」ことです。要はクライアントから connect が実行されないと関数が終了せず、関数の中でプログラムが待たされることになります。

2つ目は accept 関数はソケットを作成し「戻り値がその作成したソケットの識別子」であることです。

この作成されたソケットが接続要求を受け付けたクライアントと「接続済のソケット」になります。

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

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

データを送信する:send

ここまででサーバーとクライアントとの接続が確立された(つまりソケットが接続済になった)ので、続いて通信の目的でもあるデータのやり取りを行う関数を紹介していきます。

まず紹介するのが send 関数です。send 関数は、接続先に対してデータを送信する関数です。

send
#include <sys/socket.h>

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

send 関数の引数は下記のようになります。

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

sockfd は接続済のソケットで、このソケットに送信先の情報(IP アドレスやポート番号など)が設定されています。

接続待ち状態のソケットなど、接続済でないソケットを指定するとエラーになるので注意してください。

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

データを受信する:recv

次に紹介するのが recv 関数です。recv 関数は、接続先が送信したデータを受信する関数です。

recv
#include <sys/socket.h>

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

recv 関数の引数は下記のようになります。

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

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

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

クライアントサーバー間では、一方が send 関数で送信したデータを、もう一方が recv 関数でデータを受信することを繰り返すことで、データのやり取りを行います。

このときに重要なのは前述の通り「サーバークライアント間で息を合わせる」ことです。

どちらがどのタイミングで send 関数 or recv 関数を実行するかを上手く設計しないと、お見合い状態になるようなこともあるので注意してください。

例えば両方が recv 関数でデータ受信待ちになるとデータのやり取りがそこで止まってしまいます。

スポンサーリンク

接続を閉じる:close

データのやり取りが一通り終わった際には、最後に close 関数で接続を閉じます。

close
#include <unistd.h> 

int close(int fd);

引数 fd にはソケットの識別子を指定します。これにより接続が閉じられ、一連の通信処理が終了します。

ソケット通信の簡単なプログラム例

では、ここまで解説してきた内容を踏まえてソケット通信の簡単なプログラムを紹介していきたいと思います。

MEMO

プログラムは MacOSX で作成・動作確認したものになりますが、おそらく Linux でも動作するのではないかと思います

Windows の場合はインクルードするファイルが違ったりするので注意が必要ですが、各関数の名前や実行するタイミングなどはほぼ同じになると思います

作成するプログラムは「サーバー用」「クライアント用」の2つになります。つまりソースコードも2つ紹介します。

サーバープログラム

サーバー用のプログラムは下記のようになります。

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

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

int transfer(int);

int transfer(int sock) {

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

    while (1) {

        /* クライアントから文字列を受信 */
        recv_size = recv(sock, recv_buf, BUF_SIZE, 0);
        if (recv_size == -1) {
            printf("recv error\n");
            break;
        }
        if (recv_size == 0) {
            /* 受信サイズが0の場合は相手が接続閉じていると判断 */
            printf("connection ended\n");
            break;
        }
        
        /* 受信した文字列を表示 */
        printf("%s\n", recv_buf);
        
        /* 文字列が"finish"ならクライアントとの接続終了 */
        if (strcmp(recv_buf, "finish") == 0) {

            /* 接続終了を表す0を送信 */
            send_buf = 0;
            send_size = send(sock, &send_buf, 1, 0);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
            break;
        } else {
            /* "finish"以外の場合はクライアントとの接続を継続 */
            send_buf = 1;
            send_size = send(sock, &send_buf, 1, 0);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
        }
    }

    return 0;
}

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

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

    /* 構造体を全て0にセット */
    memset(&a_addr, 0, 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) {
        printf("bind error\n");
        close(w_addr);
        return -1;
    }

    /* ソケットを接続待ちに設定 */
    if (listen(w_addr, 3) == -1) {
        printf("listen error\n");
        close(w_addr);
        return -1;
    }

    while (1) {
        /* 接続要求の受け付け(接続要求くるまで待ち) */
        printf("Waiting connect...\n");
        c_sock = accept(w_addr, NULL, NULL);
        if (c_sock == -1) {
            printf("accept error\n");
            close(w_addr);
            return -1;
        }
        printf("Connected!!\n");

        /* 接続済のソケットでデータのやり取り */
        transfer(c_sock);

        /* ソケット通信をクローズ */
        close(c_sock);

        /* 次の接続要求の受け付けに移る */
    }

    /* 接続待ちソケットをクローズ */
    close(w_addr);

    return 0;
}

スポンサーリンク

クライアントプログラム

クライアント用のプログラムは下記のようになります。

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

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

int transfer(int);

int transfer(int sock) {
    char send_buf[BUF_SIZE], recv_buf;
    int send_size, recv_size;

    while (1) {

        /* サーバーに送る文字列を取得 */
        printf("Input Message...\n");
        scanf("%s", send_buf);

        /* 文字列を送信 */
        send_size = send(sock, send_buf, strlen(send_buf) + 1, 0);
        if (send_size == -1) {
            printf("send error\n");
            break;
        }

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

        /* 応答が0の場合はデータ送信終了 */
        if (recv_buf == 0) {
            printf("Finish connection\n");
            break;
        }
    }
    return 0;
}

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


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

    /* 構造体を全て0にセット */
    memset(&addr, 0, 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("connect error\n");
        close(sock);
        return -1;
    }
    printf("Finish connect!\n");

    /* 接続済のソケットでデータのやり取り */
    transfer(sock);

    /* ソケット通信をクローズ */
    close(sock);

    return 0;
}

プログラムの解説

基本的にここまで解説してきた内容や関数に基づいて作成しているので、ソースコードを読んでいただければ何をやっているかは理解していただけるのではないかと思います。

ちょっとややこしいのが sockaddr_in 構造体の設定の仕方だと思います。

またデータのやり取り部分はプロトコルは意識せずに私が独自に決めたやり取りを行なっているので、この2点についてのみ解説しておきたいと思います。

sockaddr_in 構造体の設定

サーバー・クライアントともに sockaddr_in 構造体を利用し、この構造体のメンバに情報を詰め、サーバーでは bind、クライアントでは connect を行なっています(実際には関数の引数指定時に sockaddr 構造体にキャストされている)。

bind ではサーバー自身のソケットの設定を行なうために sockaddr_in のメンバを指定しています。なので、sockaddr_in に設定する情報は、サーバーの IP アドレスとポート番号になります。

また、connect では接続先(つまりサーバー)を設定するために sockaddr_in のメンバをしています。なので、sockaddr_in に設定する情報は、サーバーの IP アドレスとポート番号になります。

つまり、意味合いは違いますが、両方で sockaddr_in に設定する情報は同じで、サーバーの IP アドレスとポート番号を設定しています。

サーバーの IP アドレスとしては "127.0.0.1"(SERVER_ADDR) を設定しています。これはループバックアドレスと呼ばれ、プログラムを実行する PC 自身のアドレスを示すアドレスになります。

なので、今回のプログラムでは、サーバーはソケットに PC 自身のアドレスを設定し、クライアントは connectで接続要求を送る先のアドレスを PC 自身のアドレスに設定している事になります。

つまり、クライアントが connect を実行すると、同じ PC のサーバーのソケットに接続しにいくようになります。

こんな感じで1台でサーバークライアント間での通信を行う場合などに "127.0.0.1" を使うと便利なので覚えておくと良いと思います。

ただし、IP アドレスを設定する sockaddr_in 構造体の sin_addr.s_addr メンバに設定する必要があるのは数値であり、"127.0.0.1" といった文字列ではありません。

この IP アドレスを表した文字列を数値に変換する必要があるので、inet_addr 関数を使ってこの変換を行なっています(同時に inet_addr 関数では次に説明する htons 同様にホストバイトオーダーをネットワークバイトオーダーに変換する効果もあります)。

ポート番号としては 8080(SERVER_PORT) を指定しています。PC 上で利用されている他のポートと被らないように設定すれば良いので、他の数値でも問題ありません。

ただし、sockaddr_in 構造体の sin_port にこのままポート番号を設定してはダメです。

これは、sin_port にはネットワークバイトオーダーのデータを設定する必要があるためです。要は PC 上でのデータの扱い方(ホストバイトオーダー)とネットワーク上でのデータの扱い方が異なるので、ネットワーク上でのデータに変換してから sin_port に設定する必要があります。

この変換を行うのが htons になります。

上記を考慮して、クライアントサーバーともに下記のように sockaddr_in 構造体を設定しています。

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);

AF_INET は設定する IP アドレスが IPv4 であることを指定するためのものになります。

こんな感じで、sockaddr_in 構造体のメンバに設定する項目自体は割と分かりやすいですが、設定する前に htonsinet_addr 等を用いて sockaddr_in 構造体が期待する形式に変換する必要がある点に注意です。

データのやり取り(transfer

接続確立後に行うデータのやり取りはサーバー・クライアントともに transfer 関数の中で行なっています。

この transfer 関数で行う具体的なデータのやり取りは下記になります。

  • クライアントがサーバーに文字列を送信する
  • サーバーがクライアントが送信した文字列を受信する
    • サーバーは受信した文字列を表示する
  • サーバーがクライアントに接続を継続するかどうかを示すデータを送信する
    • 受信した文字列が "finish" の場合は接続を継続しないことを示すデータ(0)を送信する
    • それ以外の文字列の場合は接続を継続することを示すデータ(1)を送信する
  • クライントが接続を継続するかどうかを示すデータを受信する
    • 0 を受信した場合は接続を閉じる
    • 1 を受信した場合は最初に戻って文字列を送信する

なので、このプログラムにおけるサーバーが提供するサービスは「受信した文字列を表示する」という単純なものになります。

また、このやり取りを行うために、サーバーはまず recv でデータ(文字列)が送信されてくるのを待機し、逆にクライアントはまず send でデータ(文字列)の送信を行うようにしています。

さらに、クライアントは send 実行後は接続を継続するかどうかを示すデータを受信するために recv を行い、逆にサーバーは文字列受信後に文字列を表示した後に、接続を継続するかどうかを示すデータを送信するために send を実行しています。

こんな感じで、両方が同じタイミングで recv を実行したりしてデータのやり取りが止まってしまわないように、お互いに sendrecv を実行するタイミングの息を合わせるように transfer 関数を作成しています。

プログラムの実行方法

実行するプログラムはサーバー用のものとクライアント用のものの2つですので、2つのソースコードのコンパイルと2つのプログラムの実行が必要になります。

ポイントは2つのプログラムを同時に実行する必要があるところです。

例えば Mac や Linux のターミナルでは、基本的に1つのウィンドウ(タブ)で実行できるのは1つのプログラムだけです。

なので、ターミナルウィンドウを2つ立ち上げ、それぞれのウィンドウでサーバー用のプログラムとクライアント用のプログラムを実行する必要があります。

上記はターミナルでプログラムを実行するときの説明ですが、要はプログラムを同時に2つ実行できればそれで良いです。

コンパイル

コンパイルはいつも通りに行うことが可能です。

例えばターミナルで gcc を用いてコンパイルするのであれば、ソースコード server.cclient.c を置いている場所に移動し、下記を実行することでコンパイルが行われます。

gcc server.c -o server.exe
gcc client.c -o client.exe

プログラムの実行

上記を実行することで server.execlient.exe が生成されますので、これらを実行することでプログラムを起動することができます。

例えばターミナルからプログラムを実行するのであれば、まず server.exe を1つ目のターミナルウィンドウから実行します。

server.exe では listen が実行され、ソケットは接続待ち状態になり、さらには accept が実行されて接続要求がくるまで待機している状態になります。

1つ目のターミナル
% ./server.exe
Waiting connect...

続いて2つ目のターミナルウィンドウから client.exe を実行します。

2つ目のターミナル
% ./client.exe
Start connect...
Finish connect!
Input Message...

client.exeserver.exe が作成して bind したソケットに対して connect を行います。server.exe のソケットはすでに接続待ちの状態ですので、client.execonnect に成功して「データのやり取り」に移行します。

一方で、server.execlient.exe の接続要求を受け付けることで accept が完了し、データのやり取りに移行します。

1つ目のターミナル
% ./server.exe
Waiting connect...
Connected!!

接続確立後は、client.exe に文字列を入力すれば、それを client.exeserver.exe に送信し、それを受信した server.exe が受信した文字列をターミナル表示する、というデータのやり取りを行います(実際にまだ接続を継続するかどうかのデータのやり取りも行なっています)。

client.exe に文字列を入力してからエンターキーを押すと、server.exe 側でその文字列が表示されることが確認できると思います。

また、server.exe が "finish" を受信した場合は、接続が終了するようにしています。

また、このプログラムでは下記のような現象が発生します。なぜこのような事になるかは解説の中で説明していますので、気になる方&理由がわからない方はぜひ解説を読み返してみてください。

  • server.exe よりも  client.exe の方を先に起動すると接続できない
  • client.exe をたくさん立ち上げると接続が行われなくなる

スポンサーリンク

まとめ

このページではC言語でのソケット通信を行う流れの説明や、ソケット通信に使用する関数の紹介、さらには実際のソケット通信を行うプログラムの紹介を行いました!

サーバーやクライアントの役割や、ソケット通信を行う際にそれぞれで実行する関数について理解していただけたのではないかと思います。

通信は今や生活になくてはならないものですよね!

このサイトも通信によって閲覧していただいていますし、今やいろんな物が通信で繋がることで便利な機能がたくさん実現されています。で、この通信はソケット通信で行われているケースが多いです。

今後もさらに通信の需要も増えていくと思いますし、プログラマーやエンジニアにとっても必要な知識になっていますので、是非このページで解説したソケット通信についても覚えておいていただければと思います!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です