C言語でのネットワークプログラミング入門: 基礎から応用まで

C言語は、高いパフォーマンスと柔軟性を持つプログラミング言語として、システムプログラミングや組み込みシステムで広く利用されています。ネットワークプログラミングにおいても、C言語はその威力を発揮します。本記事では、C言語を使ったネットワークプログラミングの基本概念から具体的な実装方法までを段階的に解説し、実践的なスキルを習得できる内容を提供します。

目次

ネットワークプログラミングの基本概念

ネットワークプログラミングは、コンピュータ同士が通信を行うための技術です。現代の多くのアプリケーションはネットワークを介してデータを送受信し、リアルタイムで情報を交換します。この通信は、プロトコルと呼ばれる一連のルールに基づいて行われます。ネットワークプログラミングを理解するためには、まずこれらの基本概念を理解することが重要です。

通信の基本的な仕組み

ネットワーク通信は、通常、クライアントとサーバーという二つの主要なエンティティによって行われます。クライアントはリクエストを送信し、サーバーはそのリクエストに応答します。このやり取りを効率的に行うための仕組みがネットワークプロトコルです。

ネットワークプロトコルの役割

ネットワークプロトコルは、データのフォーマット、転送方法、エラー検出と修正などのルールを定めます。これにより、異なるシステム間でデータを安全かつ確実に送受信することが可能になります。主なプロトコルには、TCP/IPやUDPが含まれます。

これらの基本概念を理解することで、次に進むソケットプログラミングや具体的な実装方法への理解が深まります。

ソケットプログラミングとは

ソケットプログラミングは、ネットワーク通信を行うための基本的な技術です。ソケットは、ネットワーク通信を行うためのエンドポイントであり、クライアントとサーバーの間でデータを送受信するために使用されます。C言語では、このソケットを利用してネットワーク通信を実装します。

ソケットの基本概念

ソケットは、IPアドレスとポート番号によって一意に識別されます。IPアドレスはネットワーク上のデバイスを識別し、ポート番号は特定のアプリケーションを識別します。ソケットを使用することで、異なるデバイス間でデータをやり取りすることが可能になります。

ソケットの種類

ソケットには、主に二つの種類があります。

ストリームソケット(TCP)

ストリームソケットは、TCP(Transmission Control Protocol)を使用し、信頼性の高い通信を実現します。データは順序通りに送受信され、エラーが発生した場合は再送されます。

データグラムソケット(UDP)

データグラムソケットは、UDP(User Datagram Protocol)を使用し、信頼性よりも高速性を重視します。データは順序を保証せずに送信され、エラーが発生しても再送されません。

C言語でのソケットの使用方法

C言語でソケットプログラミングを行うためには、まずソケットを作成し、次に必要な設定を行います。その後、サーバーとクライアント間でデータを送受信します。具体的なコード例は、後述する各章で詳しく説明します。

ソケットプログラミングの基本を理解することで、ネットワーク通信を効率的に実装するための基礎が固まります。次に、具体的なサーバーとクライアントの実装方法について見ていきましょう。

サーバーとクライアントの基本構造

ネットワークプログラミングにおけるサーバーとクライアントの役割は非常に重要です。サーバーはクライアントからのリクエストを受け取り、それに応答します。このセクションでは、サーバーとクライアントの基本的な構造と役割について説明します。

サーバーの役割

サーバーは、ネットワーク上でサービスを提供するプログラムです。サーバーは特定のポートで待機し、クライアントからの接続リクエストを受け付けます。接続が確立されると、サーバーはクライアントとの間でデータを送受信します。サーバーは通常、以下の手順で動作します。

  1. ソケットの作成
  2. ソケットにアドレスとポートをバインド
  3. 接続リクエストをリッスン
  4. クライアントからの接続を受け入れ
  5. データの送受信
  6. 接続の終了

クライアントの役割

