LoginSignup
3
7

LinuxでSocket通信をやってみる

Last updated at Posted at 2023-11-19

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

図に示すと,以下のようになります.

socket通信の状態遷移.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 : 受信されたバイト数
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);
};
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';
};

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のサイズ
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);
};
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';
};

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

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

というのも,

  • 接続が寸断された
  • サーバ側が死んだ
  • クライアント側が死んだ

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

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

参考記事

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7