C言語でのソケット通信の究極ガイド!実例とコードを交えた14ステップ

C言語とソケット通信のイメージ図C言語
この記事は約27分で読めます。

 

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

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

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

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

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

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

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

はじめに

ソケット通信とは、通信を行うためのAPI(Application Programming Interface)で、通信を行うためのソケットという抽象化された端点を提供します。

ソケット通信は、インターネットや内部ネットワークなどを介してコンピュータ間でデータをやり取りする際に、一般的に使用される手法です。

ソケットは通信の両端点に存在し、データ送信側のソケットからデータが送られ、データ受信側のソケットでそのデータが受け取られます。

このため、ソケット通信を行うには、送信側と受信側の両方でソケットを作成し、適切に接続を行う必要があります。

それでは、C言語を用いてソケット通信を行う基本的な手順を見ていきましょう。

●C言語でのソケット通信の基本

まず、ソケット通信を行うためには、次の基本的な手順を実行する必要があります。

○ソケットの作成

ソケットの作成は、socket関数を用いて行います。

この関数は、ソケットを生成し、そのソケットに対するディスクリプタ(一意の識別子)を返します。

IPv4でTCP通信を行うソケットの作成例を紹介します。

#include <sys/types.h>
#include <sys/socket.h>

int main() {
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        /* エラーハンドリング */
    }
    /* 続きの処理 */
    return 0;
}

このコードでは、socket関数により新たなソケットが作成され、そのディスクリプタがsockfdに格納されています。

この例では、エラーが発生した場合(socket関数の戻り値が-1の場合)には、適切なエラーハンドリングが行われることを表しています。

○サーバーとの接続

ソケットが作成されたら、次に行うべきはサーバーとの接続です。

サーバーとの接続は、connect関数を用いて行います。

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

int main() {
    int sockfd;
    struct sockaddr_in addr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        /* エラーハンドリング */
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        /* エラーハンドリング */
    }
    /* 続きの処理 */
    return 0;
}

このコードでは、作成されたソケットを用いてサーバーとの接続が行われています。

接続先の情報は、sockaddr_in構造体を用いて指定します。

この例では、ローカルホストの12345番ポートに接続を試みています。

また、connect関数が0未満の値を返した場合にはエラーハンドリングが行われます。

○データの送受信

サーバーとの接続が成功したら、次はデータの送受信を行います。

データの送信はsend関数を、受信はrecv関数を用いて行います。

“Hello, world!”というメッセージを送信し、その後サーバーからの応答を受け取る例を紹介します。

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

#define BUFFER_SIZE 256

int main() {
    int sockfd;
    struct sockaddr_in addr;
    char buffer[BUFFER_SIZE];

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        /* エラーハンドリング */
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        /* エラーハンドリング */
    }

    if (send(sockfd, "Hello, world!", 14, 0) == -1) {
        /* エラーハンドリング */
    }

    memset(buffer, 0, BUFFER_SIZE);
    if (recv(sockfd, buffer, BUFFER_SIZE-1, 0) == -1) {
        /* エラーハンドリング */
    }
    /* 続きの処理 */
    return 0;
}

このコードでは、”Hello, world!”というメッセージがサーバーに送信され、その後サーバーからの応答がbufferに格納されています。

send関数やrecv関数が-1を返した場合には、エラーハンドリングが行われます。

○ソケットのクローズ

最後に、通信が終了したらソケットをクローズします。

ソケットのクローズは、close関数を用いて行います。

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

int main

() {
    int sockfd;
    struct sockaddr_in addr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        /* エラーハンドリング */
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        /* エラーハンドリング */
    }
    /* 続きの処理 */
    close(sockfd);
    return 0;
}

このコードでは、ソケットがクローズされています。

クローズすることで、そのソケットが占有していたリソースが解放され、他のソケットで使用できるようになります。

また、通信相手に対しても通信終了の意を伝えることができます。

●ソケット通信のサンプルコード1:エコーサーバーの作成

今回紹介するコードはエコーサーバーを作成するものです。

