現在 TCP/UDP でデバイスと接続する IoT モバイルアプリのプロジェクトをしています。
コロナ対策で在宅推奨にはなってるんですが借用している機材を持ち帰るわけにも行かないので、動作確認用のシミュレータを作りました。その覚書です。
Environment
Server
- OSX 10.14.5
- python 3.7
Client
- Android 10.0
- Kotlin 1.3.50
クライアント (モバイルアプリ) が UDP ブロードキャストでサーチした後、見つかったデバイスに対して TCP 接続してデータが取得できるまでを目指します。
- ざっくり図
今回はシミュレータ(サーバ)作成のためクライアント側は割愛します。。。
Python の Socket 通信
Python で TCP/UDP 通信をする時は socket ライブラリを使います。
ソケットファミリー
socket で通信するときは以下の組み合わせで通信方法が決まります。
- アドレスファミリー
アドレスファミリ | 方式 |
---|---|
AF_INET | IPv4 |
AF_INET6 | IPv6 |
- ソケットタイプ
ソケットタイプ | 方式 |
---|---|
SOCK_STREAM | TCP |
SOCK_DGRAM | UDP |
AF_INET + SOCK_STREAM
にすると IPv4 + TCP 通信
という事になります。
今回は IPv4 + TCP / IPv4 + UDP を使います。
UDP
まずは UDP サーバを作成します。
UDP ブロードキャストで飛んできたパケットに対してレスポンスを返すところまで。
import socket
# ブロードキャストするときは 255.255.255.255 ではなく空文字
HOST = ''
PORT = 60000
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind((HOST, PORT))
while True:
try:
msg, addr = s.recvfrom(1024)
s.sendto('Hello World!'.encode('utf-8'), addr)
except KeyboardInterrupt:
print('Interrupted.')
break
except socket.error:
print('Has error occurred.')
pass
クライアントに対して Hello World! がレスポンスされていれば成功です。
TCP
TCP サーバも作成します。
UDP ができたら通信方法を変えるだけです。
import socket
PORT = 60001
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# ホストに割当たっている IP を指定する
host = socket.gethostname()
s.bind((socket.gethostbyname(host), PORT))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.listen(1)
while True:
try:
client, addr = s.accept()
# バッファサイズ 100 byte
data = client.recv(100)
print(f'data: {data}, addr: {addr}')
client.sendall("Hello World!".encode('utf-8'))
except KeyboardInterrupt:
print('Interrupted.')
break
except socket.error:
print('Has error occurred.')
pass
同じく Hellor World! がレスポンスされていれば成功です。
XML からデータを返す
やりたい事はクライアントからのリクエストに対して対応したレスポンスを返す TCP/UDP サーバの作成なので、xml から取得したパラメータを返す様にしたいと思います。
Python で xml を扱うには xml.etree.ElementTree
を使います。
xml の詳細な使い方はまた別途・・・・
<?xml version="1.0"?>
<data>
<res>Hello World!</res>
</data>
import socket
import xml.etree.ElementTree as ET
PORT = 60001
RES_XML = 'xml/res.xml'
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# ホストに割当たっている IP を指定する
host = socket.gethostname()
s.bind((socket.gethostbyname(host), PORT))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.listen(1)
while True:
try:
client, addr = s.accept()
# バッファサイズ 100 byte
data = client.recv(100)
print(f'data: {data}, addr: {addr}')
# XML から抽出したデータでレスポンスのバイナリを作成
b = bytearray()
root = ET.parse(RES_XML).getroot()
res = root.find('res').text.encode('utf-8')
b.extend(res)
client.sendall(b)
except KeyboardInterrupt:
print('Interrupted.')
break
except socket.error:
print('Has error occurred.')
pass
これで xml ファイルを書き換えたら即時反映されてレスポンスが変わる様になりました。
udp_server, tcp_server を同時に立ち上げたら晴れて在宅ワーカーです😎
Docker で配布
自分だけ在宅すると批判が殺到するのでプロジェクトメンバにも同じ環境を提供できる様に docker-compose で立ち上げれる様にしました。
Environment
- Docker
Client: Docker Engine - Community
Version: 19.03.8
API version: 1.40
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:21:11 2020
OS/Arch: darwin/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.8
API version: 1.40 (minimum version 1.12)
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:29:16 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.2.13
GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
docker-init:
Version: 0.18.0
GitCommit: fec3683
- docker-compose
docker-compose version 1.25.4, build 8d51620a
docker-py version: 4.1.0
CPython version: 3.7.5
OpenSSL version: OpenSSL 1.1.1d 10 Sep 2019
docker-compose で立ち上げる
階層はこんな感じで作成していきます。
socket
├── docker-compose.yml
├── tcp/
│ ├── Dockerfile
│ ├── tcp_server.py
│ └── xml/
│ └── res.xml
└── udp/
├── Dockerfile
└── udp_server.py
まずは Dockerfile を作ります。
FROM python:3.7.4
ARG project_dir=/socket/
WORKDIR $project_dir
続いて docker-compose.yml を作ります。
version: '3.3'
services:
udp:
build: ./udp/.
volumes:
- ./udp:/socket
ports:
- 60000:60000/udp
restart: 'no'
command: python udp_server.py
tcp:
build: ./tcp/.
volumes:
- ./tcp:/socket
ports:
- 60001:60001
restart: 'no'
command: python tcp_server.py
ホストの port をコンテナに割り当てる事でサーバがクライアントと通信できるようになります。
これで docker-compose up
で TCP/UDP サーバが立ち上がりクライアントとの疎通ができました。
みんなハッピー👆👆😄👆👆
Docker クソザコナメクジ過ぎてコピペしかできない。。。
もっとこうするべきだ等々あれば教えて下さい。。。