シンプルなTCPサーバーとクライアント
どんなプログラミング言語を学ぶときも最初にHello Worldを書くように、ソケットプログラミングでもシンプルなHello Worldプログラムを実装してみましょう。
この記事では、シンプルなTCPサーバーとクライアントを実装します。
- サーバーは特定のポートとIPアドレスで待ち受けます。
- クライアントはサーバーに接続し、データを送信します。
- サーバーはデータを受信すると、クライアントに応答を返します。
- クライアントは応答を受信して表示します。
プログラムの流れ図

image
出典: Socket Programming in C/C++ - GeeksforGeeks
サーバーの例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* サーバーのIPアドレス */
#define PORT 43866 /* ポート番号 */
#define STRING_LEN_16 16
#define STRING_LEN_64 64
#define BACKLOG 10
char *_inet_ntoa(struct in_addr *addr, char *ipAddr, int len)
{
// バイナリIPアドレスを可読なIPアドレスに変換
if (NULL == addr || NULL == ipAddr || 16 > len)
{
printf("invalid param\n");
return NULL;
}
unsigned char *tmp = (unsigned char *)addr;
snprintf(ipAddr, len, "%d.%d.%d.%d", tmp[0], tmp[1], tmp[2], tmp[3]);
return ipAddr;
}
int main(int argc, char *argv[])
{
// 1. ソケットを作成 socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCPソケット
if (sockfd == -1)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// sockaddr_in構造体を手動で構築
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 構造体を0で初期化
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(PORT); // ポート番号
inet_aton(IPADDR, &addr.sin_addr); // 可読IPアドレスをバイナリIPアドレスに変換
// inet_aton はIPv6をサポートしていないため、現在は推奨されていません
// 2. ソケットを特定のIPアドレスとポートにバインド bind()
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call bind, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. ソケットを受動的な待ち受け状態にする listen()
if (listen(sockfd, BACKLOG) == -1)
{
printf("fail to call listen, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
printf("server listen on %s:%u\n", IPADDR, PORT);
// 4. ソケットが待ち受け状態のとき、accept関数でクライアントの接続を受け付ける accept()
struct sockaddr_in peerAddr;
socklen_t peerAddrLen = sizeof(struct sockaddr_in);
int connfd = accept(sockfd, (struct sockaddr *)&peerAddr, &peerAddrLen);
if (connfd == -1)
{
printf("fail to call accept, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
char peerIPAddr[STRING_LEN_16];
_inet_ntoa(&peerAddr.sin_addr, peerIPAddr, STRING_LEN_16);
printf("peer client address [%s:%u]\n", peerIPAddr, ntohs(peerAddr.sin_port));
while (1)
{
// 5. クライアントから送信されたデータを受信 recv()
char buf[STRING_LEN_64];
int n = recv(connfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
if (0 == n) // nが0の場合は相手が切断したことを示す
{
printf("peer close\n");
break;
}
printf("recv msg from client : %s\n", buf);
sleep(2);
// 6. クライアントにデータを送信 send()
char str[] = "recved!";
printf("send msg to client : %s\n", str);
send(connfd, str, strlen(str), 0);
}
// 7. 通信終了、ソケットを閉じる close()
close(connfd);
close(sockfd);
return 0;
}
クライアントの例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* サーバーのIPアドレス */
#define PORT 43866 /* サーバーのポート番号 */
#define STRING_LEN_64 64
int main()
{
// 1. TCPソケットを作成 socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sockfd = %d\n", sockfd);
if (-1 == sockfd)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// 2. ソケットを特定のIPアドレスとポート番号に接続 connect()
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_aton(IPADDR, &addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call connect, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. ソケットを通じてサーバーにデータを送信 send()
char str[] = "hello world";
printf("send msg to server : %s\n", str);
send(sockfd, str, strlen(str), 0);
// 4. ソケットを通じてサーバーからデータを受信 recv()
char buf[STRING_LEN_64];
int n = recv(sockfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
printf("recv msg from server : %s\n", buf);
// 5. 通信終了、ソケットを閉じる close()
close(sockfd);
}
コンパイルと実行
Compiling:
gcc client.c -o client
gcc server.c -o server
出力結果
$ ./server # サーバー端末でサーバーを実行
server listen on 127.0.0.1:43866
peer client address [127.0.0.1:33844]
recv msg from client : hello world
send msg to client : recved!
peer close
$ ./client # クライアント端末でクライアントを実行
sockfd = 3
send msg to server : hello world
recv msg from server : recved!
潜在的な問題
- サーバープログラムは同時に1つのクライアントしか処理できません。
- コードがIPv6をサポートしていません。(inetntoa, inetaton)
- このプログラムにはセキュリティ対策が一切ありません。クライアントの認証や通信の暗号化が行われていないため、なりすましや盗聴などの攻撃に対して脆弱です。
解決策
- select() を使用して複数のクライアントを処理します。
- getaddrinfo(), ntop(), pton() を使用してIPv6をサポートします。
- SSL/TLS を使用して通信を暗号化します。