エコーサーバーは、クライアントから受け取ったデータをそのまま返すというシンプルなサーバーです。

サーバーとクライアントの基本的なやり取りを理解するのに適した例です。

また、データの送受信方法や、多重接続についての理解を深めるための素晴らしいスタートポイントとなります。

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

#define BUF_SIZE 256

int main() {
    int sockfd, new_sockfd;
    struct sockaddr_in addr;
    socklen_t addr_size;
    char buffer[BUF_SIZE];

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

    listen(sockfd, 5);

    addr_size = sizeof(addr);
    new_sockfd = accept(sockfd, (struct sockaddr *)&addr, &addr_size);

    while(1) {
        memset(buffer, 0, BUF_SIZE);
        read(new_sockfd, buffer, BUF_SIZE);
        write(new_sockfd, buffer, strlen(buffer));
    }

    close(new_sockfd);
    close(sockfd);

    return 0;
}

○コードの解説

このコードでは、まずソケットを作成しています。

その後、ソケットにアドレスとポート番号を関連付け(bind)、ソケットが接続要求を待ち受けるように設定(listen)します。

これでサーバー側のソケットの設定が完了します。

次に、クライアントからの接続要求を待ち、接続要求が来たら新たなソケットを作成(accept)して、そのソケットでデータの送受信を行います。

具体的には、無限ループの中で、クライアントから送られてきたデータを読み込み(read)、そのデータをそのままクライアントに送り返す(write)という処理を繰り返しています。

最後に、新たに作成したソケットと元々のソケットを閉じる(close)ことで、使用していたリソースを解放します。

このコードを実行すると、クライアントから送られてきたメッセージをそのまま返すエコーサーバーが起動します。

具体的な動作としては、クライアントから「Hello, Server!」というメッセージが送られてきたとすると、サーバーはそのメッセージを読み取り、同じ「Hello, Server!」というメッセージをクライアントに返します。

このエコーサーバーの例を通じて、ソケット通信の基本的な流れと、データの送受信の基本的な方法を理解することができます。

●ソケット通信のサンプルコード2:チャットサーバーの作成

実際のチャットサーバーの作成を通じて、ソケット通信の応用例を理解していきましょう。

ここでは、1対1の通信ではなく、複数のクライアントが同時にサーバーに接続し、それぞれがメッセージを送信できるようなシステムを作成します。

まずは、次のサンプルコードをご覧ください。

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

#define BUF_SIZE 100
#define MAX_CLNT 256

void * handle_clnt(void * arg);
void send_msg(char * msg, int len);

int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    int clnt_adr_sz;
    pthread_t t_id;
    if(argc!=2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    pthread_mutex_init(&mutx, NULL);
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");

    while(1)
    {
        clnt_adr_sz=sizeof(clnt_adr);
        clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);

        pthread_mutex_lock(&mutx);
        clnt_socks[clnt_cnt++]=clnt_sock;
        pthread_mutex_unlock(&mutx);

        pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
        pthread_detach(t_id);
        printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
    }
    close(serv_sock);
    return 0;
}

void * handle_clnt(void * arg)
{
    int clnt_sock=*((int*)arg);
    int str_len=0, i;
    char msg[BUF_SIZE];

    while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)
        send_msg(msg, str_len);

    pthread_mutex_lock(&mutx);
    for(i=0; i<clnt_cnt; i++)  /* remove disconnected client */
    {
        if(clnt_sock==clnt_socks[i])
        {
            while(i++<clnt_cnt-1)
                clnt_socks[i]=clnt_socks[i+1];
            break;
        }
    }
    clnt_cnt--;
    pthread_mutex_unlock(&mutx);
    close(clnt_sock);
    return NULL;
}

void send_msg(char * msg, int len)   /* send to all */
{
    int i;
    pthread_mutex_lock(&mutx);
    for(i=0; i<clnt_cnt; i++)
        write(clnt_socks[i], msg, len);
    pthread_mutex_unlock(&mutx);
}

このコードでは、ソケット通信を使ってチャットサーバーを作成しています。

