C言語でTCPクライアント・サーバーを実装する

ソケットとかTCPを理解するために、C言語でTCPクライアント、サーバーを実装していく。

今回は、TCPの超基本を理解するために、「クライアントから送られたデータを、サーバーがそのまま送り返す」というechoサーバーを実装する。

説明のやり方として、まずはサンプルコード全体を掲載して、その後に重要な部分を1つ1つ解説していく、という方法を取っていく。

開発環境

  • Mac 10.14.6

今回の開発環境はMacで行なったが、同じUnix環境であるLinuxでも同様にできると思う。ただし、Windowsではできない。

TCPクライアントを実装

TCPクライアントは、以下のようなコードにした。

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

void error_message(int line);

int main() {
  int port = 5000;
  char *mes = "hello server";
  char *ip = "127.0.0.1";
  int len = strlen(mes);
  int sock;

  if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    error_message(__LINE__);

  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(ip);
  addr.sin_port = htons(port);

  if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    error_message(__LINE__);

  if (send(sock, mes, len, 0) != len) error_message(__LINE__);

  printf("RECEIVE: ");
  int total = 0;
  int num;
  char buf[50];

  while (total < len) {
    if ((num = recv(sock, buf, 49, 0)) <= 0) error_message(__LINE__);
	
    total += num;
    buf[num] = '\0';
    printf("%s", buf);
  }

  printf("\n");
  close(sock);
  exit(1);
  return 1;
}

void error_message(int line) {
  printf("ERROR: LINE %d", line);
  exit(1);
}

以下は、コードの重要な部分をそれぞれ解説していく。

includeの定義

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

2行目の<arpa/inet.h>は、バイトオーダーを扱う上で便利な関数を定義しているヘッダーファイル。

3行目の<sys/socket.h>は、ソケット通信を行う上で必要になる関数socketconnectなどを定義しているヘッダーファイルとなる。(socketやconnectに関しては後述する)

socketを定義

int sock;
if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
error_message(__LINE__);

ソケット通信を行う時には、まずはsocket関数を使って、ソケット通信を「どのような種類の通信(プロトコルファミリー )をして、どのようにデータを送るか」を定義する必要がある。

今回は第1引数にPF_INETをしているが、これはprotocol_family_internetの略で、通常のネット通信を行うのに必要なもの。つまり、TCP/IP通信を行う。第2引数のSOCK_STREAMは、信頼性があり双方向の通信を行えるようになる。

参考:Man page of SOCKET

参考:Man page of SOCKET

connectの定義

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);

if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
  error_message(__LINE__);

socket関数でソケット通信の設定をした後は、実際に通信の送信先が通信可能である状態かをチェックし、もし通信可能であれば通信できる状態を作る必要がある。

そのためには、connect関数とstruct sockaddr_in構造体を使って、送信先との通信を試みれば良い。

struct sockaddr_in構造体のメンバ変数は、以下のようになる。分かりやすく説明すると、IPアドレス、ポート番号、IPアドレスがどんな種類かを示したsin_familyなどのメンバ変数で構成されている構造体となる。

struct sockaddr_in {
  u_char sin_len;     //このメンバは古いOSでは存在しない
  u_char sin_family;  //アドレスファミリ.今回はAF_INETで固定
  u_short sin_port;   //ポート番号
  struct in_addr sin_addr;  // IPアドレス
  char sin_zero[8];  //無視してもよい.「詰め物」のようなもの
};

今回のサンプルコードではaddr.sin_family = AF_INET;と定義しているが、AF_INETはaddress_family_internetの略。簡単にいうと、「今回使うIPアドレスの種類はネット通信で使うアドレスですよ」と示したものになる。

参考:sockaddr_in構造体

先ほどsocketの部分でPF_INETを定義しているので二度手間な感じもするが、これは将来的にソケット通信の実装に変更があった場合に、柔軟に対応できるように面倒な処理を行なっているのだ。

    addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);

上記の二行の部分では、inet_addrhtons関数を使って、IPアドレスとポート番号をソケット通信に適した型に修正している。

参考:inet_addr

参考:htons() – ネットワーク・バイト・オーダーへの符号なし短整数の変換

    if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
error_message(__LINE__);

そして、connect関数だが、ポインタは第2引数。先ほどからsockaddr_in構造体の解説をしていたが、元々はsockaddr構造体がソケット通信を担う構造体で、この構造体をネット通信に適した構造体が先ほどから説明しているsockaddr_in構造体になる。

connect関数の第2引数はsockaddr構造体なので、scokaddr_in構造体を型キャストをする必要がある。

if (send(sock, mes, len, 0) != len) error_message(__LINE__);

printf("RECEIVE: ");

int total = 0;
int num;
char buf[50];

while (total < len) {
  if ((num = recv(sock, buf, 49, 0)) <= 0) error_message(__LINE__);
  total += num;
  buf[num] = '\0';
  printf("%s", buf);
}

残りの部分は、send関数でサーバーにメッセージを送って、recv関数でサーバー側から返されたメッセージを処理している。

参考:Man page of SEND

参考:Man page of RECV

サーバー側の実装

サーバー側の実装は、クライアント側が理解できれば早い。

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

void error_message(int line);
void handle_client(int sock);

int main() {
  int port = 5000;
  struct sockaddr_in client;
  struct sockaddr_in server;
  int server_sock;
  int client_sock;

  if ((server_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    error_message(__LINE__);

  server.sin_family = AF_INET;
  server.sin_addr.s_addr = htonl(INADDR_ANY);
  server.sin_port = htons(port);

  if (bind(server_sock, (struct sockaddr *)&server, sizeof(server)) < 0)
    error_message(__LINE__);

  if (listen(server_sock, 5) < 0) error_message(__LINE__);

  while (1) {
    int size = sizeof(client);
    if ((client_sock = accept(server_sock, (struct sockaddr *)&client, &size)) <
        0)
      error_message(__LINE__);

    handle_client(client_sock);
  }
  return 1;
}

void error_message(int line) {
  printf("ERROR: LINE %d", line);
  exit(1);
}

void handle_client(int sock) {
  char buf[300];
  int mes_size;
  
  if ((mes_size = recv(sock, buf, 300, 0)) < 0) error_message(__LINE__);

  while (mes_size > 0) {
    if (send(sock, buf, mes_size, 0) != mes_size) error_message(__LINE__);
    if ((mes_size = recv(sock, buf, 300, 0)) < 0) error_message(__LINE__);
  }

  close(sock);
}

ReratedPosts

C言語のsizeofにおける配列、ポインタ、構造体などの動きのまとめ
Linuxのopen(低水準入出力)の使い方やfopenとの違いをまとめていく
C言語のstrstr関数の色々な使い方
socketのgetaddrinfoの使い方や仕組みについてまとめてみる
segmentation fault 11(core dumped)の原因と2つのチェックポイント
C言語のコンパイルにおけるアセンブラ→実行ファイルまでの流れをまとめてみた