1. はじめに
この記事には続編があります.
1.1. 動機
最近,業務でLinuxを扱う機会があり,個人でも触り始めました.
業務では,組み込みLinuxを主に使用しており,
WindowsPCとのデータ・コマンドのやり取りのために,
Socket通信が用いられています.
Socket通信は汎用性が高そうですし,なにより自分で実装できるよう
なりたいと思い,今回はその過程を記事として投稿することとしました.
ですので,誤った情報を記載している可能性もございます.
温かく見守りつつ,ご指摘いただけますと幸いです.
1.2. 目標
今回は,Socket通信を用いてTCP/IPにより,
文字列をお互いに送受信できることを目標とします.
この目標が達成できれば,プロセスやマシン間で
自由に情報のやり取りが出来るようになり,夢が広がるはずです.
なお,ネイティブでLinuxが動くPCを持っていないので,
以下の環境で動作検証を用います.
- wsl2 (Windows 11)
- Ubunts 20.04
wsl2を複数起動する技術とマシンスペックの用意が無いので,
localhost(127.0.0.1)に,サーバとクライアント両方のポートを用意し,
お互いに通信を行うこととします.
(サーバのポート番号は2001.クライアントは指定なし.)
2. ソケット通信について
Linuxのシステムコールsocket()で作成されるエンドポイントを用いた通信を
ソケット通信と呼んでいるようです.
サポートされているプロトコルは幾つかあり,
自身が知っているところでは,
- トランスポート層 : TCP,UDP
- ネットワーク層 : IPv4, IPv6
などを選べるようです.
今回は,通信速度は多少犠牲になっても,確実な情報のやり取りを行うことができるように,
TCP/IP通信の実現を目標とします.
2.1. ソケット通信(TCP/IP)の流れ
ソケット通信(TCP/IP)では,サーバとクライアントの二つの役回りがあります.
サーバ側がエンドポイントの作成を行い,そこへクライアントが接続要求を行います.
そして,その要求をサーバが受け入れることで,ようやく通信が確立され,
データの送受信が可能となります.いわゆる3wayハンドシェイクを行う必要があります.
図に示すと,以下のようになります.
3. C++で実装してみた
サーバ側とクライアント側のプログラムを
それぞれC++でクラス化して実装してみました.
どちら側も,ソケットの作成し通信確立したのちに,
送信.受信処理をそれぞれタスク化して動かしています.
- 受信処理タスク
無限ループ内で,受信されるメッセージを待ち受け. - 送信処理タスク
メッセージを1秒おきに送る.
通信確立は,サーバ側・クライアント側で違いはあれど,
それ以外の処理は,共通するところが多いです.
詳細は下記にコードとともに示します.
<サーバ側>
int bind(int __fd, const sockaddr *__addr, socklen_t __len)
- __fd : アドレスを紐付けるソケットのfd
socket()で作成したソケットのfdを指定. - __addr : ソケットに紐づけるアドレス
構造体sockaddr_inにipアドレスとポート番号を指定. - __len : __addrのサイズ
構造体sockaddrについて
アドレス情報を格納する構造体として,
関数の引数の指定は,構造体sockaddrとなっていますが,
これは汎用型となっており,今回のようにIPv4でアドレスを指定する際は,
構造体sockaddr_inを使用します.
int listen(int __fd, int __n)
- __fd : 接続待ちするソケットのfd
socket()で作成し,アドレスをbind()したソケットのfdを指定. - __n : 接続要求のキューの最大サイズ
最大サイズを指定するためにSOMAXCONNを指定. - return : 成功すれば0, 失敗すれば-1を返す
int accept(int __fd, sockaddr *restrict __addr, socklen_t *restrict __addr_len)
- __fd : 接続を受け入れるfd
listen()していたソケットのfdを指定. - __addr : 接続先のアドレスが格納される.
- __addr_len : __addrのサイズ
- return : 接続を受け入れたfd
ここで注意なのが,
ssize_t send(int __fd, const void *__buf, size_t __n, int __flags)
同様にデータを送るための関数sendto(), sendmsg(), write()など
がありますが,オプションの有無や引数の指定方法が異なります.
- __fd : 送受信に用いるソケットのfd
- __buf : 送信するデータ
- __n : 送信するデータのサイズ
- __flag : フラグ引数
(詳しい内容は理解できておりません) - return : 送信されたバイト数
ssize_t recv(int __fd, const void *__buf, size_t __n, int __flags)
同様にデータを受信するための関数recvfrom(), recvmsg(), read()など
がありますが,オプションの有無や引数の指定方法が異なります.
- __fd : 送受信に用いるソケットのfd
- __buf : 受信するデータ
- __n : 受信するデータのサイズ
- __flag : フラグ引数
(詳しい内容は理解できておりません) - return : 受信されたバイト数
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class SocketServer
{
private:
int socket_;
struct sockaddr_in addr_server_;
struct sockaddr_in addr_client_;
public:
SocketServer(int port_server);
void Connect();
void SendText(const char* text);
void RecvText(char* text);
};
#include "SocketServer.h"
SocketServer::SocketServer(int port_server)
{
addr_server_.sin_addr.s_addr = INADDR_ANY;
addr_server_.sin_port = htons(port_server);
addr_server_.sin_family = AF_INET;
};
void SocketServer::Connect()
{
socket_ = socket(AF_INET, SOCK_STREAM, 0);
bind(socket_, (sockaddr*)&addr_server_, sizeof(addr_server_));
listen(socket_, SOMAXCONN);
socklen_t addr_len = sizeof(addr_client_);
socket_ = accept(socket_, (sockaddr *)&addr_client_, &addr_len);
};
void SocketServer::SendText(const char* text)
{
send(socket_, text, std::strlen(text), 0);
};
void SocketServer::RecvText(char* text)
{
int recv_size = recv(socket_, text, 1024, 0);
text[recv_size] = '\0';
};
#include <iostream>
#include <thread>
#include <chrono>
#include "SocketServer.h"
int c = 0;
void SendTask(SocketServer* sock)
{
for(;;){
std::string text = "server->client" + std::to_string(c);
c++;
sock->SendText(text.c_str());
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
void RecvTask(SocketServer* sock)
{
for(;;){
char text[1024];
sock->RecvText(text);
std::cout << "Recive : " << text << std::endl;
}
}
int main()
{
std::cout << "Communicate with TCP/IP!" << std::endl;
int socket_port = 2001;
SocketServer socket_server(socket_port);
socket_server.Connect();
std::thread thread_send(SendTask, &socket_server);
std::thread thread_recv(RecvTask, &socket_server);
thread_send.join();
thread_recv.join();
}
<クライアント側>
int connect(int __fd, const sockaddr *__addr, socklen_t __len)
- __fd : 接続するソケットのfd
socket()で作成したソケットのfdを指定. - __addr : 接続先のアドレス
構造体sockaddr_inにipアドレスとポート番号を指定. - __len : __addrのサイズ
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class SocketClient
{
private:
int socket_;
int sockaddr_len_ = sizeof(struct sockaddr_in);
struct sockaddr_in addr_server_;
public:
SocketClient(const char* ip_server, int port_server);
void Connect();
void SendText(const char* text);
void RecvText(char* text);
};
#include "SocketClient.h"
SocketClient::SocketClient(const char* ip_server, int port_server)
{
addr_server_.sin_addr.s_addr = inet_addr(ip_server);
addr_server_.sin_port = htons(port_server);
addr_server_.sin_family = AF_INET;
}
void SocketClient::Connect()
{
socket_= socket(AF_INET, SOCK_STREAM, 0);
connect(socket_, (sockaddr *)&addr_server_, sockaddr_len_);
};
void SocketClient::SendText(const char* text)
{
send(socket_, text, std::strlen(text), 0);
};
void SocketClient::RecvText(char* text)
{
ssize_t recv_size = recv(socket_, text, 1024, 0);
text[recv_size] = '\0';
};
#include <iostream>
#include <thread>
#include <chrono>
#include "SocketClient.h"
int count = 0;
void SendTask(SocketClient* sock)
{
for(;;){
std::string text = "cilent->server" + std::to_string(count);
count++;
sock->SendText(text.c_str());
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
void RecvTask(SocketClient* sock)
{
for(;;){
char text[1024];
sock->RecvText(text);
std::cout << "Recive : " << text << std::endl;
}
}
int main()
{
std::cout << "Communicate with TCP'/IP!" << std::endl;
int port_server = 2001;
const char* ip_server = "127.0.0.1";
SocketClient socket_client(ip_server, port_server);
socket_client.Connect();
std::thread thread_send(SendTask, &socket_client);
std::thread thread_recv(RecvTask, &socket_client);
thread_send.join();
thread_recv.join();
}
4. 次回について
このままでは,安心して通信を行うことが出来ないので,
プログラムを改良する必要があります.
というのも,
- 接続が寸断された
- サーバ側が死んだ
- クライアント側が死んだ
などのトラブルが生じると,プログラムが落ちるなどして通信が止まってしまいます.
また,今のままでは,サーバ -> クライアントの順で立ち上げないと動きません.
そこで次回では,上記の問題に対処していければと思います.
参考記事