この例では、複数のクライアントから同時に接続を受け付け、それぞれから送信されたメッセージを全てのクライアントにブロードキャストしています。

具体的には、サーバーソケットを作成し、クライアントからの接続要求を待ち、接続があるたびに新たなスレッドを生成してクライアントとの通信を行っています。

それぞれのクライアントは自身のソケットを通じてメッセージをサーバーに送信し、そのメッセージはサーバー上でブロードキャストされ、全ての接続されたクライアントに送信されます。

これにより、チャットルームのような環境を作り出しています。

このコードの中には重要な要素が多く含まれていますが、特にpthread_createを使ったスレッドの生成と、そのスレッド内でのメッセージの読み取り・送信は、マルチクライアント環境を理解するうえで非常に重要です。

また、pthread_mutex_lockpthread_mutex_unlockを用いたスレッド間でのデータ競合(race condition)の回避も重要なポイントです。

次に、このコードを実行した場合の結果について見ていきましょう。

まずサーバーを起動し、その後で複数のクライアントを起動します。それぞれのクライアントがメッセージを送信すると、それがサーバーによって他の全てのクライアントにブロードキャストされます。

したがって、一つのクライアントが送信したメッセージは、全てのクライアントに表示されることになります。

このように、このサンプルコードを通じて、複数のクライアントが同時にサーバーに接続し、リアルタイムにデータの送受信を行うソケット通信の基本を学びました。

ここから更に応用して、例えば複数のクライアントが同時にアクセス可能なWebサーバーやデータベースサーバーを作成することも可能です。

●ソケット通信のサンプルコード3:ファイル送信サーバーの作成

今回のステップでは、ソケット通信を用いてファイルを送受信するサーバーの作成について解説します。

これは一部のネットワークプログラムでよく使われるテクニックであり、FTP(File Transfer Protocol)などもこれに相当します。

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

#define MAX_BUF 1024

int main(int argc, char *argv[])
{
    struct sockaddr_in server;
    int sock;
    int fd;
    int len;
    int read_len;
    char buf[MAX_BUF];
    char *fname = "test.txt";

    /* サーバーの情報を設定 */
    server.sin_family = AF_INET;
    server.sin_port = htons(12345);
    server.sin_addr.s_addr = inet_addr("127.0.0.1");

    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_STREAM, 0);

    /* サーバーに接続 */
    connect(sock, (struct sockaddr *)&server, sizeof(server));

    /* ファイルを開く */
    fd = open(fname, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    /* ファイルの内容を読み込み、ソケットに送信 */
    while (1) {
        len = read(fd, buf, MAX_BUF);
        if (len == -1) {
            perror("read");
            return 1;
        }
        if (len == 0) break;

        write(sock, buf, len);
    }

    /* ファイルとソケットを閉じる */
    close(fd);
    close(sock);

    return 0;
}

○コードの解説

上記のコードはファイル送信サーバーを作成するためのものです。

ここでは、’test.txt’という名前のファイルを読み込んでその内容をサーバーに送信します。

まず、ソケットの作成から始まります。この段階でエラーチェックをすることは重要です。

その後、ファイルを開き、その内容を読み込みます。そして、読み込んだ内容をサーバーに送信します。

最後に、開いたファイルとソケットを閉じます。

これはリソースの無駄遣いを防ぐために重要なステップです。

このコードを実行すると、指定したファイルの内容がサーバーに送信されます。

また、エラーが発生した場合には適切なエラーメッセージが表示されます。

●ソケット通信のサンプルコード4:マルチスレッドサーバーの作成

ソケット通信の応用例として、C言語でのマルチスレッドサーバーの作成を見ていきましょう。

マルチスレッドプログラミングを用いると、複数のクライアントからのリクエストを同時に処理することが可能となります。

// 必要なヘッダーファイルをインクルード
#include<stdio.h>
#include<string.h>   
#include<pthread.h> 
#include<stdlib.h>  
#include<unistd.h>  
#include<arpa/inet.h>

