ソケット通信とは?【C言語】
ソケット通信とはソケットを用いた通信のこと。
「通信」とは非常に身近なものであり、今読んでいただいているこのページも通信によって閲覧している。
さまざまな物が通信でつながることで、便利な機能がたくさん実現されており、これらの通信には「ソケット通信」で行われているケースが多い。
ソケットを使用することで、異なるマシン間(もしくは同一のマシン上)の異なるプロセス間で通信を可能にする。
そもそもソケットとは?
ソケットっていうのは通信端点のこと。
いいかえると、アプリとアプリ(プログラムとプログラム)の通信の出入り口。
ソケット通信を行うには、まずはアプリやプログラム内で「ソケット」を作成。
そして、それを他のアプリやプログラムのソケットに接続してデータのやり取りを行う。
なので、まずはこのソケットを作成する必要がある。
C言語ではこのソケットを、socket 関数により作成することが可能だ。
ソケットを利用し、サーバーとクライアント間でデータのやり取りを行うことができる。
つぎに、この「サーバーとクライアント」について見ていく。
サーバーとクライアン
サーバーとは、クライアントにサービスを提供するソフトウェアやコンピュータ。
そしてクライアントとは、サーバーからサービスを受けるソフトウェアやコンピュータ。
例えばウェブブラウザを使って、ウェブページの閲覧する場合、以下のようになる。
・クライアント:ウェブページを閲覧しようとしている PC やスマホ
・サーバー:ウェブページのデータを保存しているウェブサーバー
一般的に、サーバーとクライアントは別々のPCに分かれている。
しかし、自作のソケット通信を自分のPCで試す場合は、自分のPCにサーバーとクライアント両方の役割を持たせて動作させることができる。
それでは、さっそくソケット通信に入っていく。
ソケット通信のおおまかな流れ
①サーバー・クライアント間で接続を確立
ソケット作成 (socket)
まずは通信を行うためのソケット作成が必要。
注意が必要なのは、サーバーとクライアント両方にソケットが必要なので、サーバークライアント両方でソケットを作成する必要があること。
ソケットの作成は socket関数を使用する。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain:プロトコルファミリー(アドレスファミリー)を指定
//type:ソケットのタイプを指定
//protocol:使用するプロトコルを指定
//example
sock = socket(AF_INET, SOCK_STREAM, 0);
サーバー側でソケットの関連付け (bind)
ソケット作成後に行うのが bindとなり、ソケットに IP アドレスやポート番号の設定をおこなう。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:ソケット
//addr:ソケットに割り当てるアドレスやポート番号の情報
//addrlen:addr のサイズ(バイト数)
サーバー側で接続を待つ (listen)
クライアントから接続要求がくるまで、サーバー側で「接続待ち」をおこない、待機させる。
この接続待ちには listen 関数を使用する。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//sockfd:接続を待つソケット
//backlog:接続要求を保持する数 -> accept していない接続要求を、最大いくつまで溜めておくかを指定する。これにより、サーバーは接続要求が来たタイミングではなく、サーバー自身のタイミングが良いときに接続を受けつけ、実際のデータのやり取りを行うようなことが可能。
クライアント側で接続を要求する (connect)
つぎに、クライアントがサーバーに対して「接続要求」を行う。
この接続要求には connect関数を使用する。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:接続を行うソケット
//addr:接続先(アドレスやポート番号など)の情報
//addrlen:addr のサイズ(バイト数)
サーバーが接続を受け付ける (accept)
最後に、サーバーは受け取った接続要求を受けつける。
ここで、サーバーとクライアントの間で接続が確立される。
この接続の受けつけは accept 関数を使用。
accept 関数は、接続要求がくるまでは関数が終了しない。
つまり、クライアントから connectが実行されなければ、関数の中でプログラムが待たされることになる。
また、accept 関数はソケットを作成し、戻り値がその作成したソケットの識別子となる。
作成されたソケットは、接続要求を受け付けたクライアントと「接続済のソケット」である。
一方、accept 関数の第1引数 sockfdで指定したソケットは、いまだ listen で接続待ちの状態になっているソケット。
結果、接続済のクライアントとデータのやり取りを行うには、accept 関数の戻り値(接続済のソケット)を用いて、データのやり取りを行うことになる。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//sockfd:接続待ちを行なっているソケット
//addr:接続先の情報へのポインタ
//addrlen:addr のサイズ(バイト数)へのポインタ
②サーバー・クライアント間でデータのやり取り
ソケット同士の接続が確立後、つぎはソケットを用いて通信の目的となるデータのやり取りを行う。
データの送信(send)
send 関数は、接続先に対してデータを送信する関数です。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//sockfd:接続済のソケット
//buf:送信するデータへのポインタ
//len:送信するデータのサイズ(バイト数)
//flags:送信時の動作の詳細設定
send 関数の戻り値は、実際に接続先に送信したデータのバイト数となる。
データの受信(recv)
recv 関数は、接続先が送信したデータを受信する関数。
recv 関数はデータが到着するまで待ち続ける。
#include <sys/socket.h>
ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
//sockfd:接続済のソケット
//buf:受信データを格納するバッファのアドレス
//len:buf のサイズ(バイト数)
//flags:受信時の動作の詳細設定
recv 関数の戻り値は、実際に接続先から受信したデータのバイト数。
ポイント
サーバーとクライアントでタイミングを合わせることが必要。
サーバーとクライアントのどちらが、どのタイミングで、どんなデータを送信(または受信)するかを定める必要がある。
このようなデータのやり取りの仕方は「プロトコル」によって定められている。
たとえば、ウェブページを要求するさいには HTTP というプロトコルに従ってデータのやり取りを行う必要がある。
HTTP の P は、プロトコルの頭文字でもある。
③サーバー・クライアント間の接続を閉じる(close)
データのやり取りが終了したら、最後に接続を閉じる。
これによりサーバーとクライアント間の接続が解消される。
接続を閉じる処理は close関数を使用。
#include <unistd.h>
int close(int fd);
//fd: ソケットの識別子
サンプルコード
server.c
//server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 1024
int communicate(int c_sock)
{
int recv_size;
int send_size;
char recv_buf[BUF_SIZE];
char send_buf;
while (1)
{
// クライアントから文字列を受信
recv_size = recv(c_sock, recv_buf, BUF_SIZE, 0);
if (recv_size == -1)
{
printf("Error: recv()\n");
break;
}
if (recv_size == 0)
{
// 受信サイズが0の場合は相手が接続閉じていると判断
printf("Connection ended\n");
break;
}
// 受信した文字列を表示
printf("%s", recv_buf);
// 文字列が"exit"ならクライアントとの接続終了
if (strcmp(recv_buf, "exit\n") == 0)
{
// 接続終了を表す0を送信
send_buf = 0;
send_size = send(c_sock, &send_buf, 1, 0);
if (send_size == -1)
{
printf("Error: send()\n");
break;
}
printf("A client exited\n");
break;
}
else
{
// "finish"以外の場合はクライアントとの接続を継続
send_buf = 1;
send_size = send(c_sock, &send_buf, 1, 0);
if (send_size == -1)
{
printf("Error: send()\n");
break;
}
}
}
return (0);
}
int main(void)
{
int w_addr;
int c_sock;
struct sockaddr_in a_addr;
// ソケットを作成
w_addr = socket(AF_INET, SOCK_STREAM, 0);
if (w_addr == -1) {
printf("Error: socket()\n");
return (-1);
}
// 構造体を全て0にセット
bzero(&a_addr, 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)
// if (bind(w_addr, (const struct sockaddr *)&a_addr, sizeof(a_addr)) == -1)
{
printf("Error: bind()\n");
close(w_addr);
return (-1);
}
// ソケットを接続待ちに設定
if (listen(w_addr, 3) == -1)
{
printf("Error: listen()\n");
close(w_addr);
return (-1);
}
while (1)
{
// 接続要求の受け付け(接続要求くるまで待ち)
printf("Waiting for a client's connection...\n");
c_sock = accept(w_addr, NULL, NULL);
if (c_sock == -1)
{
printf("Error: accept()\n");
close(w_addr);
return (-1);
}
printf("Connected!!\n");
// 接続済のソケットでデータのやり取り
communicate(c_sock);
// ソケット通信をクローズ
close(c_sock);
// 次の接続要求の受け付けに移る
}
// 接続待ちソケットをクローズ
close(w_addr);
return (0);
}
client.c
//client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 1024
int communication(int sock)
{
int send_size;
int recv_size;
char send_buf[BUF_SIZE];
char recv_buf;
while (1)
{
// サーバーに送る文字列を取得
fgets(send_buf, BUF_SIZE, stdin);
// 文字列を送信
send_size = send(sock, send_buf, strlen(send_buf) + 1, 0);
if (send_size == -1)
{
printf("Error: send()\n");
break;
}
// サーバーからの応答を受信
recv_size = recv(sock, &recv_buf, 1, 0);
if (recv_size == -1)
{
printf("Error: recv()\n");
break;
}
if (recv_size == 0)
{
// 受信サイズが0の場合は相手が接続閉じていると判断
printf("Server is closed\n");
break;
}
// 応答が0の場合はデータ送信終了
if (recv_buf == 0)
{
printf("Exited from the connection to a server\n");
break;
}
}
return 0;
}
int main(void)
{
int sock;
struct sockaddr_in addr;
// ソケットを作成
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
printf("Error: socket()\n");
return -1;
}
// 構造体を全て0にセット
bzero(&addr, 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("Error: connect()\n");
close(sock);
return (-1);
}
printf("Connection is establised!\n");
printf("Ready to send messages\n");
// 接続済のソケットでデータのやり取り
communication(sock);
// ソケット通信をクローズ
close(sock);
return (0);
}
コンパイル
gcc -Wall -Werror -Wextra server.c -o server.exe
gcc -Wall -Werror -Wextra client.c -o client.exe
動作環境
Windows11 (version 22H2)
WSL2 (5.10.102.1-microsoft-standard-WSL2)
Ubuntu (20.04.5 LTS (Focal Fossa))
Discussion