クライアントは、ネットワーク上のサービスを利用するプログラムです。クライアントはサーバーに接続し、リクエストを送信します。サーバーからの応答を受け取り、それを処理します。クライアントは通常、以下の手順で動作します。

  1. ソケットの作成
  2. サーバーへの接続リクエストを送信
  3. データの送受信
  4. 接続の終了

サーバーとクライアントの通信フロー

サーバーとクライアントの通信は、以下のようなフローで行われます。

  1. クライアントがサーバーに接続リクエストを送信
  2. サーバーが接続リクエストを受け入れ、接続を確立
  3. クライアントがサーバーにデータを送信
  4. サーバーがデータを受信し、処理を行い、応答を送信
  5. クライアントが応答を受信
  6. 必要に応じてデータの送受信を繰り返す
  7. 通信が終了したら、接続を終了

これらの基本構造を理解することで、次に進むTCP/IPプロトコルの概要と具体的な実装に役立ちます。

TCP/IPプロトコルの概要

TCP/IPは、インターネットやその他のネットワークで使用される主要な通信プロトコルです。TCP/IPプロトコルは、データの送受信を効率的かつ信頼性高く行うための一連のルールと手順を提供します。このセクションでは、TCP/IPプロトコルの基本概念とその役割について説明します。

TCP(Transmission Control Protocol)

TCPは、信頼性の高いデータ通信を提供するプロトコルです。TCPは、データを小さなセグメントに分割し、それぞれにシーケンス番号を付けて送信します。受信側はこれらのセグメントを元のデータに再構成します。TCPは、以下の特徴を持ちます。

  1. コネクション型通信: 通信開始前に接続を確立し、終了時に接続を解除する。
  2. 信頼性: データが正しく送受信されたかを確認し、不足やエラーがあれば再送する。
  3. 順序保証: データは送信された順序で受信される。

IP(Internet Protocol)

IPは、ネットワーク間でデータを転送するためのプロトコルです。IPは、データをパケットに分割し、それぞれに送信先と送信元のIPアドレスを付与します。IPは、以下の特徴を持ちます。

  1. コネクションレス通信: 接続を確立せずにデータを送信する。
  2. パケット転送: データを小さなパケットに分割し、それぞれ独立して転送する。
  3. ルーティング: データパケットを最適な経路で送信先に届ける。

TCP/IPの動作例

TCP/IPプロトコルは、以下のように動作します。

  1. アプリケーションが送信するデータをTCPがセグメントに分割。
  2. TCPがセグメントにシーケンス番号を付与し、IPに渡す。
  3. IPがセグメントをパケットに分割し、送信先IPアドレスを付与してネットワークに送信。
  4. 受信側IPがパケットを受信し、TCPに渡す。
  5. TCPがシーケンス番号を基にデータを再構成し、アプリケーションに渡す。

TCP/IPのメリット

TCP/IPプロトコルの主なメリットは以下の通りです。

  1. 信頼性: データが確実に送受信される。
  2. 柔軟性: 異なるネットワーク環境での通信が可能。
  3. 標準化: 多くのシステムで広く利用されている。

TCP/IPプロトコルの基本を理解することで、次に進む具体的なC言語でのTCPサーバーとクライアントの実装方法に役立ちます。

C言語でのTCPサーバーの実装

TCPサーバーは、クライアントからの接続リクエストを受け入れ、データを送受信する役割を担います。C言語でTCPサーバーを実装する際には、以下の手順に従います。

ソケットの作成

まず、サーバーソケットを作成します。これは通信のエンドポイントを作成するためのステップです。

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