// クライアントからのリクエストを処理するスレッドの関数
void *connection_handler(void *socket_desc)
{
    // クライアントソケットのディスクリプタを取得
    int sock = *(int*)socket_desc;
    int read_size;
    char client_message[2000];

    // クライアントからのデータを受信
    while( (read_size = recv(sock , client_message , 2000 , 0)) > 0 )
    {
        // 受信データをクライアントにエコーバック
        write(sock , client_message , strlen(client_message));
    }

    if(read_size == 0)
    {
        printf("クライアントが接続を切断しました\n");
        fflush(stdout);
    }
    else if(read_size == -1)
    {
        perror("recv関数が失敗しました");
    }

    // スレッド終了
    free(socket_desc);
    return 0;
}

int main(int argc , char *argv[])
{
    int socket_desc , new_socket , c , *new_sock;
    struct sockaddr_in server , client;

    // ソケットを作成
    socket_desc = socket(AF_INET , SOCK_STREAM , 0);
    if (socket_desc == -1)
    {
        printf("ソケットの作成に失敗しました");
    }
    puts("ソケットの作成に成功しました");

    // sockaddr_in構造体に設定をする
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( 8888 );

    // ソケットに設定を適用
    if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0)
    {
        perror("bind関数が失敗しました");
        return 1;
    }
    puts("bindに成功しました");

    // リスニング開始
    listen(socket_desc , 3);

    puts("クライアントからの接続を待機しています");
    c = sizeof(struct sockaddr_in);

    // クライアントからの接続要求を待機
    while( (new_socket = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c)) )
    {
        puts("接続が受け入れられました");

        // スレッドを作成
        pthread_t sniffer_thread;
        new_sock = malloc(1);
        *new_sock = new_socket;

        if( pthread_create( &sniffer_thread , NULL ,  connection_handler , (void*) new_sock) < 0)
        {
            perror("スレッドの作成に失敗しました");
            return 1;
        }

        puts("ハンドラーを割り当てました");
    }

    if (new_socket<0)
    {
        perror("accept関数が失敗しました");
        return 1;
    }

    return 0;
}

○コードの解説

このコードでは、新規に接続されるクライアントごとにスレッドを作成しています。

スレッドは個々のクライアントからのメッセージを受け取り、そのメッセージをエコーバックします。

スレッド関数は void *connection_handler(void *socket_desc) で定義しています。

この関数はクライアントソケットのディスクリプタを引数として受け取り、そのソケットを通じてクライアントからデータを受け取ります。

メイン関数はまず、サーバーソケットを作成します。

次に、サーバーのアドレスとポート番号を設定します。bind() 関数を用いて、サーバーソケットにこれらの設定を適用します。

そして listen() 関数を用いて、サーバーソケットでクライアントからの接続要求を待ちます。

新たなクライアントからの接続があると、accept() 関数が新たなソケットを作成します。

この新たなソケットは、サーバーとクライアント間の通信に用いられます。

新たなスレッドがこのソケットを引数として connection_handler() 関数を呼び出し、このスレッドはクライアントとの通信を独立して処理します。

このマルチスレッドサーバーを起動し、複数のクライアントから同時に接続すると、サーバーは各クライアントからのメッセージを独立して受け取り、そのメッセージを各クライアントにエコーバックします。

これは、一つのサーバーが複数のクライアントと同時に通信できることを表しています。

大規模なネットワークアプリケーションで多くのクライアントからのリクエストを効率よく処理するためには、このようなマルチスレッドサーバーの設計が必要となります。

●注意点と対処法

ソケット通信を行う上で注意すべきポイントとその対処法について解説します。

ソケット通信における一般的な問題点は、主に「ブロッキングとノンブロッキングの違い」と「エラーハンドリングの方法」の2つです。

○ブロッキングとノンブロッキングの違い

ブロッキングとは、ある操作が完了するまでプログラムの進行が止まる状態を指します。

一方、ノンブロッキングは逆に、操作が終了するのを待たずにプログラムが進行する状態を指します。

ソケット通信においては、データの送受信時にこのブロッキングとノンブロッキングが重要な役割を果たします。

