LinuxでSocket通信をやってみる

# 1. はじめに

この記事には続編があります．

https://qiita.com/Y-Yoshimura1997/items/1295b4ccdf6f2f5e974c


# 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ハンドシェイクを行う必要があります．

図に示すと，以下のようになります．

![socket通信の状態遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3459012/fb87bc66-af3b-3de5-1da1-01cb58097a7f.png)





# 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 : 受信されたバイト数

```diff_cpp:SocketServer.h

#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);
};
```

```diff_cpp:SocketServer.cpp
#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';
};

```

```diff_cpp:Main.cpp
#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のサイズ



```diff_cpp:SocketClient.h

#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);
};
```

```diff_cpp:SocketClient.cpp

#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';
};

```
```diff_cpp:Main.cpp

#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. 次回について

このままでは，安心して通信を行うことが出来ないので，
プログラムを改良する必要があります．

というのも，
* 接続が寸断された
* サーバ側が死んだ
* クライアント側が死んだ

などのトラブルが生じると，プログラムが落ちるなどして通信が止まってしまいます．
また，今のままでは，サーバ -> クライアントの順で立ち上げないと動きません.

そこで次回では，上記の問題に対処していければと思います．

https://qiita.com/Y-Yoshimura1997/items/1295b4ccdf6f2f5e974c

# 参考記事


https://ja.manpages.org/socket/2


https://zenn.dev/hosu/articles/29812a9226aa12