int main() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // ソケット作成
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    // オプション設定
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("オプション設定に失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // アドレスとポートの設定
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // ソケットにアドレスとポートをバインド
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("バインドに失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 接続リクエストをリッスン
    if (listen(server_fd, 3) < 0) {
        perror("リッスンに失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("サーバーがポート8080で待機中...\n");

    return 0;
}

接続リクエストの受け入れ

サーバーは、クライアントからの接続リクエストを受け入れ、接続を確立します。

int new_socket;
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
    perror("接続受け入れに失敗");
    close(server_fd);
    exit(EXIT_FAILURE);
}
printf("接続が確立されました\n");

データの送受信

接続が確立されたら、クライアントとデータを送受信します。

char buffer[1024] = {0};
read(new_socket, buffer, 1024);
printf("クライアントからのメッセージ: %s\n", buffer);
char *response = "こんにちは、クライアント!";
send(new_socket, response, strlen(response), 0);
printf("メッセージを送信しました\n");

接続の終了

通信が終了したら、ソケットを閉じます。

close(new_socket);
close(server_fd);

これで、C言語での基本的なTCPサーバーの実装が完了です。次に、TCPクライアントの実装方法を見ていきましょう。

C言語でのTCPクライアントの実装

TCPクライアントは、サーバーに接続リクエストを送り、データを送受信する役割を担います。C言語でTCPクライアントを実装する際には、以下の手順に従います。

ソケットの作成

まず、クライアントソケットを作成します。これは通信のエンドポイントを作成するためのステップです。

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

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *message = "こんにちは、サーバー!";
    char buffer[1024] = {0};

    // ソケット作成
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("ソケット作成に失敗\n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8080);

    // サーバーアドレスの設定
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("無効なアドレス/アドレスタイプ\n");
        return -1;
    }

    // サーバーへの接続リクエスト送信
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("接続に失敗\n");
        return -1;
    }

    printf("サーバーに接続しました\n");

    // メッセージ送信
    send(sock, message, strlen(message), 0);
    printf("メッセージを送信しました: %s\n", message);

    // サーバーからの応答受信
    read(sock, buffer, 1024);
    printf("サーバーからのメッセージ: %s\n", buffer);

    // ソケットのクローズ
    close(sock);

    return 0;
}

サーバーへの接続リクエスト送信

クライアントは、サーバーに接続リクエストを送信し、接続を確立します。

if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
    printf("接続に失敗\n");
    return -1;
}
printf("サーバーに接続しました\n");

データの送受信

接続が確立されたら、サーバーにメッセージを送信し、応答を受信します。

send(sock, message, strlen(message), 0);
printf("メッセージを送信しました: %s\n", message);

read(sock, buffer, 1024);
printf("サーバーからのメッセージ: %s\n", buffer);

接続の終了

通信が終了したら、ソケットを閉じます。

close(sock);

これで、C言語での基本的なTCPクライアントの実装が完了です。次に、UDPプロトコルの概要とその使用例について説明します。

UDPプロトコルの概要と使用例

UDP(User Datagram Protocol)は、軽量で高速な通信を提供するプロトコルです。TCPと異なり、コネクションレスであるため、信頼性よりも速度が求められる用途に適しています。このセクションでは、UDPプロトコルの基本概念とその使用例について説明します。

UDPプロトコルの基本概念

UDPは、データグラムと呼ばれる独立したパケットを送信するプロトコルです。各データグラムは独立して処理され、送信された順序や到着の確認は行いません。これにより、以下の特徴を持ちます。

  1. コネクションレス通信: 接続の確立や維持が不要で、データを直接送信する。
  2. 高速性: オーバーヘッドが少なく、リアルタイム性が求められる通信に適している。
  3. 信頼性の欠如: データの順序や到着の確認が行われないため、信頼性は低い。

UDPの利点と欠点

利点

  1. 高速性: TCPよりも高速なデータ転送が可能。
  2. シンプルさ: プロトコルがシンプルで、実装が容易。
  3. リアルタイム通信: 遅延が少ないため、リアルタイム性が求められるアプリケーションに適している。

欠点

  1. 信頼性の欠如: データが失われる可能性がある。
  2. 順序の保証なし: データが送信された順序で受信される保証がない。

使用例