ブロッキングモードのソケットでは、データが利用可能になるまで送受信関数が戻らないため、効率的な通信を阻害する可能性があります。

一方で、ノンブロッキングモードではデータが利用可能でなくても送受信関数がすぐに戻るため、他の作業を並行して進めることが可能です。

下記のコードでは、ソケットをノンブロッキングに設定する方法を紹介しています。

#include <fcntl.h>
// sockfdはソケットディスクリプタ
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

このコードでは、fcntl関数を用いてソケットをノンブロッキングモードに設定しています。

まず、fcntl関数を使って現在のファイル状態フラグを取得します。

そして、取得したフラグにO_NONBLOCKフラグを追加して、再度fcntl関数で設定します。

これにより、ソケットがノンブロッキングモードになります。

○エラーハンドリングの方法

ソケット通信において、エラーハンドリングは重要な部分です。

通信中に何か問題が発生した場合、適切なエラーハンドリングを行うことでプログラムの安定性を保つことができます。

下記のコードは、ソケット関数の戻り値を確認し、エラーが発生した場合にエラーメッセージを表示する方法を表しています。

#include <stdio.h>
#include <stdlib.h>
// sockfdはソケットディスクリプタ、bufは送信データ、lenは送信データの長さ
ssize_t n = send(sockfd, buf, len, 0);
if (n < 0) {
    perror("send error");
    exit(1);
}

このコードでは、send関数を使用してデータを送信しています。

戻り値が0未満の場合、エラーが発生していることを表しています。

その場合、perror関数でエラーメッセージを表示し、exit関数でプログラムを終了しています。

エラーハンドリングの詳細は状況により異なりますが、最低限、各ソケット関数の戻り値を確認し、エラーが発生した場合には適切な処理を行うことが重要です。

これらの注意点と対処法を理解し、適切にコーディングすることで、ソケット通信を安全かつ効率的に行うことが可能となります。

●ソケット通信のカスタマイズ方法

ここまでのステップでは、C言語でのソケット通信の基本的な流れと、その具体的なコード例を紹介してきました。

さらに深い理解と応用のために、次にソケット通信のカスタマイズ方法を解説します。

具体的には、送受信バッファの調整とタイムアウトの設定について説明します。

○送受信バッファの調整

送受信バッファは、ソケット通信における重要なパラメータの一つです。

バッファとは、一時的にデータを蓄える領域のことで、そのサイズによって通信速度やエラー発生率に影響を及ぼします。

バッファのサイズは「setsockopt」関数を使って調整することが可能です。

例えば、次のコードでは送信バッファのサイズを1024バイトに設定しています。

int send_buffer_size = 1024;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &send_buffer_size, sizeof(send_buffer_size));

これにより、一度に送信できるデータの量を制御することが可能となります。

ただし、バッファサイズを適切に設定するためには、ネットワークの状態や送受信するデータの特性を理解することが重要です。

○タイムアウトの設定

ネットワーク通信では、接続先のサーバーからの応答が遅い、あるいは全く返ってこないという事態が想定されます。

そういった場合に、一定時間が経過したら接続を断つように設定することをタイムアウトと呼びます。

タイムアウトも「setsockopt」関数を用いて設定することができます。

下記のコードは、送信のタイムアウトを5秒に設定しています。

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout));

以上のように、C言語によるソケット通信では、各種パラメータを自由に調整することで、より複雑な通信処理を行うことが可能になります。

しかし、それぞれの設定には適切な理解と経験が必要となりますので、繰り返し学習と実践を行ってください。

まとめ

本記事では、C言語を用いたソケット通信の基本的な手順とサンプルコード、応用例、注意点と対処法、カスタマイズ方法までを詳細に解説しました。

これらを通じて、C言語でのソケット通信の理解が深まり、より効果的なネットワークプログラミングが可能になることを期待します。

ソケット通信は、様々なネットワークアプリケーションの基盤となる重要な技術です。

今回学んだ知識をベースに、さまざまなプロジェクトや問題解決に役立ててください。

これからもC言語とソケット通信の学習を続けて、より高度なネットワークプログラミングの世界を探求してください。