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>
は、ソケット通信を行う上で必要になる関数socket
やconnect
などを定義しているヘッダーファイルとなる。(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
は、信頼性があり双方向の通信を行えるようになる。
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アドレスの種類はネット通信で使うアドレスですよ」と示したものになる。
先ほどsocketの部分でPF_INET
を定義しているので二度手間な感じもするが、これは将来的にソケット通信の実装に変更があった場合に、柔軟に対応できるように面倒な処理を行なっているのだ。
``` addr.sin_addr.s_addr = inet_addr(ip); addr.sin_port = htons(port);
上記の二行の部分では、`inet_addr`と`htons`関数を使って、IPアドレスとポート番号をソケット通信に適した型に修正している。
参考:[inet\_addr](http://chokuto.ifdef.jp/advanced/function/inet_addr.html)
参考:[htons() - ネットワーク・バイト・オーダーへの符号なし短整数の変換](https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.2.0/com.ibm.zos.v2r2.bpxbd00/htons.htm)
if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
error_message(LINE);
そして、`connect`関数だが、ポインタは第2引数。先ほどから`sockaddr_in`構造体の解説をしていたが、元々は`sockaddr`構造体がソケット通信を担う構造体で、この構造体をネット通信に適した構造体が先ほどから説明している`sockaddr_in`構造体になる。
`connect`関数の第2引数は`sockaddr`構造体なので、`scokaddr_in`構造体を型キャストをする必要がある。
```c
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
関数でサーバー側から返されたメッセージを処理している。
サーバー側の実装
サーバー側の実装は、クライアント側が理解できれば早い。
#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);
}