UDPは、以下のような用途で広く利用されています。

  1. リアルタイムアプリケーション: オンラインゲームやビデオストリーミングなど、低遅延が求められるアプリケーション。
  2. ブロードキャスト通信: ネットワーク上の全デバイスに同時にデータを送信する場合。
  3. 簡易的なメッセージング: 短時間のデータ転送や単純なリクエスト/レスポンスのやり取り。

具体的な使用例: 簡易チャットアプリ

UDPを使用した簡易チャットアプリの例を以下に示します。

サーバー側

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

int main() {
    int sockfd;
    char buffer[1024];
    struct sockaddr_in servaddr, cliaddr;

    // ソケット作成
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // サーバー情報の設定
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(8080);

    // ソケットにアドレスとポートをバインド
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("バインドに失敗");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    int len, n;
    len = sizeof(cliaddr);

    // メッセージの受信
    n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("クライアントからのメッセージ: %s\n", buffer);

    // 応答の送信
    sendto(sockfd, (const char *)"Hello from server", strlen("Hello from server"), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);

    close(sockfd);
    return 0;
}

クライアント側

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

int main() {
    int sockfd;
    char buffer[1024];
    char *message = "Hello from client";
    struct sockaddr_in servaddr;

    // ソケット作成
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    // サーバー情報の設定
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    int n, len;

    // メッセージの送信
    sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));

    // 応答の受信
    n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("サーバーからのメッセージ: %s\n", buffer);

    close(sockfd);
    return 0;
}

このように、UDPプロトコルを使用することで、簡単かつ高速なデータ通信が可能となります。次に、C言語での具体的なUDP通信の実装について詳しく説明します。

C言語でのUDP通信の実装

UDPプロトコルを使用した通信は、シンプルで高速なデータ転送が可能です。ここでは、C言語でUDPサーバーとクライアントの実装手順を具体的なコード例と共に解説します。

UDPサーバーの実装

UDPサーバーは、クライアントからのメッセージを受信し、応答を送信します。

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

int main() {
    int sockfd;
    char buffer[1024];
    struct sockaddr_in servaddr, cliaddr;

    // ソケット作成
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // サーバー情報の設定
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(8080);

    // ソケットにアドレスとポートをバインド
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("バインドに失敗");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    int len, n;
    len = sizeof(cliaddr);

    // メッセージの受信
    n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("クライアントからのメッセージ: %s\n", buffer);

    // 応答の送信
    const char *response = "Hello from server";
    sendto(sockfd, (const char *)response, strlen(response), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);

    close(sockfd);
    return 0;
}

UDPクライアントの実装

UDPクライアントは、サーバーにメッセージを送信し、応答を受信します。

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

int main() {
    int sockfd;
    char buffer[1024];
    char *message = "Hello from client";
    struct sockaddr_in servaddr;

    // ソケット作成
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    // サーバー情報の設定
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    int n, len;

    // メッセージの送信
    sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("メッセージを送信しました: %s\n", message);

    // 応答の受信
    n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("サーバーからのメッセージ: %s\n", buffer);

    close(sockfd);
    return 0;
}

実装のポイント

  1. ソケット作成: サーバーとクライアントの両方でソケットを作成します。
  2. サーバーアドレスの設定: サーバー側ではアドレスとポートを設定し、ソケットにバインドします。
  3. データ送受信: sendto関数とrecvfrom関数を使用して、データを送受信します。

これで、C言語での基本的なUDP通信の実装が完了です。UDPの特徴である高速かつシンプルな通信を活用し、さまざまなネットワークアプリケーションに応用できます。次に、エラーハンドリングとデバッグ方法について説明します。

エラーハンドリングとデバッグ方法

ネットワークプログラミングにおいて、エラーハンドリングとデバッグは非常に重要です。適切なエラーハンドリングを行うことで、プログラムの信頼性を高め、問題発生時に迅速に対処できるようになります。このセクションでは、C言語でのエラーハンドリングとデバッグ方法について説明します。

エラーハンドリングの基本

