Closed47

TCP/IPをハンズオンで学ぶ[ソケットプログラミング]

ハガユウキハガユウキ

ネットワークプログラミング

  • ネットワークを扱うプログラムを記述することを、一般的にネットワークプログラミングと呼ぶ。
  • ネットワークプログラミングをするには、いくつかのAPIが知られているが、Unix系OSのデファクトスタンダードになっているソケットというAPIを使う。
  • ソケットを使ったネットワークプログラミングのことを、とくにソケットプログラミングと呼ぶ。
  • ソケットは、ネットワークを抽象化したインターフェイスである。ホスト間の通信や、ホスト内でのIPC(InterProcessCommunication)に用いられる。
  • ソケットプログラミングには、何らかのプログラミング言語が必要。今回はpythonを使う。
ハガユウキハガユウキ
  • ソケットは、コマンドラインツールだけではなく、Webアプリケーションを動かすのにも使われる。しかし、何らかのWebフレームワークによってソケットのような抽象度の低いAPIは隠蔽される。
  • ソケットは、Webアプリケーションフレームワークが動作するアプリケーションサーバーで登場する。クライアントとネットワーク越しにバイト列をやりとりする、相対的に低いレイヤーな部分でソケットは使われる。
ハガユウキハガユウキ
  • ソケットの使い方はサーバーとクライアントで分かれている。つまり、クライアントからの接続を待ち受けるか、サーバーへ接続しに行くかである。
ハガユウキハガユウキ

ソケットによる通信

ソケットによる通信は、ざっくり次の手順で行う。

  1. サーバ側でソケットを生成し、クライアントからの接続を待ち受ける
  2. クライアント側でソケットを生成し、サーバのホストとポートを指定して接続する

これにより、サーバとクライアントの間に、任意のデータを双方向に送ることができる伝送路が作られる。

ハガユウキハガユウキ

HTTPクライアント

pythonに組み込まれているhttpサーバーを実行する
(httpサーバーってことは通信にhttpのプロトコルを使う。httpのフォーマットでデータをやり取りする。httpはtcpの上で動く)

