1. はじめに
Webサーバなどをはじめ様々なツールがオープンソースで公開されています。
便利なツールが簡単に手に入るようになったことで、ネットワークなど低レイヤの技術に直接触れる機会がほとんどないという方も多いのではないでしょうか。
私自身、ネットワーク技術にあまり触れた経験が無いので、
アプリケーション間の具体的な通信方法などは理解できていません。
ということで、唐突ですがネットワークの基礎技術であるソケット通信について調べてみました。
また、本記事ではPythonを用いて実際にソケット通信を実装してみます。
2. ソケット通信概要
2.1 ソケットとは?
ソケットとは、通信を行う際のインターフェース、エンドポイント、接続ポイント、受け口などと説明されます。
以下は概念図ですが、例えば、IPアドレスとポート番号などアプリケーション間の通信の出入り口となるものがソケットになります。
上記の説明はあくまで概念的なものです。
では具体的にソケットとは何かというと、ソケットはネットワーク通信に利用する 「ファイルディスクリプタ」です。 ファイルの一種です。(2022/10/1修正 ご指摘いただいたので修正します。)
そして、このソケットに紐付けられた「ファイルディスクリプタ」を用いることでアプリケーションに対してデータを送ることができます。
ファイルディスクリプタとはプログラムが利用する標準入出力やファイル入出力をOSが識別するための識別子です。
この識別子は0から順に整数値が割り当てられ、0は標準入力、1は標準出力、2は標準エラー出力と決められています。従って、ファイルへの入出力やソケット通信で用いる入出力には3以上の整数が用いられます。
2.2 ソケット通信のフロー概要
ソケット通信は一般的にサーバとクライアント間で通信を用います。
ソケット通信のフローは以下のようになります。
(実際にはTCPとUDPを使う時などで若干フローは異なります。)
サーバサイドのフロー
- ① socket()はソケット(ファイルディスクリプタ)を作成します。
- ② bind()はソケットをローカルのアドレス(ソケットファイルやIP+ポート)にバインドします。
- ③ listen()はソケットに接続を待ち受けるように命令します。
- ④ accept()は外部からの接続に対して新しいソケットを作成します。
- ⑤⑥ send()/receive()はデータの送受信を行います。
- ⑦ close()ではソケットをクローズし、ファイルディスクリプタも削除します。
クライアントサイドのフロー
- ① socket()はソケット(ファイルディスクリプタ)を作成します。
- ② connect()はリモートのソケットに接続します。
- ③④ send()/receive()はデータの送受信を行います。
- ⑤ close()ではソケットをクローズし、ファイルディスクリプタも削除します。
2.3 ソケット通信のバリエーション
ソケット通信はいくつもの方式があります。
ソケット通信の方式を決めるためのキーとなるパラメータは大きく2つあり、1つ目は「アドレスファミリー」、2つ目は「ソケットタイプ」です。
それぞれについて以下で簡単に説明します。
2.3.1 アドレスファミリー
アドレスタイプは作成したソケットにバインドするアドレスの種類を表します。
以下に主要なアドレスファミリーとその概要を示します。
例えば、IPv4を使用した通信であればAF_INET、IPv6を使用した通信であればAF_INET6を利用します。
このようにアドレスファミリーでは、ソケット(ファイルディスクリプタ)にソケットファイルやIPアドレス+ポート番号を紐づけます。
| アドレスファミリー | 説明 | 指定するアドレス | 参考ページ | 
|---|---|---|---|
| AF_UNIX | ローカル通信に使用する。同一マシン上で効率的なプロセス間通信を可能とする。Unixドメインソケット通信。 | ソケットファイルのファイルパス | unix(7) | 
| AF_INET | IPv4 インターネットプロトコル。TCPソケット通信。 | ホスト名、ポート番号 | ip(7) | 
| AF_INET6 | IPv6 インターネットプロトコル。TCPソケット通信。 | ホスト名、ポート番号 | ipv6(7) | 
上記以外にも複数のアドレスファミリーがあります。
詳細は下記ページを参照ください。
socket(2) — Linux manual page
address_families(7) — Linux manual page
2.3.2 ソケットタイプ
ソケットファミリーのほかに指定する必要があるのは「ソケットタイプ」です。
以下に主要なソケットタイプとその概要を示します。
例えば、TCP通信を行うのであればSOCK_STREAM、UDP通信を行うのであればSOCK_DGRAMを使用します。
| タイプ | 説明 | 
|---|---|
| SOCK_STREAM | 順序性と信頼性がある双方向のバイトストリーム。TCPソケット通信で利用されるタイプ。 | 
| SOCK_DGRAM | データグラム(コネクションレスで信頼性が無く、最大サイズが固定)をサポート。UDPソケット通信で利用される。 | 
上記以外にも複数のソケットタイプがあります。
詳細は下記ページを参照ください。
3. Pythonによるソケット通信
3.1 サーバサイドベースクラスの作成
ソケット通信を行うためのサーバサイドのベースクラスを下記の通り実装します。
ここではserver.pyという名前でスクリプトを作成しています。
acceptメソッドを見てみると、
socket() → bind() → listen() → accept() → receive()/send() → close()
の流れで機能が呼び出されています。
これはソケット通信におけるサーバサイドのフローそのものであることが分かります。
close()メソッドは、ソケットのクローズを行なっており、インスタンスの初期化(__init__)や破棄(__del__)、コネクションが切断されたタイミングで呼び出されるようにしています。
import os
import socket
class BlockingServerBase:
    def __init__(self, timeout:int=60, buffer:int=1024):
        self.__socket = None
        self.__timeout = timeout
        self.__buffer = buffer
        self.close()
    def __del__(self):
        self.close()
    def close(self) -> None:
        try:
            self.__socket.shutdown(socket.SHUT_RDWR)
            self.__socket.close()
        except:
            pass
    def accept(self, address, family:int, typ:int, proto:int) -> None:
        self.__socket = socket.socket(family, typ, proto)
        self.__socket.settimeout(self.__timeout)
        self.__socket.bind(address)
        self.__socket.listen(1)
        print("Server started :", address)
        conn, _ = self.__socket.accept()
        while True:
            try:
                message_recv = conn.recv(self.__buffer).decode('utf-8')
                message_resp = self.respond(message_recv)
                conn.send(message_resp.encode('utf-8'))
            except ConnectionResetError:
                break
            except BrokenPipeError:
                break
        self.close()
    def respond(self, message:str) -> str:
        return ""