エラーハンドリングは、関数がエラーを返した場合に適切な対処を行うことです。ネットワークプログラミングでは、ソケットの作成、接続、データの送受信など、さまざまな段階でエラーが発生する可能性があります。C言語では、通常、関数の戻り値を確認し、エラーが発生した場合に適切な対処を行います。

エラーコードの確認

多くのネットワーク関数は、エラーが発生した場合に負の値を返します。例えば、socketbindlistenacceptなどの関数は、失敗すると-1を返します。この戻り値を確認し、エラーの場合にはperror関数を使用してエラーメッセージを表示します。

int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    perror("ソケット作成に失敗");
    exit(EXIT_FAILURE);
}

errnoの使用

エラーの詳細を取得するために、errno変数を使用します。errnoは、直近のエラーコードを保持しており、strerror関数を使用してエラーメッセージを取得できます。

#include <errno.h>
#include <string.h>

if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
    printf("接続に失敗: %s\n", strerror(errno));
    close(sockfd);
    exit(EXIT_FAILURE);
}

デバッグ方法

ネットワークプログラムのデバッグは難しい場合がありますが、いくつかの方法を用いることで効率的に行うことができます。

ログの使用

プログラムの実行中にログを記録することで、問題の原因を特定しやすくなります。printf関数やファイルにログを出力する方法を利用します。

FILE *log_file = fopen("debug.log", "w");
if (log_file == NULL) {
    perror("ログファイルの作成に失敗");
    exit(EXIT_FAILURE);
}

// ログ記録
fprintf(log_file, "サーバーに接続しました\n");

// ログファイルを閉じる
fclose(log_file);

デバッガの使用

gdbなどのデバッガを使用することで、プログラムの実行をステップ実行し、変数の状態や関数の呼び出しを確認できます。

gcc -g -o myprogram myprogram.c
gdb ./myprogram

パケットスニファの使用

Wiresharkなどのパケットスニファを使用することで、実際のネットワークトラフィックをキャプチャし、通信の内容を確認できます。これにより、通信プロトコルの問題やデータの不整合を検出できます。

エラー処理の例

