3章ではネットワークプログラミングについて学びます。まず3.1章で通信プロトコルについて学びます。そして3.2章以降で、ソケットを用いたネットワークプログラミングの技法を体得します。3章が終わる頃には、分散アルゴリズムの実装に向けて最低限必要なプログラミングのテクニックが身についていることでしょう。 # 3.1 通信プロトコル ## 3.1.1 コンピュータネットワークと通信プロトコル **コンピュータネットワークは、コンピュータ同士がコミュニケーションするための技術です**。そしてコンピュータネットワークを介して円滑にコミュニケーションするためには、主に以下の2点をお互いが守る必要があります。これらを定めたものを、**通信プロトコル**といいます。 - **やり取りの手順** - **フォーマット** **コミュニケーションには共通の通信プロトコルが不可欠です**。人間社会に例えると理解しやすいでしょう[^1]。ビジネスマンとヤンキーの間ではやり取りの手順が異なります。ビジネスマンはまず丁寧に会釈をし、その後本題を切り出します。しかしヤンキーはまず相手の学校に乗り込み、バット・竹刀等を駆使して威圧したあとに本題を切り出します。 また、やり取りの手順が同じビジネスマン同士でも、フォーマットが異なる場合があります。日本語話者のビジネスマンは「こんにちは」と喋るのに対して、マケドニア語話者のビジネスマンは「Здраво」と喋ります。 こうした例からわかるように、**やり取りの手順やフォーマットが異なると、お互いの意思疎通が難しい**のです。ビジネスマンとヤンキー、日本語話者とマケドニア語話者はいつまでも意思疎通できないでしょう。 ![[communication-different-protocol.excalidraw.svg]] <center><b>Fig 1: やり取りの手順・フォーマットが異なることでコミュニケーションが破綻している図</b></center> ## 3.1.2 プロトコルの階層 前章では、コンピュータ同士で通信するためにはプロトコルを定める必要があり、プロトコルはやり取りの手順およびフォーマットを定めたものだということを学びました。 具体的には、0と1を何で(光、電気、電波)どう表現し、どのような手順でやり取りするか(挨拶の仕方、一部の情報が失われたら送り直す方法など)を定める必要があります。 これらを1つのプロトコルで定めてしまうと、仕様書が膨大になってしまいます。そのため、**いくつかのレイヤに分けて通信プロトコルが標準化されています**。レイヤ分けのモデルとしては、国際標準化機構(ISO)によって策定された**OSI参照モデル**が有名です。 <center><b>Table 1: OSI参照モデル</b></center> | レイヤ番号 | レイヤ名 | 機能 | 代表的なプロトコル | | ----- | ---------- | -------------------------------------------------------- | ------------------ | | 7 | アプリケーション層 | アプリケーションごと必要なやり取りの手順を定義 | HTTP・IMAP・FTP | | 6 | プレゼンテーション層 | 通信に用いるデータの表現形式(バイナリ)とアプリケーション層で取り扱うデータの表現形式を相互に変換する方法を定義 | 文字コード・CSV・JSON | | 5 | セッション層 | 送信者と受信者の間でお互いの存在を確認し、セッションを確立してデータを送り合う方法を定義 | TLS | | 4 | トランスポート層 | メッセージを確実に宛先アプリケーションに届ける方法を定義 | TCP・UDP | | 3 | ネットワーク層 | 複数の機器間でメッセージをルーティングし、宛先に届ける方法を定義 | IP(IPv4・IPv6)・ICMP | | 2 | データリンク層 | 2台の機器の間をつなぐ方法を定義 | Ethernet・Wi-Fi・ARP | | 1 | 物理層 | メディアを使って0・1を表現し、届ける方法を定義 | ISDN, 1000BASE-T | **各レイヤは、下層のレイヤに依存しながらそれぞれの仕事をこなし、上位レイヤにサービスを提供します。** 本資料ではOSI参照モデルの詳細には触れませんが、気になる方は勉強してみてください。 ## 3.1.3 IPとTCP・UDP インターネットでは、主にトランスポート層プロトコルとしてTCP・UDPが、ネットワーク層のプロトコルとしてInternet Protocol(IP)が使われます。本セクションでは、IPおよびTCP・UDPの概要について説明します。 IPは、インターネットを介して送信側の端末から受信側の端末までデータを送るためのプロトコルです。 IPでは、それぞれの端末を識別するためにIPアドレスと呼ばれる識別子を用います。IPアドレスの長さは32bitであり[^2]、1バイトごと`.`で区切った上で10進数で表すことが一般的です(例: `133.27.4.220`)。 このIPアドレスを用いて受信者のマシンを指定すると、たくさんのルーターを介してバケツリレーのようにデータが届けられます。 トランスポート層の通信方式は、コネクション型とコネクションレス型に大別されます。IPの上では、トランスポート層プロトコルとしてコネクション型のTCPとコネクションレス型のUDPが広く使われています。 コネクション型はデータの送信を開始する前に、受信側と送信側の間に論理的な回線を引く方式です。TCPではまずコネクションを確立してから通信を始め、通信が終わったらコネクションを切断します。コネクション型はストリーム指向であるとも言われ、一度コネクションを確立すれば切断するまでの間、たくさんのデータを双方向に流し続けることができます。 TCPはしばしば電話に例えられます。電話で誰かと話すときは、まず相手の電話番号を入力して、相手が電話に出てから話し始めますよね。そして長話を続けたあと、会話が終わったら受話器をおいて電話を切ります。 コネクションレス型は、名前の通りそうしたコネクションの確立や切断といった処理がありません。UDPでは、送信側がいつでも受信側めがけてパケットと呼ばれる通常数キロバイトサイズのデータの塊を相手にを送ることができます(送るだけです)。受け取り手はいつ誰からメッセージを受け取るかわかりません。 これはしばしば郵便に例えられます。送信者は好きなときに送りたい住所に小包を出すことができます。受信側のポストには、適当なタイミングに不特定多数からの小包が届きます(相手がもうそこに住んでいなくて届かないかもしれません)。 両者にはそれぞれ、メリットとデメリットがあります。TCPは、コネクションが張られている限りメッセージが順番通りに全て届くよう、順序保証・再送制御を行います。メッセージの一部が欠けたり、バラバラの順番で届くことを防ぎます。一方のUDPはそういった保証はありません。メッセージはバラバラの順番で届き、ある確率でロストします。しかしコネクションを張ったり到達確認をする必要がなく、高速です。 両者の性質を活かして、TCPはWebやファイル転送などの確実にデータを届けることが要求されるアプリケーションで、UDPはビデオ通話等の多少データが欠けても問題なく、レイテンシーをなるべく低く抑えたいアプリケーションで使われています。 ![[tcp-udp.excalidraw.svg]] <center> <b>Fig x: TCP・UDPのイメージ</b> </center> ![TCP vs UDP](https://i.redd.it/duv11av99nm11.png) <center> <b>Fig x: TCPとUDPの違いを端的に表現したMEME</b> <a href="https://www.reddit.com/r/ProgrammerHumor/comments/9gcwgw/tcp_vs_udp/">r/ProgrammingHumorのスレッド</a>より引用。TCPはペットボトルと口の間にコネクションを張って、水が確実に飲めるよう工夫している。UDPは多数の水しぶきを自由にに浴びている。 </center> TPCもUDPも、識別子としてPort番号を用います。Port番号の長さは16bitであり、10進数で表現されます(例: `8080`)。IPアドレスが宛先の端末を示すのに対し、Port番号は端末の中で動いているアプリケーションを指し示します。 # 3.2 ネットワークプログラミング それでは、実際にTCPとUDPを用いたネットワークプログラミングにチャレンジしましょう! まず最初に、OSで提供されているネットワークプログラミング用のAPIであるソケットについて学びます。そして、ソケットを使って簡単なメッセージングプログラムを実装します。 ## 3.2.1 Socket 相手のコンピュータと通信するときに、プロトコルを一から実装する必要はありません。実は、トランスポート層以下はOSに実装されており、アプリケーションプログラマはSocketと呼ばれるAPIを通じて呼び出すことができます。 Soketは土管のようなイメージです(Fig x)。Socketの一方の端に向けてデータを投げ込むと、もう片方の端から取り出すことができます。 ![[socket.excalidraw.svg]] <center> <b>Fig x: Socketのイメージ</b> </center> ## 3.2.1 UDPでデータを送ってみる Socketを使ってデータを送ってみましょう。 **コード1-1: UDPでデータを送るプログラム(送信側)** ```python import socket import time def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for i in range(5): msg: str = f"hello, from sender {i}" data: bytes = msg.encode("utf-8") sock.sendto(data, ("127.0.0.1", 8000)) time.sleep(1) end_msg: str = "END" end_data: bytes = end_msg.encode("utf-8") sock.sendto(end_data, ("127.0.0.1", 8000)) sock.close() main() ``` **コード1-2: UDPでデータを送るプログラム(受信者側)** ```python import socket import time def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("127.0.0.1", 8000)) while True: data: bytes = sock.recv(1024) msg = data.decode("utf-8") print(msg) if msg == "END": break sock.close() main() ``` **実行結果** ```sh # Terminal 1 hiroki@HirokiMBP16 codes % uv run --python 3.14t 03/udp_receiver.py hello, from sender 0 hello, from sender 1 hello, from sender 2 hello, from sender 3 hello, from sender 4 END # Terminal 2 hiroki@HirokiMBP16 codes % uv run --python 3.14t 03/udp_sender.py ``` ## 3.2.2 TCPでデータを送ってみる 送ってみよう **コード2-1: TCPでデータを送るプログラム(送信側)** ```python import socket def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("127.0.0.1", 9000)) for i in range(5): msg = f"hello, from sender {i}" data = msg.encode("utf-8") sock.send(data) end_msg: str = "END" end_data: bytes = end_msg.encode("utf-8") sock.send(end_data) sock.close() main() ``` **コード2-2: TCPでデータを送るプログラム(受信側)** ```python import socket def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 9000)) sock.listen(1) client_sock, sender_addr = sock.accept() while True: data = client_sock.recv(1024) if len(data) > 0: msg = data.decode("utf-8") print(msg) else: client_sock.close() break client_sock.close() sock.close() main() ``` **実行結果** ```sh # Terminal 1 hiroki@HirokiMBP16 codes % uv run --python 3.13t 03/tcp_receiver.py hello, from sender 0hello, from sender 1hello, from sender 2hello, from sender 3hello, from sender 4END # Terminal 2 uv run --python 3.13t 03/tcp_sender.py ``` 双方向に送ってみよう **コード3-1 TCPで双方向にデータを送るプログラム(送信側)** ```python import socket import time def recv_oneline(sock: socket.socket) -> bytes: data = bytes() while True: d = sock.recv(1) if len(d) == 0 or d == b"\n": break data += d return data def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("127.0.0.1", 9000)) for i in range(5): msg = f"hello, from sender ({i})\n" data = msg.encode("utf-8") sock.send(data) recv_data = recv_oneline(sock) msg = recv_data.decode("utf-8") print(msg) time.sleep(1) main() ``` **コード3-2 TCPで双方向にデータを送るプログラム(受信側)** ```python import socket def handler(sock: socket.socket): for i in range(5): data = recv_oneline(sock) if len(data) == 0: continue msg = data.decode('utf-8') print(msg) echo_msg = "ECHO: " + msg + "\n" echo_data = echo_msg.encode("utf-8") sock.send(echo_data) sock.close() def recv_oneline(sock: socket.socket) -> bytes: data = bytes() while True: d = sock.recv(1) if len(d) == 0 or d == b"\n": break data += d return data def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 9000)) sock.listen(1) while True: client_sock, sender_addr = sock.accept() try: handler(client_sock) except: continue main() ``` **実行結果** ```sh # Terminal 1 hiroki@HirokiMBP16 03 % uv run --python 3.13t tcp_echo_receiver.py hello, from sender (0) hello, from sender (1) hello, from sender (2) hello, from sender (3) hello, from sender (4) # Terminal 2 hiroki@HirokiMBP16 03 % uv run --python 3.13t tcp_echo_sender.py ECHO: hello, from sender (0) ECHO: hello, from sender (1) ECHO: hello, from sender (2) ECHO: hello, from sender (3) ECHO: hello, from sender (4) ``` ↑同時に複数のsenderを立ち上げてみて、順番にしか処理されなくてこまることを実演する ## 3.2.3 TCPで複数のコネクションをさばく(マルチスレッド) マルチスレッドでハンドリングしよう! 受診側だけ改変すればOK(送信側はコード3-1を使用) **コード4-1 TCPで双方向にデータを送るプログラム(マルチスレッド版、受信側)** ```python import socket import threading from typing import List def handler(sock: socket.socket): for i in range(5): data = recv_oneline(sock) if len(data) == 0: continue msg = data.decode('utf-8') print(msg) echo_msg = "ECHO: " + msg + "\n" echo_data = echo_msg.encode("utf-8") sock.send(echo_data) def recv_oneline(sock: socket.socket) -> bytes: data = bytes() while True: d = sock.recv(1) if len(d) == 0 or d == b"\n": break data += d return data def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 9000)) sock.listen(1) while True: client_sock, sender_addr = sock.accept() t = threading.Thread(target=handler, args=(client_sock,)) t.start() main() ``` --- 前ページ: [[02 マルチスレッドプログラミング]] [^1]: 人間社会でもプロトコルは非常に重要です。実際、プロトコルという用語は外交分野からもたらされたものです。[参考](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB) [^2]: これはIPv4の場合です。IPv6では128bitに拡張され、表記法も異なっています。