vagrant@ubuntu-focal:/var/tmp/http-home$ sudo python3 -m http.server -b 127.0.0.1 80
Serving HTTP on 127.0.0.1 port 80 (http://127.0.0.1:80/) ...
ハガユウキハガユウキ

pythonを用いてHTTPクライアント作れた。ちゃんと動くことも確認できた。
yieldの部分がよく分からんなあ。
goで再実装してみたい。

#!/usr/bin/env python3
# if sheban exitsts, run script with python3

# http client script with socket

# the socket module is imported
# the socket module in Python provides the API for working with sockets
import socket

# function to write the specified byte sequence to the socket.
# Actually, it is not guaranteed that you can write the entire byte sequence to the socket in one go.
# Therefore, in this send_msg() function, it iteratively repeats the process until all the byte sequence is written.
# Inside the function, it repeatedly calls the send() method on the instance of the socket to write the byte sequence.
def send_msg(sock, msg):
    total_sent_byte_length = 0

    # get the length of a string
    total_msg_byte_length = len(msg)

    while total_sent_byte_length < total_msg_byte_length:
        # write a byte sequence to a socket and obtain the nubmer of bytes written.
        # an array that includes elements from index total_sent_byte_lenght to the end
        sent_byte_length = sock.send(msg[total_sent_byte_length:])

        if sent_byte_length == 0:
            raise RuntimeError("socket connection broken")

        total_sent_byte_length += sent_byte_length

# generator function that reads byte sequence from a socket until the connection is closed.
# To read the response that should have been sent from the HTTP server, you can read it from the socket.
def recv_msg(sock, chunk_len=1024):
    while True:
        # read a specified number of bytes from a socket
        received_chunk = sock.recv(chunk_len)

        # When you are unable to read anything at all, it means that the connection has been closed.
        if len(received_chunk) == 0:
            break


        # return the received byte sequence
        yield received_chunk

def main():
    # prepare a socket for communication using IPv4/TCP.
    # An instance of a socket is created using the socket() function from the imported socket module.
    # socket.AF_INET specifies the use of IPv4 as the network layer protocol.
    # socket.SOCK_STREAM specifies the use of TCP as the transport layer protocol.
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # The created instance of the socket is used to connect to the server
    # connect to the TCP/80 port of the loopback address.
    # to connect to the server, you call the connect() method on the instance of the socket.
    # this method takes the IP address and port number of the server as parameters.
    client_socket.connect(("127.0.0.1", 80))

    request_text = "GET / HTTP/1.0\r\n\r\n"

    # to write data to a socket, you need to provide a byte array instead of a string.
    # encode a string into a byte sequence using ASCII encoding.
    request_bytes = request_text.encode("ASCII")

    # write the byte sequence of a request to a socket.
    send_msg(client_socket, request_bytes)

    # read the byte sequence of a response from a socket.
    received_bytes = b''.join(recv_msg(client_socket))

    # decode a byte sequence into a string.
    received_text = received_bytes.decode("ASCII")

    print(received_text)

    # close a socket that is no longer needed.
    client_socket.close()

if __name__ == "__main__":
    # execute the main function as the entry point of a script.
    main()
ハガユウキハガユウキ

送受信したい文字列に日本語などのマルチバイト文字を含むときは文字コードにASCIIが使えません。代わりに、UTF8などのマルチバイト文字に対応した文字コードを使う必要があります。また、文字コードはサーバとクライアントで同じものを使う必要があります。もし、異なる文字コードとしてバイト列を解釈すると、いわゆる文字化けが発生するためです。

ハガユウキハガユウキ

エコーサーバー

ソケットをサーバーで使ってみる。
エコーサーバーとは、送られてきたメッセージを送ってきた相手にそのまま返すプログラムである。

ハガユウキハガユウキ
#!/usr/bin/env python3

# script that implements an echo server using sockets

import socket

def send_msg(sock, msg):
    total_sent_byte_length = 0

    total_msg_byte_length = len(msg)

    while total_sent_byte_length < total_msg_byte_length:
        sent_byte_length = sock.send(msg[total_sent_byte_length:])
        if sent_byte_length == 0:
            raise RuntimeError("socket connection broken")

        total_sent_byte_length += sent_byte_length


def recv_msg(sock, chunk_length = 1024):
    while True:
        # 実際、1回で全てのバイト列をソケットに書き込める保証はない。
        # そのため、このsend_msg()関数では、全てのバイト列が書き込まれるまで繰り返し処理を行います。
        # 指定したバイト数(chunk_length)だけデータを受信し、そのデータをバイナリ形式で返します。
        # つまり、一度にすべてのデータを受信するわけではありません。
        received_chunk = sock.recv(chunk_length)

        if len(received_chunk) == 0:
            break

        # 受信したデータをジェネレータが返す値として登録する
        yield received_chunk

def main():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # the solution to avoid "Address already in use" error.
    # このメソッドは、ソケットの挙動を変更するオプションを指定するためにあります。
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

    # specify the IP address and port number to listen for connections from clients.
    # これはクライアントからの接続を待ち受けるIPアドレスとポートを指定するメソッドです。
    # ホストのIPアドレスは、ひとつとは限りません。ネットワークインターフェイスが複数あれば、
    # IPアドレスも複数あるかもしれないからです。あるいは、ひとつのネットワークインターフェイスに、
    # 複数のIPアドレスが付与されることもあります。そのため、どのIPアドレスを使ってクライアントからの接続を待ち受けるか、
    # bind()メソッドを使って指定します。もし、すべてのIPアドレスで接続を待ち受けるときは
    # IPアドレスとして''(空文字)を指定します。
    server_socket.bind(("127.0.0.1", 54321))

    # start listening for connections.
    # このメソッドを呼ぶと、実際にクライアントからの接続の待ち受けを開始します。
    server_socket.listen()

    print("starting server.....")

    # handle the connection.
    # ソケットのインスタンスに対してaccept()メソッドを呼び出しています。
    # このメソッドは、実際にクライアントから接続があったときに、それを処理するためのものです。
    # accept()メソッドからは、クライアントを表したソケットのインスタンスと、
    # 接続してきたクライアントの情報が得られます。接続してきたクライアントの情報というのは、
    # 具体的には送信元のIPアドレスとポート番号です。IPアドレスはループバックアドレスになっているはずですが、
    # ポート番号はエフェメラルポートなので毎回異なります。
    client_socket, (client_address, client_port) = server_socket.accept()

    # display information about connected client.
    print(f"accepted from {client_address}:{client_port}")

    # Read byte sequence from the socket.
    for received_msg in recv_msg(client_socket):
        # write the read content back to the socket as it is(echo back).
        send_msg(client_socket, received_msg)

        print(f"echo: {received_msg}")

    # close the socket after it is no longer needed.
    client_socket.close()
    server_socket.close()

if __name__ == "__main__":
    main()
ハガユウキハガユウキ

チャンク

ネットワーキングやデータ転送の文脈での「チャンク(chunk)」とは、送信や処理中のデータの一部または断片を指します。特に、大量のデータを転送する際には、HTTPなどのプロトコルでよく使用されます。

HTTPでは、「チャンク(chunk)」という転送エンコーディングを使用することで、サーバーはレスポンスを一度に送信するのではなく、複数の小さなチャンクに分割して送信します。各チャンクはサイズ指示子で先行し、クライアントは各チャンクの長さを知ることができ、完全なレスポンスを正しく再構築することができます。

https://e-words.jp/w/チャンク.html

ハガユウキハガユウキ

サーバのプログラムが想定通りに動作しているか調べるには、ソケットの状態を確認することが重要

ssというコマンドを使うとソケットが待ち受けているIPアドレスやポート番号などが確認できます。試しに、TCPで待ち受け状態にあるソケットと、それを利用しているプロセスの一覧を表示してみる。

vagrant@ubuntu-focal:~$ sudo ss -tnlp
State    Recv-Q    Send-Q       Local Address:Port        Peer Address:Port   Process
LISTEN   0         128              127.0.0.1:54321            0.0.0.0:*       users:(("python3",pid=44072,fd=3))
LISTEN   0         4096         127.0.0.53%lo:53               0.0.0.0:*       users:(("systemd-resolve",pid=570,fd=13))
LISTEN   0         128                0.0.0.0:22               0.0.0.0:*       users:(("sshd",pid=663,fd=3))
LISTEN   0         128                   [::]:22                  [::]:*       users:(("sshd",pid=663,fd=4))
vagrant@ubuntu-focal:~$

コマンドの実行結果から、python3のプロセスが、ループバックIPアドレスのTCP/54321番ポートで接続を待ち受けていることが確認できる。

ハガユウキハガユウキ

ssコマンドはLinuxシステムで、ネットワークソケットの情報を表示するために使用される。

# ss: すべてのソケットの情報を表示する。
# -t: TCPソケットのみ表示する。
# -n: サービス名の名前解決を行わない(ポート番号を表示)
# -l: リスニングソケットのみ表示する。
# -p: ソケットを使用しているプロセスも表示する
ss -tnlp

https://atmarkit.itmedia.co.jp/ait/articles/1710/06/news014.html

ハガユウキハガユウキ

TCP/IP では、プロセスとプロセスが、電話で会話をするように通信が行われるそう。
たしかに、ncコマンドで実行したときも、クライアント側で実行したncコマンドのプロセスがpythonのサーバープロセスにアクセスしてたような気がするな。実際にssコマンドで確認したらそうだった。

vagrant@ubuntu-focal:~$ ss -tnp
State    Recv-Q    Send-Q         Local Address:Port          Peer Address:Port     Process
ESTAB    0         0                  127.0.0.1:40592            127.0.0.1:54321     users:(("nc",pid=55625,fd=3))
ESTAB    0         0                  10.0.2.15:22                10.0.2.2:49884
ESTAB    0         0                  10.0.2.15:22                10.0.2.2:57026
ESTAB    0         0                  127.0.0.1:54321            127.0.0.1:40592     users:(("python3",pid=55615,fd=4))
ESTAB    0         0                  10.0.2.15:22                10.0.2.2:49870
ハガユウキハガユウキ

ソケットで抽象化されているからあんま意識しないけど、TCP/IPのネットワークで通信するときは、どのアプリケーションにパケットを届けるべきかを特定するために、ポート番号を使う。なので、プロセス同士がIPアドレスとポート番号を用いて通信しているのは何となく理解できた。tcpdumpで見てみたけど、プロセスは一度決めたポートで通信している気がするなあ。54321の方がサーバープロセス

vagrant@ubuntu-focal:~$ sudo tcpdump -i lo -tnlA "tcp"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 127.0.0.1.37576 > 127.0.0.1.54321: Flags [P.], seq 1142696138:1142696140, ack 3014003508, win 512, options [nop,nop,TS val 1864463442 ecr 1864414294], length 2
E..6-|@.@..D...........1D.(....4.....*.....
o!tRo .VH

IP 127.0.0.1.54321 > 127.0.0.1.37576: Flags [.], ack 2, win 512, options [nop,nop,TS val 1864463442 ecr 1864463442], length 0
E..4..@.@.v..........1.....4D.(......(.....
o!tRo!tR
IP 127.0.0.1.54321 > 127.0.0.1.37576: Flags [P.], seq 1:3, ack 2, win 512, options [nop,nop,TS val 1864463442 ecr 1864463442], length 2
E..6..@.@.v..........1.....4D.(......*.....
o!tRo!tRH

IP 127.0.0.1.37576 > 127.0.0.1.54321: Flags [.], ack 3, win 512, options [nop,nop,TS val 1864463442 ecr 1864463442], length 0
E..4-}@.@..E...........1D.(....6.....(.....
o!tRo!tR
ハガユウキハガユウキ

ループバックIPアドレス

ループバックアドレスとは、そのコンピュータ自身を示すIPアドレスのこと。物理的なネットワークインターフェース(NIC)ではなく、OSなどに実装された仮想的なNICであるループバックインターフェースに割り当てられる。

OSなどに実装された仮想的なNICであるループバックインターフェースに、ループバックIPアドレスが割り当てられるのか。なるほど。
https://e-words.jp/w/ループバックアドレス.html

ハガユウキハガユウキ

バイナリベースのプロトコル

  • ここまで扱ってきたのは、基本的にテキストベースのプロトコルであった。
    (エコーサーバは送られてきたバイト列をそのまま送り返すため、バイナリベースと解釈することもできる)
  • バイナリベースのプロトコルには、特有の気をつけるべきポイントがある。それは、バイトオーダーまたはエンディアンと呼ばれる概念である。
  • 0xは「この数字は16進数であることを表すprefix」

https://wa3.i-3-i.info/word1613.html

ハガユウキハガユウキ

Webとインターネットの違い

  • インターネットとは、ネットワークの集合体のこと。インターネットがあることであるネットワークにいるホストが別のネットワークにいるホストとデータをやり取りすることができるようになった。あるネットワークにいるホストが遥か先の別のネットワークのホストとデータをやり取りできるようになったのは、複数のルータが利用されているため(あるネットワークと別のネットワークは1つのルータによって接続されるため)。インターネットはTCP/IPのプロトコル群によって実現されている。インターネットのおかげで、ただのテキストメッセージとかもあるネットワークのホストから別のネットワークのホストに送れるようになる。

  • Webとは、TCP/IPによって構築されたインターネット上で、テキスト、画像、音声、動画といった様々な情報を参照することができるシステムのこと。Webでは、ざっくり言うとTCP/IPで構築されたインターネット上で、HTTPを用いてデータの転送を行っている。インターネットは、あるネットワークにいるホストと別のネットワークにいるホストと接続することに関心がある。Webではインターネットの仕組みを利用しつつ、どのようなフォーマットでどのような手順でデータを参照できるようにするかまで考えている。

http://www.coins.tsukuba.ac.jp/~syspro/2023/2023-06-28/
http://juen-cs.dl.juen.ac.jp/html/www/001/
http://www.coins.tsukuba.ac.jp/~yas/coins/syspro1-1997/1997-06-03/http.html

ハガユウキハガユウキ

テキストベースのプロトコルとバイナリベースのプロトコルの違いをざっくり理解する

テキストベースのプロトコルでは、あるフォーマットに従ったテキストをバイト列にエンコードしたものをサーバーに送っていた。レスポンスもバイト列で帰ってきたものをデコードしてテキストにしてアウトプットしていた。バイナリベースのプロトコルでは、バイナリフォーマットが決まっていて、それ通りにバイナリを可能してリクエストをする。HTTPは元々テキストベースのプロトコルだったが、HTTP/2.0からバイナリベースのプロトコルになった。

テキストベースのプロトコルにはセキュリティ上の問題があるそう。

HTTP/1はテキストベースのプロトコルだが、HTTP/2はバイナリだ。"バイナリのプロトコルは、解析のオーバヘッドが少なく、軽量だが、この大きな変更の本当の理由は、バイナリのプロトコルがシンプルで、エラーが起きにくいからだ。"氏はこの点について氏はテキストの範囲を確定する方法を例に説明しているが、HTTP/1とテキストの欠点は脆弱性にある。"HTTP/1のテキストベースの特性は多くのセキュリティ問題の原因になっている。メッセージの解析方法について異なる実装がさまざまな方針を打ち出しているため、悪意のある連中に隙を与えてしまう(例えば、レスポンス分割攻撃)."

https://www.infoq.com/jp/news/2014/02/http-2/
https://www.infraexpert.com/study/tcpip16.7.html
https://www.slideshare.net/techblogyahoo/http2-35029629
https://medium.com/walmartglobaltech/introduction-to-http-2-d3e3b4f4d662

ハガユウキハガユウキ

プロトコル

プロトコルは、フォーマットとプロシージャで構成されている。
フォーマットはやり取りするデータの構造を表していて、プロシージャはどのようにデータをやり取りするかの手順を取り決めたものである。プロトコルのプロシージャの実装には、何かしらのプログラミング言語が使用される。
https://www.f.waseda.jp/kane/sp_net/chap_3.html

ハガユウキハガユウキ

デジタルデータは0と1の羅列。このデジタルデータに意味を持たせるのがフォーマット。
フォーマットがあることで、先頭から4バイトに送信先のIPアドレスを、次の4バイトに送信元のIPアドレスを、その後に中身のデータをと決めることができる。

プロシージャはたとえばTCPプロトコルだとしたら、3ウェイハンドシェイク等である。

https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1387855663

ハガユウキハガユウキ

コンピュータのメインメモリ(主記憶装置)

  • 一般的なコンピュータのメインメモリは1バイト単位で場所を指定した読み書きができる。言い換えると、コンピュータが扱う命令やデータは、8ビット、つまり1バイトをひとまとまりとしてメインメモリに記憶される。メインメモリの内部は1バイトごとに区分けされ、0から始まる「アドレス(番地)」が付けられている。

https://slidesplayer.net/slide/15018522/
https://edn.itmedia.co.jp/edn/articles/1302/25/news008.html

ハガユウキハガユウキ

16進数の1つの桁は2進数で表すと最大4桁だから、16進数の各位の数を2進数の4桁で表すのは何となくわかるんだけど、重みとかの考慮ができているのか不安になってわからん。

ハガユウキハガユウキ

ビックエンディアン、リトルエンディアン

  • メインメモリの基準となるアドレスから、12, 34, 56, 78とアドレスの小さい番号から大きな番号に向けて順番にメインメモリに書き込むやり方はビッグエンディアンと呼ばれる。反対に78, 56, 34, 12がアドレスの小さい番号から大きい番号に向けてメインメモリに書くやり方はリトルエンディアンと呼ばれる。

https://e-words.jp/w/ビッグエンディアン.html#:~:text=ビッグエンディアンとは、複数,向けて順に取り扱う方式。

ハガユウキハガユウキ

こういったデータの表現方法を、総称してエンディアンやバイトオーダーといいます。そして、エンディアンはコンピュータが採用しているCPUのアーキテクチャや動作モードによって異なります。ビッグエンディアンとリトルエンディアンは、さまざまなCPUのアーキテクチャが採用している代表的なエンディアンです。

ハガユウキハガユウキ

TCP/IPでは異なるシステムをつなぐ。
CPUのアーキテクチャが異なっていても、ネットワークを流れるデータの順番、つまりバイトオーダーは統一されている必要がある。そこで、TCP/IPの世界ではバイトオーダーをビッグエンディアンに統一している。この点から、ネットワークにおけるビッグエンディアンをネットワークバイトオーダーと呼ぶことがある。対して、CPUのアーキテクチャに依存したコンピュータ内部での表現方法はホストバイトオーダーと呼ぶ。つまり、バイナリのデータをTCP/IPで送信するときは、ホストバイトオーダーをネットワークバイトオーダーに変換しなければならない。反対に、バイナリのデータをTCP/IPで受信するときは、ネットワークバイトオーダーをホストバイトオーダーに変換する必要がある。

ハガユウキハガユウキ

バイナリベースのプロトコルの仕様

  • 実際にバイナリベースのプロトコルを作ってみる
  • 足し算をサーバに依頼するプロトコルを作る。
  • サーバは、クライアントから2つの32ビットの整数を受け取って、それらを足し合わせて返す。単純だが、RPC(RemoteProcedureCall)の一種と解釈することもできる。

RPCとは、リモートプロシージャコールの略で、分散コンピューティング環境において、異なるプログラムやマシン間で関数呼び出しを行うプロトコルの一つです。

https://the-simple.jp/what-is-rpc-remote-procedure-call-titles-that-explain-basic-concepts-in-an-easy-to-understand-manner

分散コンピューティング(ぶんさんコンピューティング、英: distributed computing)とは、プログラムの個々の部分が同時並行的に複数のコンピュータ上で実行され、各々がネットワークを介して互いに通信を行いながら全体として処理が進行する計算手法のことである。複雑な計算などをネットワークを介して複数のコンピュータを利用して行うことで、一台のコンピュータで計算するよりスループットを上げようとする取り組み、またはそれを実現する為の仕組みである。分散処理(ぶんさんしょり)ともいう。

https://ja.wikipedia.org/wiki/分散コンピューティング

ハガユウキハガユウキ

クライアントからサーバに向けたリクエストのフォーマット

リクエストには2つの32ビット(4バイト)の整数として解釈されるフィールドが含まれる。

サーバからクライアントに向けたレスポンスのフォーマット

レスポンスには64ビット(8バイト)の整数として解釈されるフィールドが1つだけ含まれる。

このプロトコルをADDと名付ける。ADDプロトコルで通信するときのパケット(おそらく総称)は、概略図で示すと次のような構造になる。

https://e-words.jp/w/パケット.html

ハガユウキハガユウキ

バイナリベースのプロトコルのコード

サーバー側

#!/usr/bin/env python3

import socket
import struct

# このmsgがbytes型
def send_msg(sock, msg):
    total_sent_byte_length = 0
    total_message_byte_length = len(msg)
    while total_sent_byte_length < total_message_byte_length:
        # pythonのbytes型はgoのbyte sliceみたいな感じ
        sent_length = sock.send(msg[total_sent_byte_length:])
        if sent_length == 0:
            raise RuntimeError("socket connection broken")
        total_sent_byte_length += sent_length

def recv_msg(sock, total_msg_byte_size):
    total_recv_byte_size = 0
    while total_recv_byte_size < total_msg_byte_size:
        received_chunk = sock.recv(total_msg_byte_size - total_recv_byte_size)
        if len(received_chunk) == 0:
            raise RuntimeError("socket connection broken")
        yield received_chunk
        total_recv_byte_size += len(received_chunk)

def main():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    server_socket.bind(("127.0.0.1", 54321))
    server_socket.listen()
    print("starting server...")
    client_socket, (client_address, client_port) = server_socket.accept()
    print(f"accepted from {client_address}:{client_port}")
    received_msg = b"".join(recv_msg(client_socket, total_msg_byte_size=8))
    print(f"received: {received_msg}")

    # バイト列を2つの32ビットの整数として解釈する
    # 次の部分で、受信したネットワークバイトオーダーのバイト列をホストバイトオーダーのバイト列に変換している
    # バイトオーダーの変換は、Pythonではstructモジュールを使って実現できます。
    # ネットワークバイトオーダーをホストバイトオーダーに変換するにはunpack()関数を使います。
    # 引数の'!ii'という文字列は、受信したバイト列を2つの4バイト整数として解釈する、という意味です
    (operand1, operand2) = struct.unpack("!ii", received_msg)
    print(f"operand1:{operand1}, operand2:{operand2}")
    result = operand1 + operand2

    print(f"result: {result}")
    # ホストバイトオーダーをネットワークバイトオーダーに変換しているのが次の部分です。
    # ホストバイトオーダーのバイト列からネットワークバイトオーダーのバイト列への変換にはpack()関数を使います。
    # ここでは計算した値を一つの64ビット(8バイト)の整数として解釈できるように変換しています。
    result_msg = struct.pack("!q", result)
    send_msg(client_socket, result_msg)
    print(f"sent: {result_msg}")

    client_socket.close()
    server_socket.close()

if __name__ == "__main__":
    main()

クライアント側

#!/usr/bin/env python3

import socket
import struct

def send_msg(sock, msg):
    total_sent_byte_length = 0

    # get the length of a string
    total_msg_byte_length = len(msg)

    while total_sent_byte_length < total_msg_byte_length:
        # write a byte sequence to a socket and obtain the nubmer of bytes written.
        # an array that includes elements from index total_sent_byte_lenght to the end
        sent_byte_length = sock.send(msg[total_sent_byte_length:])

        if sent_byte_length == 0:
            raise RuntimeError("socket connection broken")

        total_sent_byte_length += sent_byte_length

def recv_msg(sock, total_msg_size):
    total_recv_size = 0

    while total_recv_size < total_msg_size:
        received_chunk = sock.recv(total_msg_size - total_recv_size)
        if len(received_chunk) == 0:
            raise RuntimeError("socket connection broken")
        yield received_chunk
        total_recv_size += len(received_chunk)

def main():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(("127.0.0.1", 54321))

    operand1, operand2 = 1000, 2000

    print(f"operand1: {operand1}, operand2: {operand2}")

    # ネットワークバイトオーダーのバイト列に変換する
    request_msg = struct.pack("!ii", operand1, operand2)

    # ソケットにバイト列を書き込む
    send_msg(client_socket, request_msg)
    print(f"sent: {request_msg}")

    # ソケットからバイト列を読み込む
    received_msg = b"".join(recv_msg(client_socket, 8))
    # 読み込んだバイト列を表示する
    print(f"received: {received_msg}")

    # 64ビットの整数として解釈する
    (added_value, ) = struct.unpack("!q", received_msg)

    print(f"result: {added_value}")
    client_socket.close()

if __name__ == "__main__":
    main()
ハガユウキハガユウキ

テキストベースのプロトコルでは、フォーマットに従った文字列をバイト列にして、それをサーバー側でデコードして読み取っていた。
バイナリベースのプロトコルでは、クライアント側もサーバー側もバイナリのフォーマットを暗黙的に知った上でバイト列を作成している。そのバイト列を送って、ある区切りで分けてデコードして値を取り出している。

ハガユウキハガユウキ

今まで実装してきた機能の問題点

ここまでに紹介してきたサーバの実装には、ある欠点があります。それは、同時に1つのクライアントとしか通信できない点です。サーバといえば、本来なら同時にたくさんの接続があるはずなので、これは問題といえるでしょう。ですが、この本ではソースコードがなるべく単純になるように、あえて問題を解決していません。ただし、もし解決するのであれば、次のような選択肢が考えられます。

  • それぞれのクライアントを処理するのに、専用のスレッドやプロセスを割り当てる
  • ノンブロッキングなソケットと、I/O多重化のシステムコール(selectなど)を組み合わせる

momijiame. Learning TCP/IP networking by exercise on Linux (Japanese Edition) (p. 306). Kindle Edition.

ハガユウキハガユウキ

まとめ

はじめに実装したのは、HTTPクライアントです。これは、ソケットをクライアントとして使って、テキストベースの通信をやり取りするパターンです。実験では、HTTPサーバにドキュメントを取得するリクエストを送って、レスポンスが返ってくるところを観察しました。次に実装したのは、エコーサーバです。これは、ソケットをサーバとして使って、テキストベースの通信をやり取りするパターンです。実験では、ncコマンドをクライアントとして使うことで、実装したサーバが送られてきたメッセージをオウム返しする様子を観察しました。最後に実装したのは、独自のバイナリベースのプロトコルを扱うサーバとクライアントです。バイナリベースのプロトコルでは、バイトオーダーという概念が重要になります。実験では、整数同士を足し算するプロトコルを実装することで、その考え方を知りました。

momijiame. Learning TCP/IP networking by exercise on Linux (Japanese Edition) (p. 307). Kindle Edition.

ハガユウキハガユウキ

エコーサーバは送られてきたバイト列をそのまま送り返すため、バイナリベースと解釈することもできます。

momijiame. Learning TCP/IP networking by exercise on Linux (Japanese Edition) (p. 306). Kindle Edition.

ハガユウキハガユウキ

echoコマンド

echoコマンドは、指定した引数の文字列を表示するコマンドである。

echo "Hello"
Hello

シェルに設定されている環境変数を確認したりもできる。

echo $HOME
/Users/yuuki_haga

リダイレクト

k

ヒアドキュメント

echoコマンドを使って、1行ずつファイルに追記するのは、行数が多いと大変。
そういう時はヒアドキュメントを使う。ヒアドキュメントとは、特定の文字列が入力されるまでをまとめて入力として扱う機能である。

このコマンドでは、EOFという文字列が入力されるまでを一連の入力としてファイルに書き込んでいる。

cat << "EOF" > ./sample.txt
heredoc> 1
heredoc> 2
heredoc> 3
heredoc> helo
heredoc> EOF
cat ./sample.txt
1
2
3
helo
ハガユウキハガユウキ

パイプ

パイプは、前のコマンドが出力した内容を、次のコマンドの入力につなげるための機能である。
grepコマンド使う時に、パイプをよく使う。

長いコマンドを読みやすくする

バックスラッシュ("")を使うことでコマンドを複数の行で分割できます。

echo $HOME \
> sample.txt
ハガユウキハガユウキ

ユーザーとsudoコマンド

  • Linuxにはシステムにログインするためのアカウントとして、ユーザーという概念がある。ユーザーのアカウントにはユーザー名や認証情報が含まれます。

whoamiコマンドを使えば、ログインしているユーザー名を確認できる。

whoami
yuuki_haga
  • また、Linuxのユーザーにはスーパーユーザーという考え方がある。スーパーユーザーはシステムの管理人である。スーパーユーザーはその他のユーザーでは実行できないような、システムを改変するような操作も実行できる。例えば、sudoコマンドを使うと、一時的にスーパーユーザーとしてコマンドを実行できる。
ハガユウキハガユウキ

top

topコマンドは、CPUやメモリの利用状況を表示するためのコマンドである。
このコマンドは定期的に表示をリフレッシュしながら実行を続ける。

ハガユウキハガユウキ

コマンド置換

コマンド置換では、ドル記号と括弧の中にコマンドを入力する。そうすると先にそちらのコマンドが実行されて、結果がコマンドの中に置換されるように振る舞う。
コマンド置換をすることで、ファイルの場所をwhichで特定して、そのパスをコピーして別のコマンドの引数としてペーストして実行する必要がなくなる。

which go
/usr/local/go/bin/go

vim $(which go)
ハガユウキハガユウキ

ネットワークインターフェイスが複数あれば、IPアドレスも複数あるかもしれないからです。あるいは、ひとつのネットワークインターフェイスに、複数のIPアドレスが付与されることもあります。そのため、どのIPアドレスを使ってクライアントから、の接続を待ち受けるか、bind()メソッドを使って指定します。もし、すべてのIPアドレスで接続を待ち受けるときはIPアドレスとして''(空文字)を指定します。

bindをする理由はこう言う理由だったのか。

このスクラップは2023/08/18にクローズされました