以下は、ソケットの作成から接続、データ送信までのエラーハンドリングを含む例です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>

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

    // ソケット作成
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) {
        printf("無効なアドレス: %s\n", strerror(errno));
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // サーバーへの接続リクエスト送信
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        printf("接続に失敗: %s\n", strerror(errno));
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // メッセージ送信
    const char *message = "こんにちは、サーバー!";
    if (send(sockfd, message, strlen(message), 0) < 0) {
        printf("メッセージ送信に失敗: %s\n", strerror(errno));
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    close(sockfd);
    return 0;
}

適切なエラーハンドリングとデバッグ方法を取り入れることで、ネットワークプログラムの信頼性と保守性が向上します。次に、応用例として簡易チャットアプリの作成方法を解説します。

応用例: チャットアプリの作成

ここでは、これまで学んだネットワークプログラミングの技術を応用して、簡易チャットアプリを作成します。チャットアプリは、複数のクライアントがサーバーを介してメッセージをやり取りするためのプログラムです。今回は、TCPを使用して、信頼性の高いメッセージの送受信を実現します。

サーバーの実装

サーバーは、複数のクライアントからの接続を受け入れ、各クライアントからのメッセージを他のクライアントにブロードキャストします。

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

#define PORT 8080
#define MAX_CLIENTS 10

int clients[MAX_CLIENTS];
int client_count = 0;
pthread_mutex_t clients_mutex = PTHREAD_MUTEX_INITIALIZER;

void *handle_client(void *client_socket) {
    int sock = *(int*)client_socket;
    char buffer[1024];
    int n;

    while ((n = read(sock, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[n] = '\0';
        printf("クライアントからのメッセージ: %s\n", buffer);

        // クライアントにメッセージをブロードキャスト
        pthread_mutex_lock(&clients_mutex);
        for (int i = 0; i < client_count; i++) {
            if (clients[i] != sock) {
                write(clients[i], buffer, strlen(buffer));
            }
        }
        pthread_mutex_unlock(&clients_mutex);
    }

    // クライアントが切断した場合
    close(sock);
    pthread_mutex_lock(&clients_mutex);
    for (int i = 0; i < client_count; i++) {
        if (clients[i] == sock) {
            clients[i] = clients[--client_count];
            break;
        }
    }
    pthread_mutex_unlock(&clients_mutex);
    free(client_socket);
    return NULL;
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // ソケット作成
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("ソケット作成に失敗");
        exit(EXIT_FAILURE);
    }

    // オプション設定
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("オプション設定に失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // ソケットにアドレスとポートをバインド
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("バインドに失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 接続リクエストをリッスン
    if (listen(server_fd, 3) < 0) {
        perror("リッスンに失敗");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("サーバーがポート%dで待機中...\n", PORT);

    while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
        pthread_mutex_lock(&clients_mutex);
        if (client_count < MAX_CLIENTS) {
            clients[client_count++] = new_socket;
            pthread_t tid;
            int *client_sock = malloc(sizeof(int));
            *client_sock = new_socket;
            pthread_create(&tid, NULL, handle_client, client_sock);
        } else {
            close(new_socket);
        }
        pthread_mutex_unlock(&clients_mutex);
    }

    close(server_fd);
    return 0;
}

クライアントの実装

クライアントは、サーバーに接続し、ユーザーが入力したメッセージを送信します。また、他のクライアントからのメッセージを受信して表示します。

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

#define PORT 8080

void *receive_messages(void *socket) {
    int sock = *(int*)socket;
    char buffer[1024];
    int n;

    while ((n = read(sock, buffer, sizeof(buffer) - 1)) > 0) {
        buffer[n] = '\0';
        printf("他のクライアントからのメッセージ: %s\n", buffer);
    }

    return NULL;
}

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[1024];
    pthread_t tid;

    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("ソケット作成に失敗\n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("無効なアドレス\n");
        return -1;
    }

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("接続に失敗\n");
        return -1;
    }

    printf("サーバーに接続しました\n");

    pthread_create(&tid, NULL, receive_messages, &sock);

    while (1) {
        fgets(buffer, sizeof(buffer), stdin);
        send(sock, buffer, strlen(buffer), 0);
    }

    close(sock);
    return 0;
}

動作確認

  1. サーバープログラムを実行して、サーバーを起動します。
  2. 複数のクライアントプログラムを実行し、それぞれがサーバーに接続します。
  3. クライアント間でメッセージを送信し、他のクライアントにメッセージが正しく表示されることを確認します。

これで、簡易チャットアプリの実装が完了です。ネットワークプログラミングの応用として、実践的なスキルを磨くことができます。最後に、まとめとしてネットワークプログラミングの学習の次のステップについて提案します。

まとめ

C言語でのネットワークプログラミングは、基礎をしっかりと理解し、実践することで、さまざまなアプリケーションに応用できる強力なスキルです。本記事では、ネットワークプログラミングの基本概念、ソケットプログラミング、TCP/IPプロトコル、そして具体的なサーバーとクライアントの実装方法について学びました。

また、エラーハンドリングやデバッグ方法、さらに実際にチャットアプリを作成することで、ネットワークプログラミングの実践的な技術を習得しました。

次のステップとして、以下のような高度なトピックに進むことをお勧めします。

  1. セキュアな通信の実装: SSL/TLSを使用した安全なデータ転送。
  2. 非同期通信: 非ブロッキングI/Oやマルチスレッドを使用した効率的な通信。
  3. プロトコルの実装: HTTPやFTPなどの高レベルプロトコルの実装。
  4. ネットワークプログラミングの最適化: パフォーマンスチューニングや負荷分散の手法。

これらのトピックを学ぶことで、より高度で複雑なネットワークアプリケーションの開発が可能になります。ネットワークプログラミングのスキルを磨き続け、実際のプロジェクトに応用していきましょう。

コメント

コメントする

目次