Skip to content

Instantly share code, notes, and snippets.

@grgr-dkrk
Last active December 21, 2023 16:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grgr-dkrk/99d9fb77f592f8a23a8f1cfff0d5afe2 to your computer and use it in GitHub Desktop.
Save grgr-dkrk/99d9fb77f592f8a23a8f1cfff0d5afe2 to your computer and use it in GitHub Desktop.
LINUX で動かしながら学ぶ TCP/IP ネットワーク入門(Kindle Unlimited のやつ)

メモ

linuxで動かしながら学ぶtcp/ipネットワーク入門(https://www.amazon.co.jp/dp/B085BG8CH5) のコードを macOS Big Sur(Intel) + Docker で動かすためのメモ

Docker

※ privileged がないとあとで netns の追加ができない

docker run -it -d --privileged --name tcpip ubuntu:20.04

環境導入

apt update && \
apt install sudo && \
sudo apt-get update && \
sudo apt-get -y install bash coreutils grep iproute2 iputils-ping traceroute tcpdump bind9-dnsutils dnsmasq-base netcat-openbsd python3 curl wget iptables procps isc-dhcp-client

Network Namespace(ルーターなし)

つくる

sudo ip netns add ns1
sudo ip netns add ns2

繋げる

veth(Virtual Ethernet Device)

sudo ip link add ns1-veth0 type veth peer name ns2-veth0
ip link show | grep veth

veth インターフェースを netns にセット

sudo ip link set ns1-veth0 netns ns1
sudo ip link set ns2-veth0 netns ns2
sudo ip netns exec ns1 ip link show | grep veth
sudo ip netns exec ns2 ip link show | grep veth

IP アドレスの追加

※ ドキュメンテーションアドレスを使用 ※ 同じセグメント(192.0.2) CIDR(/24)

sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0

ネットワークインタフェースを UP に設定する

sudo ip netns exec ns1 ip link set ns1-veth0 up
sudo ip netns exec ns2 ip link set ns2-veth0 up

# ping
sudo ip netns exec ns1 ping -c 3 192.0.2.2

消す

sudo ip --all netns delete

Network Namespace(ルーターあり)

つくる

sudo ip netns add ns1
sudo ip netns add router
sudo ip netns add ns2
sudo ip link add ns1-veth0 type veth peer name gw-veth0
sudo ip link add ns2-veth0 type veth peer name gw-veth1

セット

sudo ip link set ns1-veth0 netns ns1
sudo ip link set gw-veth0 netns router
sudo ip link set gw-veth1 netns router
sudo ip link set ns2-veth0 netns ns2

ns1 とルーターの IP アドレス

※ セグメント 192.0.2.0/24

sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0

ルーターと ns2 の IP アドレス

※ セグメント 198.51.100.0/24 ※ ns1 とは異なるホストアドレスということ

sudo ip netns exec router ip address add 198.51.100.254/24 dev gw-veth1
sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0

# ping
sudo ip netns exec ns1 ping -c 3 192.0.2.254 -I 192.0.2.1
sudo ip netns exec ns2 ping -c 3 198.51.100.254 -I 198.51.100.1

UP

sudo ip netns exec ns1 ip link set ns1-veth0 up
sudo ip netns exec router ip link set gw-veth1 up
sudo ip netns exec router ip link set gw-veth2 up
sudo ip netns exec ns2 ip link set ns2-veth0 up

ns とルーター間で ping

sudo ip netns exec ns1 ping -c 3 192.0.2.254 -I 192.0.2.1
sudo ip netns exec ns2 ping -c 3 198.51.100.254 -I 198.51.100.1

ルーター越しに ping(失敗)

繋がらないので、ルーティングエントリを追加する必要がある

# ping: sendmsg: Network is unreachable が出る
sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1

# ルーティングテーブルの確認
sudo ip netns exec ns1 ip route show

ルーティングエントリの追加

ns1 でルーティングエントリに一致しない場合(default)、ネクストホップ 192.0.2.254 に渡すようにする(つまりさっきルーター gw-veth0 に設定した IP アドレス) ns2 も同じやり方で、gw-veth1 に設定した IP アドレスに渡す

sudo ip netns exec ns1 ip route add default via 192.0.2.254
sudo ip netns exec ns2 ip route add default via 198.51.100.254

ルーター越しに ping(成功)

sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1

ping の結果でもしパケットロスが出ている場合は、カーネル側で IPv4 のルータとして動作しないようになっている
sysctl で設定を変える

sudo ip netns exec router sysctl net.ipv4.ip_forward=1

消す

sudo ip --all netns delete

ルーターを増やす

ルーター1 と ルーター2 を設置する

つくる

前と同じなので詳しくは省略

sudo ip netns add ns1 && sudo ip netns add router1 && sudo ip netns add router2 && sudo ip netns add ns2

veth インターフェースの作成

前と同じなので詳しくは省略

sudo ip link add ns1-veth0 type veth peer name gw1-veth0 && sudo ip link add gw1-veth1 type veth peer name gw2-veth0 && sudo ip link add gw2-veth1 type veth peer name ns2-veth0

セット

前と同じなので詳しくは省略

sudo ip link set ns1-veth0 netns ns1 && sudo ip link set gw
1-veth0 netns router1 && sudo ip link set gw1-veth1 netns router1 && sudo ip link set gw2-veth0 netns router2 && sudo ip link set gw2-veth1 netns router2 && sudo ip link set ns2-veth0 netns ns2

UP

前と同じなので詳しくは省略

sudo ip netns exec ns1 ip link set ns1-veth0 up && \
sudo ip netns exec router1 ip link set gw1-veth0 up && \
sudo ip netns exec router1 ip link set gw1-veth1 up && \
sudo ip netns exec router2 ip link set gw2-veth0 up && \
sudo ip netns exec router2 ip link set gw2-veth1 up && \
sudo ip netns exec ns2 ip link set ns2-veth0 up

IP アドレスの追加

※ ルーターが 1 つ増えたのでセグメントも 1 つ増える 203.0.113.0

sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0 && \
sudo ip netns exec router1 ip address add 192.0.2.254/24 dev gw1-veth0 && \
sudo ip netns exec router1 ip address add 203.0.113.1/24 dev gw1-veth1 && \
sudo ip netns exec router2 ip address add 203.0.113.2/24 dev gw2-veth0 && \
sudo ip netns exec router2 ip address add 198.51.100.254/24 dev gw2-veth1 && \
sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0

ルーティングエントリの追加(Default)

前と同じなので詳しくは省略

sudo ip netns exec ns1 ip route add default via 192.0.2.254 && \
sudo ip netns exec ns2 ip route add default via 198.51.100.254

Destination Net Unreachable(失敗)

ping すると Destination Net Unreachable になる。
From の 192.0.2.0 は先ほど router1 に設定した セグメント。
router1 が 198.51.100.1 をどこに渡すかわからない。

sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1
# PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
# From 192.0.2.254 icmp_seq=1 Destination Net Unreachable
# From 192.0.2.254 icmp_seq=2 Destination Net Unreachable
# From 192.0.2.254 icmp_seq=3 Destination Net Unreachable

ルーターにルーティングエントリを追加

ルーター1 のセグメント: 192.0.2.0.254/24 ルーター2 のセグメント: 198.51.100.0/24

ルーター1 がルーター2 を、ルーター2 がルーター1 の IP アドレスを指定すればいい

sudo ip netns exec router1 ip route add 198.51.100.0/24 via 203.0.113.2 && \
sudo ip netns exec router2 ip route add 192.0.2.0/24 via 203.0.113.1

手動でルーティングエントリを追加する方式をスタティックルーティングという。

一方で BGP や OSPF などのルーティングプロトコルを用いて、ルータ同士が自分自身のルーティング情報を交換する方式もあり、そちらはダイナミックルーティングという。

消す

sudo ip --all netns delete

イーサネット

NS つくる〜 IP アドレス追加まで

今までと同じなので省略

sudo ip netns add ns1 && \
sudo ip netns add ns2 && \
sudo ip link add ns1-veth0 type veth peer name ns2-veth0 && \
sudo ip link set ns1-veth0 netns ns1 && \
sudo ip link set ns2-veth0 netns ns2 && \
sudo ip netns exec ns1 ip link set ns1-veth0 up && \
sudo ip netns exec ns2 ip link set ns2-veth0 up && \
sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0 && \
sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0

MAC アドレスの指定

sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01 && \
sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02

パケットキャプチャ

sudo ip netns exec ns1 tcpdump -tnel -i ns1-veth0 icmp

もう一個ターミナルを立ち上げ

sudo ip netns exec ns1 ping -c 3 192.0.2.2 -I 192.0.2.1

ARP を観察

# 観察に影響が出るので、一回 MAC アドレスのキャッシュを消す
sudo ip netns exec ns1 ip neigh flush all

さっき tcpdump を実行したターミナルで

sudo ip netns exec ns1 tcpdump -tnel -i ns1-veth0 icmp or arp

ARP で MAC アドレスを解決するのは IPv4 のみ。 IPv6 は ICMPv6 というプロトコルで Neighbor Discoevry という仕組みを使う。

NS 消す

sudo ip --all netns delete

イーサネット(異なるフレームに積み替えられる様子の観察)

NS1, ルーター, NS2 の構成で作る

今までと同じなので省略

sudo ip netns add ns1 && \
sudo ip netns add router && \
sudo ip netns add ns2 && \
sudo ip link add ns1-veth0 type veth peer name gw-veth0 && \
sudo ip link add ns2-veth0 type veth peer name gw-veth1 && \
sudo ip link set ns1-veth0 netns ns1 && \
sudo ip link set gw-veth0 netns router && \
sudo ip link set gw-veth1 netns router && \
sudo ip link set ns2-veth0 netns ns2 && \
sudo ip netns exec ns1 ip link set ns1-veth0 up && \
sudo ip netns exec router ip link set gw-veth0 up && \
sudo ip netns exec router ip link set gw-veth1 up && \
sudo ip netns exec ns2 ip link set ns2-veth0 up && \
sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0 && \
sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0 && \
sudo ip netns exec router ip address add 198.51.100.254/24 dev gw-veth1 && \
sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0 && \
sudo ip netns exec ns1 ip route add default via 192.0.2.254 && \
sudo ip netns exec ns2 ip route add default via 198.51.100.254

MAC アドレスの設定

sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:11 && \
sudo ip netns exec router ip link set dev gw-veth0 address 00:00:5E:00:53:12 && \
sudo ip netns exec router ip link set dev gw-veth1 address 00:00:5E:00:53:21 && \
sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:22

tcpdump

ターミナルを 3 つ立ち上げ

1 (gw-veth0)

sudo ip netns exec router tcpdump -tnel -i gw-veth0 icmp or arp

2 (gw-vwth1)

sudo ip netns exec router tcpdump -tnel -i gw-veth1 icmp or arp

3 (ping)

sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1

消す

sudo ip -all netns delete

ネットワークブリッジの作成

NS作る

sudo ip netns add ns1 && \
sudo ip netns add ns2 && \
sudo ip netns add ns3

ブリッジの NS 作る

sudo ip netns add bridge

veth インターフェース 〜 Mac アドレスのセットまで

省略

sudo ip link add ns1-veth0 type veth peer name ns1-br0 && \
sudo ip link add ns2-veth0 type veth peer name ns2-br0 && \
sudo ip link add ns3-veth0 type veth peer name ns3-br0 && \
sudo ip link set ns1-veth0 netns ns1 && \
sudo ip link set ns2-veth0 netns ns2 && \
sudo ip link set ns3-veth0 netns ns3 && \
sudo ip link set ns1-br0 netns bridge && \
sudo ip link set ns2-br0 netns bridge && \
sudo ip link set ns3-br0 netns bridge && \
sudo ip netns exec ns1 ip link set ns1-veth0 up && \
sudo ip netns exec ns2 ip link set ns2-veth0 up && \
sudo ip netns exec ns3 ip link set ns3-veth0 up && \
sudo ip netns exec bridge ip link set ns1-br0 up && \
sudo ip netns exec bridge ip link set ns2-br0 up && \
sudo ip netns exec bridge ip link set ns3-br0 up && \
sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0 && \
sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0 && \
sudo ip netns exec ns3 ip address add 192.0.2.3/24 dev ns3-veth0 && \
sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01 && \
sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02 && \
sudo ip netns exec ns3 ip link set dev ns3-veth0 address 00:00:5E:00:53:03

ブリッジの作成

※ UP が必要なのでいっしょにやる

sudo ip netns exec bridge ip link add dev br0 type bridge && \
sudo ip netns exec bridge ip link set br0 up

veth インターフェースとブリッジの接続

sudo ip netns exec bridge ip link set ns1-br0 master br0 && \
sudo ip netns exec bridge ip link set ns2-br0 master br0 && \
sudo ip netns exec bridge ip link set ns3-br0 master br0

ping

sudo ip netns exec ns1 ping -c 3 192.0.2.2 -I 192.0.2.1
sudo ip netns exec ns1 ping -c 3 192.0.2.3 -I 192.0.2.1

MAC アドレスの観測

ターミナルを2 つ立ち上げ

1 (ping)

sudo ip netns exec ns1 ping 192.0.2.2 -I 192.0.2.1

2 (tcpdump)

sudo ip netns exec ns2 tcpdump -tnel -i ns2-veth0 icmp

ns3 のインターフェースを観測しても、フレームが転送されていないことがわかる。
ブリッジが、ポートの先にある MAC アドレスを把握していて、ns3 に通信相手がないことを知っているから。

sudo ip netns exec ns3 tcpdump -tnel -i ns3-veth0 icmp

ネットワークブリッジの MAC アドレスの対応関係(MAC アドレステーブル) を調べてみる

sudo ip netns exec bridge bridge fdb show br br0 | grep -i 00:00:5e

UDP

destination port の決め方

サーバー->クライアント サーバとなるアプリケーションが通信しようとするプロトコル(トランスポート層より上位層のプロトコル)によって決まる。
例えば DNS では 53 を使う。

クライアント->サーバー
への通信で、アプリケーションから特に指定がない限り OS 側が自動的に選んで割り当てるポート番号(エフェメラルポート)

ダイナミックポート(プライベートポート)の範囲

49152〜65535
それ未満は IANA が用途を管理しているので省略

nc コマンドを使った簡易 UDP

※nc は netcat の略 ターミナルを3台立ち上げる

1: サーバーの立ち上げ

# -u UDPで
# -l さーばーとして
# -n IP アドレスを DNS で名前解決させない
# -v コマンドの詳細表示
nc -ulnv 127.0.0.1 54321

2: クライアントの立ち上げ

nc -u 127.0.0.1 54321

3: tcpdump

sudo tcpdump -i lo -tnlA "udp and port 54321"

この状態で 2(クライアント)または1(サーバー)で何か入力し、Enterを押すと経路が追える

TCP

nc コマンドを使った簡易 TCP

ターミナルを3台立ち上げる

1: サーバーの立ち上げ

# -l さーばーとして
# -n IP アドレスを DNS で名前解決させない
# -v コマンドの詳細表示
nc -ulnv 127.0.0.1 54321

2: tcpdump(TCP の場合はクライアントの接続と同時に通信が始まるので、先にやる)

sudo tcpdump -i lo -tnlA "tcp and port 54321"

3: クライアントの立ち上げ

nc 127.0.0.1 54321

この時の tcpdump を見れば、3WAYハンドシェイクが行われる(クライアント->サーバー, サーバー->クライアント, クライアント->サーバー)ことがわかる。

※SYN, SYN/ACK の見方
tcpdump の Flags[] の部分。Sが SYN で . が ACK さらにクライアントで文字列を入力して送ると、P フラグが立つ、これは PSH (受信側に、今あるデータをすぐに処理してくれと言うこと)

HTTP/1.0, HTTP/1.1

ワークスペースの作成&移動

mkdir -p /var/tmp/http-home
cd /var/tmp/http-home

HTML ファイルの作成

cat << 'EOF' > index.html
<!doctype html>
<html>
  <head>
    <title>Hello, World!</title>
  </head>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>
EOF

HTTP サーバーへの接続

ターミナルを 2 台起動し

1: httpサーバーの起動

sudo python3  -m http.server -b 127.0.0.1 80

2: HTTPクライアントを使って接続

echo -en "GET / HTTP/1.0\r\n\r\n" | nc 127.0.0.1 80

# または
curl -X GET -D - http://127.0.0.1/

localhost の名前解決を確認(リゾルバ -> ホスト)

grep 127.0.0.1 /etc/hosts

ネームサーバを使った名前解決

ターミナルを 2 台起動

1: tcpdump ※ UDP のポート 53 は DNS に使われるもの

sudo tcpdump -tnl -i any "udp and port 53"

2: dig

# A は A レコード(ドメイン名に対応するIPv4 アドレス)
dig +short @8.8.8.8 example.org A

DHCP

NS, veth つくる, リンク, UP

sudo ip netns add server && \
sudo ip netns add client && \
sudo ip link add s-veth0 type veth peer name c-veth0 && \
sudo ip link set s-veth0 netns server && \
sudo ip link set c-veth0 netns client && \
sudo ip netns exec server ip link set s-veth0 up && \
sudo ip netns exec client ip link set c-veth0 up

IP アドレスの設定

server のインターフェースのみに設定する。
client は DHCP プロトコルで設定するため。

sudo ip netns exec server ip address add 192.0.2.254/24 dev s-veth0

DHCP サーバーの起動

ターミナルを 2 台起動

1: DHCPサーバー立ち上げ

sudo ip netns exec server dnsmasq \
--dhcp-range=192.0.2.100,192.0.2.200,255.255.255.0 \
--interface=s-veth0 \
--port 0 \
--no-resolv \
--no-daemon

2: クライアントから接続

sudo ip netns exec client dhclient -d c-veth0

bound to で IP アドレスが配布されているはず。

↓で client に IP アドレスやデフォルトルートが割り当てられていることが確認できる。

sudo ip netns exec client ip address show | grep "inet "
sudo ip netns exec client ip route show

消す

sudo ip --all netns delete

Source NAT

lan, router, wan を用意〜IPアドレスのセットまで

lan の 192.0.2.0/24 のセグメントを、Source NAT で 203.0.113.254 に置き換えること

sudo ip netns add lan && \ 
sudo ip netns add router && \ 
sudo ip netns add wan && \ 
sudo ip link add lan-veth0 type veth peer name gw-veth0 && \ 
sudo ip link add wan-veth0 type veth peer name gw-veth1 && \ 
sudo ip link set lan-veth0 netns lan && \ 
sudo ip link set gw-veth0 netns router && \ 
sudo ip link set gw-veth1 netns router && \ 
sudo ip link set wan-veth0 netns wan && \ 
sudo ip netns exec lan ip link set lan-veth0 up && \ 
sudo ip netns exec router ip link set gw-veth0 up && \ 
sudo ip netns exec router ip link set gw-veth1 up && \ 
sudo ip netns exec wan ip link set wan-veth0 up && \ 
sudo ip netns exec lan ip address add 192.0.2.1/24 dev lan-veth0 && \ 
sudo ip netns exec lan ip route add default via 192.0.2.254 && \ 
sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0 && \ 
sudo ip netns exec router ip address add 203.0.113.254/24 dev gw-veth1 && \ 
sudo ip netns exec wan ip address add 203.0.113.1/24 dev wan-veth0 && \ 
sudo ip netns exec wan ip route add default via 203.0.113.254

NAT の設定

テーブル形式で現在の設定を表示

sudo ip netns exec router iptables -t nat -L

"Chain PREROUTING" など、処理を適用するタイミング(Chain)に何もないことを確認

-A でチェインを指定、今回はPOSTROUTING(ルーティングが終わり、パケットがインターフェースから出ていく直前)
-s は対象となる IP アドレスの範囲
-o 処理の対象とする出力先のネットワークインターフェイス -j 条件に一致したパケットをどのように処理するか(target)ここでは IP マスカレード(Linux の Souce NAT の実装)

sudo ip netns exec router iptables -t nat \
 -A POSTROUTING \
 -s 192.0.2.0/24 \
 -o gw-veth1 \
 -j MASQUERADE

もう一回確認

sudo ip netns exec router iptables -t nat -L

通信の確認

ターミナルを 2 台起動

1: ping

sudo ip netns exec lan ping 203.0.113.1

2: tcpdump

LAN 側は今まで通り

sudo ip netns exec lan tcpdump -tnl -i lan-veth0 icmp

WAN 側は router の WAN 側に付与されている IP アドレスでやりとりされている

sudo ip netns exec wan tcpdump -tnl -i wan-veth0 icmp

※ Distination NAT に設定を再利用するので NS は消さない

Distination NAT

ルール設定

-A PREROUTING はインターフェースからパケットが入ってきた直後 -p 処理の対象となるトランスポート層のプロトコル --dport 処理の対象となるポート番号 -d 書き換える前の IP アドレス -j 条件に一致したパケットをどのように処理するか(target) ここでは Distination NAR であることを示す --to-destination 書き換えた後のアドレス

sudo ip netns exec router iptables -t nat \
 -A PREROUTING \
 -p tcp \
 --dport 54321 \
 -d 203.0.113.254 \
 -j DNAT \
 --to-destination 192.0.2.1

確認

ターミナルを 2 台起動

1: 54321 ポートを待ち受けるサーバの起動

sudo ip netns exec lan nc -lnv 54321
  1. WAN 側の NS からサーバーにアクセス
sudo ip netns exec wan nc 203.0.113.254 54321

tcpdump 確認(WAN)

ターミナルを 3 台起動

1: tcpdump

sudo ip netns exec wan tcpdump -tnl -i wan-veth0 "tcp and port 54321"
  1. サーバー
sudo ip netns exec lan nc -lnv 54321
  1. クライアント
sudo ip netns exec wan nc 203.0.113.254 54321
Hello, World!

ルーターのグローバルアドレスに相当する IP アドレスが送信元になっている

tcpdump 確認(LAN)

ターミナルを 3 台起動

1: tcpdump

sudo ip netns exec lan tcpdump -tnl -i lan-veth0 "tcp and port 54321"
  1. サーバー
sudo ip netns exec lan nc -lnv 54321
  1. クライアント
sudo ip netns exec wan nc 203.0.113.254 54321
Hello, World!

送信先の IP アドレスが LAN のセグメントに付与されたものになっている

ソケットプログラミング

  1. HTTPクライアント
  2. エコーサーバ
  3. 独自のバイナリベースのプロトコルを実装したサーバとクライアント

準備

mkdir -p /var/tmp/http-home && \
cd /var/tmp/http-home/
cat << 'EOF' > index.html
<!doctype html>
<html>
  <head>
    <title>Hello, World</title>
  </head>
  <body>
    <h1>Hello, World</h1>
  </body>
</html>
EOF

Python の HTTP サーバーを実行

sudo python3 -m http.server -b 127.0.0.1 80

httpclient.py の作成

touch httpclient.py
#!/usr/bin/env python3

import socket

# HTTP Client

# ソケットに指定したバイト数を書き込む
def send_msg(sock, msg):
  total_sent_len = 0
  total_msg_len = len(msg)
  while total_sent_len < total_msg_len:
    sent_len = sock.send(msg[total_sent_len:])
    if sent_len == 0:
      raise RuntimeError("socket connection broken")
    total_sent_len += sent_len

# ソケットから接続が終わるまでバイト列を読み込む
def recv_msg(sock, chunk_len=1024):
  while True:
    received_chunk = sock.recv(chunk_len)
    if len(received_chunk) == 0:
      break
    yield received_chunk

def main():
  # socket.AF_INET はネットワーク層に IPv4 を使うことの指定、SOCK_STREAM はトランスポート層に TCP を使うことの指定
  client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  client_socket.connect(('127.0.0.1', 80))
  request_text = 'GET / HTTP/1.0\r\n\r\n'
  request_bytes = request_text.encode('ASCII')
  send_msg(client_socket, request_bytes)
  received_bytes = b''.join(recv_msg(client_socket))
  received_text = received_bytes.decode('ASCII')
  print(received_text)
  
  client_socket.close()

if __name__ == '__main__':
  main()

echoservr.py の作成

touch echoserver.py
#!/usr/bin/env python3

import socket

# Echo Server

def send_msg(sock, msg):
  total_sent_len = 0
  total_msg_len = len(msg)
  while total_sent_len < total_msg_len:
    sent_len = sock.send(msg[total_sent_len:])
    if sent_len == 0:
      raise RuntimeError('socket connection broken')
    total_sent_len += sent_len

def recv_msg(sock, chunk_len=1024):
  while True:
    received_chunk = sock.recv(chunk_len)
    if len(received_chunk) == 0:
      break
    yield received_chunk

def main():
  server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
  # bind でどの IP アドレスを使ってクライアントからの接続を待つか指定している
  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}')
  
  # ソケットからバイト列を読み込む処理
  for received_msg in recv_msg(client_socket):
  	# 読みこんだ内容をそのまま書き込む(エコーバック)
    send_msg(client_socket, received_msg)
    print(f'echo: {received_msg}')

  client_socket.close()
  server_socket.close()

