Python基礎20:ソケット通信(socket)
1.概要
低水準ネットワークインターフェースを扱うsocketモジュールを紹介をします。通信は素人のため学習内容も含めて記載しました。
なお2台のPCで通信するため私はPC以外にRasberry Piを1台準備しました。
1-1.ソケット/ソケット通信とは
インターネットでは通信プロトコル(通信のルール)としてTCP/IPを利用しており、その出入口がソケット (Socket)です。よってTCP/IP通信をソケット通信と呼ぶこともあります(出典)。
データ通信はISOとITUでネットワーク構造の基本的な設計方針が決められており、OSI(Open Systems Interconnection:開放型システム間相互接続)と呼ばれます。このOSIで用いられる通信のモデルがOSI参照モデルとなります。
TCP/IPはOSI参照モデルにおいて第4層トランスポート層の通信プロトコルになります。「TCP/IPを扱う≒低レイヤで情報をやり取り」することだと理解しています。
1-1-1.TCPとUDPの違い
OSI参照モデルで同じ階層にあるTCP(Transmission Control Protocol)とUDP(User Datagram Protocol)ですが下記のような違いがあります。
TCP:高い信頼性で安定した通信を重視する通信プロトコル
コネクション型通信プロトコル(事前のやり取りを確認した上で通信を開始させる)として、ウェブサイト閲覧、ファイル転送などのデータ完全性が求められるアプリケーションで活用
データは確実に目的地に届けられ、パケットロスが無い
コネクションを確立した後もデータが正しく届けられたことを確認する通信を頻繁に行う
UDP:高速通信を重視する通信プロトコル
コネクションレス型通信プロトコル(事前のやり取りなしで通信を開始させる)として動画配信、音声通話、モニター監視などのリアルタイム的な分野で活用
高速通信(軽量)だが、パケットロスが発生(信頼性が低い)
TCPのようにコネクションを確立せず、またデータが正しく届けられたことを確認するための通信も行わない
1-1-2.ポートとは
ポートとはネットワーク接続を開始・終了するための仮想的なポイントです。ポートはネットワークに接続されたすべての機器で標準化されており、各ポートには番号が割り当てられています。
https://www.cloudflare.com/ja-jp/learning/network-layer/what-is-a-computer-port/
1-2.ソケット通信のフロー
ソケット通信のフロー概要は下記の通りです。なおソケット通信はコンピュータネットワーク上でのプログラム間の通信を行う方法であり、受け手と渡し手でサーバー、クライアントという名前で呼ばれております。
クライアント:サービスを利用する側です。サーバーに接続しデータやサービスを要求します。
サーバー:サービスを提供する側です。待機してクライアントからの接続を受け付け、要求に応じてデータやサービスを提供します。
【コラム:ソケットとAPIの違い】
”通信の出入口”と聞くとAPIを思い浮かべるのですが、「ソケット(socket)はTCP/IPの機能を利用するときに使う標準的なAPI」とのことです。
1-3.socketの概要
Pythonのsocketモジュールによりネットワーク通信の実装が可能です。つまり、コンピューターネットワークを通じて2つ以上のコンピュータが通信できる方法を提供します。ソケットはエンドポイント(IPアドレスとポート番号)を用います。
socketでできることは以下の通りです。
データ転送: 画像ファイルなどをネットワークを通じて転送
リモートコントロール: 他のコンピュータやデバイスを遠隔操作が可能
チャットアプリケーション: リアルタイムでのチャットが可能
ウェブサーバー:サーバーとして利用
2.環境構築
socketはPython標準ライブラリのためinstallは不要です。
ソケット通信は2台のPCで情報のやり取りをするため、PCとは別でRasberry Piを用意しました。
3.PC/ネットワーク情報
PC情報やネットワーク情報を取得する関数を紹介します。
3-1.PC情報
socketモジュールはローカルコンピュータ(実行しているコンピュータ)のホスト名やFQDN(完全修飾ドメイン名)などのPC情報を取得できます。
socket.gethostbyname():Python処理しているPCのホスト名
socket.getfqdn([name]):完全修飾ドメイン名(FQDN)を出力
[IN]
import socket
#PC情報
hostname = socket.gethostname() #ホスト名(コンピュータ名)を取得
fqdn = socket.getfqdn() #FQDN(Fully Qualified Domain Name)を取得
print(hostname)
print(fqdn)
[OUT]
DESKTOP-XX62XX3
DESKTOP-XX62XX3.katch.ne.jp
3-2.ネットワーク情報
socketモジュールはホスト名やIPアドレスに関する情報も取得可能です。
socket.gethostbyname(hostname):ホスト名からIPアドレス取得
ホスト名を '100.50.200.5' のようなIPv4形式のアドレスに変換
IPv6のアドレス解決はサポートしていない。IPv4/ v6のデュアルスタックをサポートする場合は getaddrinfo() を使用
socket.gethostbyname_ex(hostname):ホスト名からローカルネットワーク上の全てのIPアドレスとホスト名を取得
出力形式は(hostname, aliaslist, ipaddrlist)
socket.gethostbyaddr(ip_address):IPアドレスからホスト名等を出力
出力形式は(hostname, aliaslist, ipaddrlist)
[IN]
ip_address = socket.gethostbyname(hostname) #IPアドレスを取得
ip_address_ex = socket.gethostbyname_ex(hostname) #IPアドレスを取得
print(ip_address)
print(ip_address_ex)
[OUT]
192.xxx.xx.xxx
('DESKTOP-ED62KF3', [], ['192.xxx.xx.xxx'])
[OUT]
142.250.207.4
('nrt13s71-in-f4.1e100.net', [], ['142.251.222.4'])
4.ソケットの基礎
ソケット通信ではサーバーとクライアントでデータのやり取りを行います。本章ではサーバー/クライアントで共通の関数/APIを紹介し、次章ではそれぞれの関数/APIを説明していきます。
4-1.ソケットの作成:socket.socket()
通常でのsocketオブジェクトの作成は”socket.socket()”を使用します。オブジェクト作成時の引数は下記の通りです。
アドレスファミリー(family):通信プロトコル(通信のルール)をひとまとめにしたもの
ソケットタイプ(type):通信の特性や動作モードを定義
プロトコル番号(proto):通常は省略可でありデフォルトは0を指定
[API]
class socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
参考としてsocketオブジェクト作成した結果を表示しました。
[IN]
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(tcp_socket)
print(type(tcp_socket))
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print(udp_socket)
print(type(udp_socket))
[OUT]
<socket.socket fd=1480, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
<class 'socket.socket'>
<socket.socket fd=1552, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0>
<class 'socket.socket'>
他にも下記関数でsocketオブジェクトの作成が可能です。
【その他のsocketオブジェクト作成関数】
socket.socketpair()
socket.create_connection()
socket.create_server()
socket.has_dualstack_ipv6()
socket.fromfd()
socket.fromshare()
socket.SocketType()
4-1-1.アドレスファミリー
アドレスファミリ(address family)とは、ネットワークアドレスの種類を表すものです。socketモジュールでは下記を選択できます(出力は参考用)。
[IN]
print(socket.AF_INET)
print(type(socket.AF_INET))
[OUT]
AddressFamily.AF_INET
<enum 'AddressFamily'>
【アドレスファミリー一覧】
socket.AF_INET:IPv4
socket.AF_INET6:IPv6
socket.AF_UNIX:-
socket.AF_CAN:-
socket.AF_PACKET:-
socket.AF_RDS:-
4-1-2.ソケットタイプ(TCP/UDP)
ソケットタイプとは、プロセス(ソケット)の通信方式を指定するものです。socketモジュールでは下記を選択できます(出力は参考用)。
[IN]
print(socket.SOCK_STREAM)
print(type(socket.SOCK_STREAM))
[OUT]
SocketKind.SOCK_STREAM
<enum 'SocketKind'>
socket.SOCK_STREAM:TCPを使った通信
信頼性の高いデータ転送を提供
メール送信やウェブブラウジングなどで利用
socket.SOCK_DGRAM:UDPを使った通信
軽量ですが、データの完全な到達を保証しない通信を提供
ストリーミングなど、リアルタイム性が求められる場面で利用
socket.SOCK_RAW:ICMPへのアクセスを提供
4-2.ソケットをクローズ(接続の切断):close()
TCPではサーバー/クライアントとものデータ送受信したら、リソース管理やセキュリティなどの理由でソケットをクローズします。クローズは"close()"メソッドを使用します。
ただし「実用上はwithステートメントを使用し、ブロックを抜けるときにソケットは自動的にクローズされる」ようにします。
[IN]
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(tcp_socket)
tcp_socket.close()
print(tcp_socket)
[OUT]
<socket.socket fd=1564, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
<socket.socket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
4-3.ソケット動作/タイムアウト
ソケットの動作モード(ブロッキング/ノンブロッキング)やタイムアウトを制御を設定が可能です。
ノンブロッキング:操作が即座に終了し、結果がすぐに得られない場合、エラーを返します。
ブロッキング:操作が終了するまでプログラムが停止し、結果を待ちます。
各関数は以下の通りです。
setblocking(flag): ノンブロッキングモードの設定をします。
flag=False:ノンブロッキングモードでありsettimeout(0.0)と同じ
flag=True:ブロッキングモードでありsettimeout(None)と同じ
settimeout(value):ブロッキング操作が終了するまでの最大時間を指定
valueは秒単位
value=0.0はノンブロッキングモード
value=None(デフォルト)はブロッキングモード(無期限にブロック)
gettimeout(): ソケットのタイムアウト値を取得します。
タイムアウト値が設定されている場合には浮動小数点型で秒数が、設定されていなければ None が返ります。
[IN]
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
timeout = tcp_socket.gettimeout() #タイムアウトを取得
print(f'初期値:{timeout}')
tcp_socket.settimeout(10) #タイムアウトを設定
print(f'設定後:{tcp_socket.gettimeout()}')
tcp_socket.setblocking(False) #ノンブロッキングモードに設定
print(f'ノンブロッキングモード:{tcp_socket.gettimeout()}')
tcp_socket.setblocking(True) #ブロッキングモードに設定
print(f'ブロッキングモード:{tcp_socket.gettimeout()}')
[OUT]
初期値:None
設定後:10.0
ノンブロッキングモード:0.0
ブロッキングモード:None
5.サーバーサイド
サーバーはサービスを提供する側です。クライアントからの接続要求を待ち受け、接続が確立されたらデータの送受信を行います。
5-1.サーバーサイドの通信フロー
サーバーサイドの通信フローは下記の通りです。
ソケットの作成:通信のためのエンドポイントとしてソケットを作成
バインディング:特定のIPアドレスとポート番号にソケットを関連付け
リスニング:クライアントからの接続を待機するモード
接続の受付:クライアントからの接続要求が来ると、その接続を受け入る
データの送受信:接続後にサーバーはクライアントとデータの送受信を行う
ソケットのクローズ:通信が完了後にソケットを閉じる
5-2.サーバーでのAPI一覧
サーバーサイドでよく使用するAPIを紹介します。一部は「4章 ソケットの基礎」で紹介したものを含みます。
socket.socket():ソケットオブジェクトを生成
bind((host, port)):ソケットを指定したホストとポートにバインド
ポート:アプリケーションがネットワーク上で識別されるための番号
バインディング:アドレスとポートをソケットに関連付けること
listen(backlog):ソケットが接続要求を待ち受けるようにする
backlog:未処理の接続のキューの最大サイズを指定
accept():クライアントからの接続要求を受け入れる。
戻り値:(新しいソケットオブジェクト、アドレス)のペアを返す
recv()はソケット上でデータを受信するための関数のため用途が異なる
recv():データの受信
sendall():データの送信
使用例は以下の通りです。
5-3.バインディング:bind()
"socket.bind(address)"でソケットを address にbindします。bind済みのソケットを再バインドする事はできません(OSError発生)。
[IN]
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 12345))
なおサーバーサイドのアドレスとして選択するHostには下記のようなものがあります。
0.0.0.0:すべての利用可能なネットワークインターフェース上で待ち受けることを示します.
外部からの接続もローカルからの接続も受け入れることができます
127.0.0.1(localhost):ループバックアドレスの一つ
自分自身を表す特別なIPアドレス
ネットワークを介さずにコンピュータ内での通信を可能にします
5-4.接続の待機:listen()
"listen()"でサーバーはクライアントからの接続の待機モードに入ります。
[IN]
server_socket.listen()
なお”listen()”実行前に”server_socket.accept()”をするとエラーが出ます。
[IN]
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 12345))
server_socket.accept()
[OUT]
OSError: [WinError 10022] 無効な引数が提供されました。
5-5.接続の受入れ:accept()
”accept()”を実行すると、クライアントからの接続要求が来るとサーバーはその接続を受け入れます。タイムアウト/ブロッキングモードを設定しないと永続的に待機となります。
[IN]
client_socket, client_address = server_socket.accept()
参考までにタイムアウトを設定し、その時間内に受診がないとエラーが発生します。
[IN]
server_socket.settimeout(2.0) #タイムアウトを設定
print(f'Timeout:{server_socket.gettimeout()} sec')
client_socket, client_address = server_socket.accept()
[OUT]
Timeout:2.0 sec
timeout: timed out
5-6.データの送受信:recv()/sendall()
”socket.recv(bufsize[, flags])”では、ソケットからデータを受信し、結果を bytes オブジェクトで返します。”socket.sendall(bytes[, flags])”では、ソケットにデータを送信します。
これらのメソッドにより、接続が確立されるとサーバーはクライアントとデータの送受信を行います。
[IN]
data = client_socket.recv(1024)
client_socket.sendall(b"Hello, client!")
通信が完了したら通常はソケットを閉じます(実際はwith構文を使用)。
[IN]
client_socket.close()
6.クライアントサイド
クライアントはサービスを利用する側です。サーバーに接続し、データやサービスを要求します。
6-1.クライアントサイドの通信フロー
クライアントサイドの通信フローは下記の通りです。
ソケットの作成:通信のためのエンドポイントとしてソケットを作成
サーバーへの接続:サーバーのIPアドレスとポート番号を指定して接続
データの送受信:接続後、クライアントはサーバーとデータの送受信を行う
ソケットのクローズ:通信が完了後にソケットを閉じる
6-2.クライアントでのAPI一覧
クライアントサイドでよく使用するAPIを紹介します。一部は「4章 ソケットの基礎」で紹介したものを含みます。
socket.socket(): ソケットオブジェクトを生成
connect((host, port)): サーバーの指定したホストとポートに接続
send(bytes): データをバイト列として送信
クライアントやサーバーがデータを相手に送信するために使用
recv(bufsize): クライアントやサーバーが相手からのデータを受信時に使用
bufsize:受信するバイト数を指定
クライアント->サーバー:リクエストデータを受信する場面で使用
サーバー->クライアント:応答データを受信する場面で使用
sendall(bytes[, flags]):指定されたバイト列が全て送信されるまで繰り返し送信する
データ送信はsend()と同じだが条件に応じてこちらを設定
使用例は以下の通りです。
6-3.サーバーへの接続:connect()
”connect()”メソッドでクライアントはサーバーのIPアドレスとポート番号を指定して接続を開始します。
※'server_address'は適切なIPアドレスに変更
[IN]
client_socket.connect(('server_address', 12345))
サーバー側が受信できる状態でなければエラーが発生します。
6-4.データ送受信:recv()/sendall()
サーバーサイドでの説明と同様のメソッドを使用します。接続が確立されると、クライアントはサーバーとデータの送受信を行います。
[IN]
client_socket.sendall(b"Hello, server!")
response = client_socket.recv(1024)
通信が完了したら通常はソケットを閉じます(実際はwith構文を使用)。
[IN]
client_socket.close()
7.実践1:ソケット通信の動作確認
実際にソケット通信を行うことでどのような動作になるかを確認しました。
サーバー:Rasberry Pi
クライアント:自分のPC
7-1.Rasberry Piの準備
細かいRasberry Piの準備は下記記事をご確認ください。
準備が出来たらRasberry Piをサーバーとして接続するためにIPアドレスを確認します。確認方法は”ip a”コマンドで実行可能です。
[IN]
ip a
7-2.サーバーサイドの立ち上げ
Raspberry Pi(サーバー)上で下記スクリプトを実行し、クライアントからの接続を待機します。
このスクリプトはクライアントから送られてきたデータ"data = conn.recv(1024)"をそのままの形”conn.sendall(data)”で返します。
[server.py]
import socket
# ソケット設定
HOST = '0.0.0.0' # すべてのアドレスからの接続を許可
PORT = 65432 #適当に設定
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT)) #バインディング:ソケットにIPアドレスとポート番号を設定
s.listen() #接続要求の準備
conn, addr = s.accept() #接続要求を受け入れ※Timeoutはなし
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024) #データ受信※1024はバッファサイズ
if not data:
break
conn.sendall(data) # 受信データをそのまま返す(エコーサーバー)
[Terminal]
python3 server.py
7-3.クライアントから要求
クライアント側からサーバーへ要求を出します。スクリプトとして実行してもよいですが私はJupyter上から実行しました。
[IN]
import socket
# サーバーの設定
HOST = '192.XXX.1X.XXX' # Raspberry PiのIPアドレス
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b'Hello, Raspberry Pi!')
data = s.recv(1024)
print('Received', repr(data))
[OUT]
Received b'Hello, Raspberry Pi!'
結果から下記が確認されます。
server.pyで実行したconn, addr = s.accept()のアドレス情報(addr)がRasberry Piのターミナルに出力された。
最初の値はPC側のIPアドレス
後の値はその時の動的に割り当てられたポート番号
ポート番号は動的な値のため実行毎に変わる
クライアント側で連続に実行すると”ConnectionRefusedError”発生
サーバーサイドが実行後に切断するため
クライアントはサーバーからデータが来るまで待機している
”sendall()”後に”recv()”の部分でサーバーからの応答を待っている
データ取得方法がサーバーとクライアントで異なる。
サーバー:conn.recv()->通信から取得
クライアント:s.recv()->ソケットから取得
sendall()のデータはバイト形式で得られる(repr()で文字列に変換)
8.実践2:チャットシステム(交互のみ)
サーバーとクライアント間でチャットできるシステムを作成してみます。基本的なシステムは前章と同じであり、チャット入力できるように"input()"関数を使用しました。
8-1.サーバーサイド
サーバーサイドは下記の通りです。
クライアントサイドからデータを取得したらinput()でメッセージ入力可
クライアントサイドのデータがNone(またはsocketをClose)すると接続(conn)が切断されるようになっている。
[server_chat.py]
import socket
HOST = '0.0.0.0'
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print("サーバーが起動しました。クライアントの接続を待っています。")
conn, addr = s.accept()
with conn:
print(f'{addr} から接続がありました。')
while True:
data = conn.recv(1024)
# クライアントからの接続が切れるとdataには空の文字列が返される
if not data:
break
print("クライアント:", data.decode('utf-8'))
msg = input("サーバー: ")
conn.sendall(msg.encode('utf-8'))
[Terminal]
python3 server_chat.py
8-2.クライアントサイド
クライアントサイドは送信(sendall())するデータをinput()で入力できるようにしただけとなります。
[client_chat.ipynb]
import socket
HOST = '192.XXX.1X.XXX' # Raspberry PiのIPアドレス
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
while True:
msg = input("クライアント: ")
s.sendall(msg.encode('utf-8'))
data = s.recv(1024)
print("サーバー:", data.decode('utf-8'))
8-3.動作確認
動作確認しました。
①クライアントサイドからメッセージ:サーバーが受信+チャット入力可能
[Client]
test_1st コメント
②サーバー側からメッセージ:クライアントが受信+チャット可能
[Server]
1stコメントうけてサーバーから返信
後は交互に実行可能です。現在は交互にしかできませんが、非同期通信やマルチスレッディングの技術を用いれば自由にチャット可能となるはずです。
9.実践3:非同期チャットシステム
前章は交互のみでしたが、今度はどちら側からでも連絡できるようにしてみます。非同期通信としてthreadingを使用しました。
Threadingの説明をすると長くなるため、今回は動作検証のみとなります。
9-1.サーバーサイド
サーバーサイドは下記の通りです。
※エラー確認用で送受信用関数はtry構文にしています。
[server_conn.py]
import socket
import threading
def receive_message(conn):
while True:
try:
data = conn.recv(1024)
if not data:
print("クライアントが接続を終了しました。")
break
print(f"クライアント: {data.decode()}")
except ConnectionResetError:
print("接続がクライアントによってリセットされました。")
break
except Exception as e:
print(f"その他のエラーが発生しました: {e}")
break
def send_message(conn):
while True:
try:
msg = input("サーバー: ")
if not conn: # 追加: クライアントの接続が存在しない場合
print("クライアントとの接続がありません。")
break
conn.sendall(msg.encode())
except BrokenPipeError: # この例外をキャッチ
print("クライアントが接続を終了しました。メッセージの送信ができません。")
break
except Exception as e:
print(f"メッセージ送信時にエラーが発生しました: {e}")
break
HOST = '0.0.0.0'
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT)) #バインディング
s.listen() #接続待ち
conn, addr = s.accept() #接続されたらコネクションとアドレスを取得
# 受信用と送信用のスレッドを起動
threading.Thread(target=receive_message, args=(conn,)).start()
threading.Thread(target=send_message, args=(conn,)).start()
[Terminal]
python3 server_conn.py
9-2.クライアントサイド
クライアントサイドは下記の通りです。こちらもスクリプト形式にしてターミナルから起動しました。
[client_conn.py]
import socket
import threading
def receive_message(s):
while True:
try:
data = s.recv(1024)
if not data:
print("サーバーが接続を終了しました。")
break
print(f"サーバー: {data.decode()}")
except ConnectionResetError:
print("接続がサーバーによってリセットされました。")
break
except Exception as e:
print(f"その他のエラーが発生しました: {e}")
break
HOST = '192.XXX.1X.XXX' # Raspberry PiのIPアドレス
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
# 受信用のスレッドを起動
threading.Thread(target=receive_message, args=(s,)).start()
while True:
try:
msg = input("クライアント: ")
s.sendall(msg.encode())
except Exception as e:
print(f"メッセージ送信時にエラーが発生しました: {e}")
break
[Terminal]
python3 client_conn.py
起動がうまくいけば”クライアント:”と表示され文字が入力できます。
9-3.動作確認
後は自由にチャットできるか確認してみました。処理順は"C, C, S, S, C, S, S"としてみました。
チャットの切り替え時の見た目は悪いですが、とりあえず自由に連絡できることは確認できました。
参考記事
あとがき
とりあえず先出。
Rasberry PiをNAS(ネットワークHDD)として使ったり、クラウド環境で自動売買システム用に使ったりしたいので、それはやってみたに落とし込もうかな・・・・