3.2 クライアントサイドベースクラスの作成
ソケット通信を行うためのクライアントサイドのベースクラスは下記の通り実装します。
ここではclient.pyという名前でスクリプトを作成しています。
connect()メソッド、およびsend()メソッドの両方を合わせて見てみると
socket() → connect() → receive()/send() → close()
の順で呼びされます。
これもまたソケット通信におけるクライアントサイドのフローそのものになっています。
import socket
class BaseClient:
    def __init__(self, timeout:int=10, buffer:int=1024):
        self.__socket = None
        self.__address = None
        self.__timeout = timeout
        self.__buffer = buffer
    def connect(self, address, family:int, typ:int, proto:int):
        self.__address = address
        self.__socket = socket.socket(family, typ, proto)
        self.__socket.settimeout(self.__timeout)
        self.__socket.connect(self.__address)
    def send(self, message:str="") -> None:
        flag = False
        while True:
            if message == "":
                message_send = input("> ")
            else:
                message_send=message
                flag = True
            self.__socket.send(message_send.encode('utf-8'))
            message_recv = self.__socket.recv(self.__buffer).decode('utf-8')
            self.received(message_recv)
            if flag:
                break
        try:
            self.__socket.shutdown(socket.SHUT_RDWR)
            self.__socket.close()
        except:
            pass
    def received(self, message:str):
        print(message)
3.3 TCPソケット通信
ここではソケット通信の中でもIPアドレスとポート番号を用いたTCPソケット通信を行ってみます。
3.3.1 サーバサイド
TCPソケット通信を行う際のサーバサイドの実装は以下のようにします。
サンプル実装なので、タイムアウト値と読み込みバッファサイズは固定としておきます。
(バッファサイズを超える分のデータは破棄されます。)
ソケット作成時のアドレスファミリーにはAF_INETを指定し、ソケットタイプはSOCK_STREAMとします。
アドレスにはホストとポート番号を指定します。
全てのクライアントから接続を許可するためにホストのデフォルト値は0.0.0.0とし、ポート番号は8080としておきます。
class InetServer(BlockingServerBase):
    def __init__(self, host:str="0.0.0.0", port:int=8080) -> None:
        self.server=(host,port)
        super().__init__(timeout=60, buffer=1024)
        self.accept(self.server, socket.AF_INET, socket.SOCK_STREAM, 0)
    def respond(self, message:str) -> str:
        print("received -> ", message)
        return "Server accepted !!"
if __name__=="__main__":
    InetServer()
3.3.2 クライアントサイド
TCPソケット通信を行う際のクライアントサイドの実装例は以下になります。
サーバの設定と同じく、ソケット作成時のアドレスファミリーにはAF_INETを指定し、ソケットタイプはSOCK_STREAMとします。
InetClientクラスのインスタンス生成時にconnect()メソッドを呼び出してサーバに接続、その後send()メソッドを呼び出してデータを送信できるようにしています。
class InetClient(BaseClient):
    def __init__(self, host:str="0.0.0.0", port:int=8080) -> None:
        self.server=(host,port)
        super().__init__(timeout=60, buffer=1024)
        super().connect(self.server, socket.AF_INET, socket.SOCK_STREAM, 0)
if __name__=="__main__":
    cli = InetClient()
    cli.send()
3.3.3 実行確認
それでは実際にTCPソケット通信を試してみます。
下記のgif画像左側のウィンドウでサーバ、右側のウィンドウでクライアントを実行し、クライアントからサーバへ対してメッセージを送っています。
また、メッセージを受け取ったサーバはクライアントへ対して「Server accepted !!」という文字列を返却しています。
とても簡単な例ですが、TCPソケット通信ができました。
3.4 Unixドメインソケット通信
ここでは、ソケットファイルと呼ばれる特殊なファイルを用いたソケット通信(Unixドメインソケット通信)を試してみます。
3.4.1 サーバサイド
Unixドメインソケット通信を行う際のサーバサイドの実装は以下のようにします。
TCPソケット通信の時と同様、タイムアウト値と読み込みバッファのサイズは固定としておきます。
ソケット作成時のアドレスファミリーにはAF_UNIXを指定し、ソケットタイプはSOCK_STREAMとします。
また、アドレスにはソケットファイルのファイル名(ここではserver.sockをデフォルト値に設定)を指定します。
このソケットファイルを通じて通信が行われます。
class UnixServer(BlockingServerBase):
    def __init__(self, path:str="server.sock"):
        self.server = path
        self.delete()
        super().__init__(timeout=60, buffer=1024)
        super().accept(self.server, socket.AF_UNIX, socket.SOCK_STREAM, 0)
    def __del__(self):
        self.delete()
    def delete(self):
        if os.path.exists(self.server):
            os.remove(self.server)
    def respond(self, message:str) -> str:
        print("received -> ", message)
        return "Server accepted !!"
if __name__=="__main__":
    UnixServer()
3.4.2 クライアントサイド
Unixドメインソケット通信を行う際のクライアントサイドの実装は以下のようにします。
サーバサイドと同じくアドレスファミリーにはAF_UNIXを指定し、ソケットタイプはSOCK_STREAMとします。
また、サーバと同じソケットファイルを指定します。
class UnixClient(BaseClient):
    def __init__(self, path:str="server.sock"):
        self.server=path
        super().__init__(timeout=60, buffer=1024)
        super().connect(self.server, socket.AF_UNIX, socket.SOCK_STREAM, 0)
if __name__=="__main__":
    cli = UnixClient()
    cli.send()
3.4.3 実行確認
ソケットファイルを用いたソケット通信(Unixドメインソケット通信)を試してみます。
gif画像左側のウィンドウでサーバ、右側のウィンドウでクライアントを実行し、クライアントからサーバへ対してメッセージを送っています。
また、メッセージを受け取ったサーバはクライアントへ対して「Server accepted !!」という文字列を返却しています。
とても簡単な例ですが、ソケットファイルを用いたソケット通信(Unixドメインソケット通信)も実行確認ができました。
4. TCPソケットにHTTPリクエストを投げる
TCPソケット通信では、IPアドレスとポート番号を指定したソケットを作成します。
そこで、本記事で実装したTCPソケット通信のサーバに対してcurlコマンドでPOSTリクエストを投げてみます。
下記gif画像の左側のウィンドウでは本記事で実装したTCPサーバを動かし、右側のウィンドウではcurlコマンドでPOSTリクエストを投げています。
実行結果からサーバ側ではPOSTリクエストを正常に受信できていることが確認できます。
また、この結果からHTTPの通信はTCPソケット通信を用いた単なるテキストデータの交換であることがわかります。
所感
今回はネットワークの基礎となるソケット通信に入門しました。
実際にPythonでソケット通信を行うことで、これまでよく理解していなかったネットワーク通信の具体的な方法を知ることができました。
また、実装したTCPソケットにHTTPのPOSTリクエストを送ることで、プロトコルというのはデータフォーマットを定めただけのものであることを改めて確認できました。
今回調べられたのはTCPソケット通信とUnixドメインソケット通信の2つだけであり、ソケット通信の中のほんの一部分だけになりますが、これまで何となくもやもやしていたHTTP通信の具体的な実現方法について理解が深められました。
低レイヤの技術は地味な印象になりがちですが、知識を持っているとアプリケーションなど上位レイヤの理解も深まるのでやはり重要ですね。