if __name__ == '__main__':
  main()

ターミナルを 2 台用意

1: エコーサーバを起動

python3 echoserver.py

2: nc でクライアントの作成

nc 127.0.0.1 54321
Hello, World!

ソケットとプロセス一覧の確認

sudo ss -tlnp

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

TCP/IP ではバイトオーダーをビッグエンディアンに統一している。 ネットワークにおけるビッグエンディアンを、ネットワークバイトオーダーと呼ぶ。 CPU のアーキテクチャに依存したコンピューター内部の表現方法はホストバイトオーダーと呼ぶ。

ホストバイトオーダー <-> ネットワークバイトオーダーに変換する必要がある。

足し算をする ADD プロトコルを実装する。

サーバー

touch addserver.py
#!/usr/bin/env python3

import socket
import struct

# ADD Protocol

def send_msg(sock, msg):
  total_sent_len = 0
  total_msg_len = len(msg)
  while total_sent_len < total_msg_len:
    sent_len = sock.send(msg[total_sent_len:])
    if sent_len == 0:
      raise RuntimeError('socket connection broken')
    total_sent_len += sent_len

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():
  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_size=8))
  print(f'received: {received_msg}')
  # ネットワークバイトオーダーからホストバイトオーダーへ変換している。struct.unpack() で '!ii' を指定し、受信したバイト列を 2 つの 4 バイト整数で解釈している。
  (operand1, operand2) = struct.unpack('!ii', received_msg)
  print(f'operand1: {operand1}', f'operand2: {operand2}')
  result = operand1 + operand2
  print(f'result: {result}')
  # ホストバイトオーダーからネットワークバイトオーダーへ変換している。struct.pack() で '!q' を指定し、結果を 64 ビットの整数として解釈できるように変換している。
  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()

クライアント

touch addclient.py
#!/usr/bin/env python3

import socket
import struct

def send_msg(sock, msg):
  total_sent_len = 0
  total_msg_len = len(msg)
  while total_sent_len < total_msg_len:
    sent_len = sock.send(msg[total_sent_len:])
    if sent_len == 0:
      raise RuntimeError('socket connection broken')
    total_sent_len += sent_len


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}')
    (added_value, ) = struct.unpack('!q', received_msg)
    print(f'result: {added_value}')
    client_socket.close()


if __name__ == '__main__':
  main()

起動

ターミナルを 2 台起動する

1: サーバー

python3 addserver.py

2: クライアント

python3 addclient.py

> operand1: 1000, operand2: 2000
> sent: b'\x00\x00\x03\xe8\x00\x00\x07\xd0'
> received: b'\x00\x00\x00\x00\x00\x00\x0b\xb8'
> result: 3000
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment