TPCソケットの切断復旧メモ

2024/MAR/17

更新履歴
日付 変更内容
2024/MAR/17 新規作成

目次


概要

ちょいとお仕事でTCPソケットを使うので、基本動作の確認をば。

2台のUbunut PCを1つをwifiアクセスポイントにして、もう1つでそのwifiに接続して、TCPソケットでデータをやりとりします。

ただし、PCお距離が遠ざかったり近づいたりしてwifiの接続が切断、復旧を繰り返します。

アクセスポイント側のPCで、TCPソケットのポートを開いてサーバとして待ちます。

もう一台のPCはクライアントとして、サーバに接続しにいきます。


プログラム

test_sock.py
#!/usr/bin/env python3

import sys
import time
import select
import socket

import empty
import thr

def close( sock ):
	sock.close()

def conn( port, host='localhost' ):
	cs = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
	try:
		cs.connect( ( host, port ) )
	except:
		return None

	return cs

def send( sock, s ):
	try:
		b = s.encode()
		sock.sendall( b )
	except:
		return False

	return True

def recv( sock ):
	while True:
		r = select.select( [ sock ], [], [], 1.0  )
		if r[ 0 ]:
			break

	s = ''
	try:
		bufmax = 100 * 1024
		b = sock.recv( bufmax )
		s = b.decode()
	except:
		return None

	return s

def recv_thr_new( sock, cb ):
	e = empty.new()
	e.quited = False

	def f():
		s = recv( sock )
		if not s:
			quit()
			return

		cb( s )

	th = thr.loop_new( f )

	def quit():
		if e.quited:
			return

		e.quited = True
		th.quit_ev.set()
		close( sock )
		cb( None )

	th.start()

	def stop():
		quit()
		th.stop()

	return empty.add( e, locals() )

def srv_sock_new( port ):
	ss = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
	ss.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
	ss.bind( ( '', port ) )
	ss.listen( 5 )
	return ss

def accept( sock ):
	try:
		( cs, adr ) = sock.accept()
	except:
		return None

	return cs

def acc_new( cs, cb_accept, accs, quits ):
	acc = empty.new()
	accs.append( acc )

	th_acc = thr.th_new( cb_accept, ( acc, ) )

	def quit():
		if not th_acc.quit_ev.is_set():
			th_acc.quit_ev.set()

			if acc in accs:
				accs.remove( acc )
				quits.append( acc )

	def stop():
		quit()
		th_acc.stop()

		if acc in quits:
			quits.remove( acc )

	empty.add( acc, locals() )

	th_acc.start()

	return acc

def srv_new( port, cb_accept ):
	srv = empty.new()

	ss = srv_sock_new( port )

	accs = []
	quits = []

	def gc():
		while quits:
			acc = quits.pop( 0 )
			acc.stop()

	def f_loop():
		cs = accept( ss )
		if not cs:
			return

		acc_new( cs, cb_accept, accs, quits )
		gc()

	th = thr.loop_new( f_loop )
	th.start()

	def stop():
		th.quit_ev.set()
		close( ss )
		th.stop()

		while accs:
			acc = accs.pop( 0 )
			acc.stop()
		gc()

	return empty.add( srv, locals() )

def cb_accept( acc ):

	def cb( s ):
		print( "srv recv {}".format( s ) )

		print( ">srv send {}".format( s ) )
		r = send( acc.cs, s )
		print( "<srv send r={}".format( r ) )

	recv_thr = recv_thr_new( acc.cs, cb )

	recv_thr.th.quit_ev.wait()

def run():
	argv = sys.argv[ 1 : ]
	port = 1234

	quit_ev = thr.event_new()

	if not argv:  # srv
		srv_new( port, cb_accept )
		quit_ev.wait()
		return

	# cli
	host = argv[ 0 ]

	cs = conn( port, host )
	print( "cli conn {}".format( cs ) )
	if not cs:
		return

	ev = thr.event_new()

	def cb( s ):
		print( "cli recv {}".format( s ) )
		ev.set()

	recv_th = recv_thr_new( cs, cb )

	i = 0
	while True:
		ev.clear()
		s = str( i )

		print( "cli> send {}".format( s ) )
		r = send( cs, s )
		print( "cli send r={}".format( r ) )

		ev.wait()
		i += 1

		time.sleep( 1 )

if __name__ == "__main__":
	run()
# EOF

構成

pythonのユーティリティ・プログラム 2020冬

をつかってます。

test_sock.py の前半の srv_new() まではライブラリ的な部品です。

close( sock ) 単に sock.close() です。
以前に sock.shutdown() を実行していた名残りで、ラッパーのまま残してます。
conn( port, host='localhost' ) クライアントのconnect用です。
send( sock, s ) 送信っす。
recv( sock ) 受信っす。
recv_thr_new( sock, cb ) 受信用のスレッドを生成します。
内部で recv( sock) を呼び出して受信すると、コールバック関数 cb( s ) を呼び出します。
srv_sock_new( port ) サーバソケットを生成します。
accept( sock ) サーバでの accept 用です。
acc_new( cs, cb_accept, accs, quits ) サーバでアクセプトしたときにスレッドを生成して、コールバック関数 cb_accept を呼び出す処理です。
srv_new( port, cb_accept ) サーバを生成します。
クライアントが接続しにくると、acc_new() を使用してスレッドを生成して、コールバック関数 cb_accept を呼び出します。

test_sock.py の後半がテスト動作用の処理です。

cb_accept( acc ) サーバ側でクライアントが接続しにきて accept すると、スレッドが生成されてこの関数が実行されます。
recv_thr_new() で受信用のスレッドを走らせておいて、クライアントからデータを受信すると、cb( s ) が呼び出されます。
cb( s ) では、データを表示したあと、同じデータをクライアントに送り返しています。
run() 引数なしで実行すると、サーバとして動作します。
引数で、ホスト名(やIPアドレス)を指定すると、クライアントとして動作します。
指定したホスト名(やIPアドレス)のサーバに接続にしきます。
文字列をサーバにデータ送信して、サーバから送り返されてくるデータを待ちます。


動作確認

動作環境

自宅のwifi環境に Mac と ThinkPad を接続。

サーバ側 MacBook Air OS X (10.11.6)
クライアント側 ThinkPad Ubuntu 18.04

サーバ起動

$ ./test_sock.py

クライアント起動

 ./test_sock.py 192.168.11.10
cli conn <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.11.11', 37276), raddr=('192.168.11.10', 1234)>
cli> send 0
cli send r=True
cli recv 0
cli> send 1
cli send r=True
cli recv 1
cli> send 2
cli send r=True
cli recv 2
cli> send 3
cli send r=True
cli recv 3
cli> send 4
cli send r=True
cli recv 4
cli> send 5
cli send r=True
cli recv 5
cli> send 6
cli send r=True
cli recv 6
cli> send 7
cli send r=True
cli recv 7
cli> send 8
cli send r=True
cli recv 8
cli> send 9
cli send r=True

サーバ側の反応

srv recv 0
>srv send 0
<srv send r=True
srv recv 1
>srv send 1
<srv send r=True
srv recv 2
>srv send 2
<srv send r=True
srv recv 3
>srv send 3
<srv send r=True
srv recv 4
>srv send 4
<srv send r=True
srv recv 5
>srv send 5
<srv send r=True
srv recv 6
>srv send 6
<srv send r=True
srv recv 7
>srv send 7
<srv send r=True
srv recv 8
>srv send 8
<srv send r=True

クライアント側でwifiをOFFに

上記の状態のまま、双方の画面の更新は停止したたままに。

クライアント側は データ "9" の send() が成功 (True) で返ってきて、 受信スレッドが recv() でブロックしたまま。

	:
cli> send 9
cli send r=True

サーバ側はデータ "8" の send(0 が 成功 (True ) で返ってきて、 受信スレッドが recv() でブロックしたまま。

	:
>srv send 8
<srv send r=True

よって、クライアント側が send() に成功した "9" は、まだ届いてない状態。

(クライアント側の send() では、サーバにデータが届いてなくても、 とりあえず成功で返るものなんだな...)

このまま 3分くらい待機。

クライアント側でwifiをONに戻す

サーバ側

srv recv 9
>srv send 9
<srv send r=True
srv recv 10
>srv send 10
<srv send r=True
srv recv 11
>srv send 11
<srv send r=True
srv recv 12
>srv send 12
<srv send r=True
srv recv 13
>srv send 13
<srv send r=True
srv recv 14
>srv send 14
<srv send r=True

クライアント側が最後にsend()成功していたはずのデータ "9" を受信。

何事もなかったように、続きのデータも順調に受信して、送り返している。

クライアント側

cli recv 9
cli> send 10
cli send r=True
cli recv 10
cli> send 11
cli send r=True
cli recv 11
cli> send 12
cli send r=True
cli recv 12
cli> send 13
cli send r=True
cli recv 13
cli> send 14

再開してサーバが送り返した "9" を受信。

こちらも何事もなかったように、後続のデータを送信。


考察

このように、常にハンドシェイクしたやりとりだと、まず問題なさそう。

TCP接続は信頼性のある、再送アリのプロトコルなので、まぁ当たり前。

お仕事で想定しているのは、サーバからクライアントに向けて、一方通的にデータが流れるタイプ。

この場合、wifi切れた状態でサーバからの複数の send() はどうなるか?

wifi復帰したときは?


サーバからクライアントへの一方通行タイプ

プログラム

test_sock2.py
#!/usr/bin/env python3

import sys
import time
import select
import socket

import empty
import thr

def close( sock ):
	sock.close()

def conn( port, host='localhost' ):
	cs = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
	try:
		cs.connect( ( host, port ) )
	except:
		return None

	return cs

def send( sock, s ):
	try:
		b = s.encode()
		sock.sendall( b )
	except:
		return False

	return True

def recv( sock ):
	while True:
		r = select.select( [ sock ], [], [], 1.0  )
		if r[ 0 ]:
			break

	s = ''
	try:
		bufmax = 100 * 1024
		b = sock.recv( bufmax )
		s = b.decode()
	except:
		return None

	return s

def recv_thr_new( sock, cb ):
	e = empty.new()
	e.quited = False

	def f():
		s = recv( sock )
		if not s:
			quit()
			return

		cb( s )

	th = thr.loop_new( f )

	def quit():
		if e.quited:
			return

		e.quited = True
		th.quit_ev.set()
		close( sock )
		cb( None )

	th.start()

	def stop():
		quit()
		th.stop()

	return empty.add( e, locals() )

def srv_sock_new( port ):
	ss = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
	ss.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
	ss.bind( ( '', port ) )
	ss.listen( 5 )
	return ss

def accept( sock ):
	try:
		( cs, adr ) = sock.accept()
	except:
		return None

	return cs

def acc_new( cs, cb_accept, accs, quits ):
	acc = empty.new()
	accs.append( acc )

	th_acc = thr.th_new( cb_accept, ( acc, ) )

	def quit():
		if not th_acc.quit_ev.is_set():
			th_acc.quit_ev.set()

			if acc in accs:
				accs.remove( acc )
				quits.append( acc )

	def stop():
		quit()
		th_acc.stop()

		if acc in quits:
			quits.remove( acc )

	empty.add( acc, locals() )

	th_acc.start()

	return acc

def srv_new( port, cb_accept ):
	srv = empty.new()

	ss = srv_sock_new( port )

	accs = []
	quits = []

	def gc():
		while quits:
			acc = quits.pop( 0 )
			acc.stop()

	def f_loop():
		cs = accept( ss )
		if not cs:
			return

		acc_new( cs, cb_accept, accs, quits )
		gc()

	th = thr.loop_new( f_loop )
	th.start()

	def stop():
		th.quit_ev.set()
		close( ss )
		th.stop()

		while accs:
			acc = accs.pop( 0 )
			acc.stop()
		gc()

	return empty.add( srv, locals() )

def cb_accept( acc ):

	i = 0
	while True:
		s = str( i )

		print( "srv> send {}".format( s ) )
		r = send( acc.cs, s )
		print( "srv send r={}".format( r ) )
		i += 1

		time.sleep( 1 )

def run():
	argv = sys.argv[ 1 : ]
	port = 1234

	quit_ev = thr.event_new()

	if not argv:  # srv
		srv_new( port, cb_accept )
		quit_ev.wait()
		return

	# cli
	host = argv[ 0 ]

	cs = conn( port, host )
	print( "cli conn {}".format( cs ) )
	if not cs:
		return

	def cb( s ):
		print( "cli recv {}".format( s ) )

	recv_th = recv_thr_new( cs, cb )

	quit_ev.wait()


if __name__ == "__main__":
	run()
# EOF

クライアントからサーバへ接続しにいくと、 サーバからデータを送り続けるタイプに変更しました。

クライアント側では受信したデータを表示するだけです。


一方通行タイプの動作確認

サーバの起動

$ ./test_sock2.py

クライアントの起動

$ ./test_sock2.py 192.168.11.10
cli conn <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.11.11', 49192), raddr=('192.168.11.10', 1234)>
cli recv 0
cli recv 1
cli recv 2
cli recv 3
cli recv 4
cli recv 5
cli recv 6
cli recv 7
cli recv 8

サーバからのデータを順調に受信して表示してます。

サーバ側

srv> send 0
srv send r=True
srv> send 1
srv send r=True
srv> send 2
srv send r=True
srv> send 3
srv send r=True
srv> send 4
srv send r=True
srv> send 5
srv send r=True
srv> send 6
srv send r=True
srv> send 7
srv send r=True
srv> send 8
srv send r=True

クライアント側のwifiをOFF

クライアント側の表示は上記のまま固定。

サーバ側

srv> send 9
srv send r=True
srv> send 10
srv send r=True
srv> send 11
srv send r=True
srv> send 12
srv send r=True
srv> send 13
srv send r=True
srv> send 14
srv send r=True
srv> send 15
	:

全然 send() 成功し続けて、何事も無いように進行してます。

しばし待機。

クライアント側のwifiをONに戻す

サーバ側は、何事もないまま、send() 成功の表示を順調に繰り返したままです。

クライアント側

cli recv 9101112131415161718192021222324252627282930313233343536
cli recv 37
cli recv 38
cli recv 39
cli recv 40

突然、受信スレッドが 1回の recv() で、一気に溜まってるデータを返してきました。

"9", "10", "11", ... "36" まで、抜けなく文字列が合体して返ってきました。

サーバ側は

	:
srv send r=True
srv> send 31
srv send r=True
srv> send 32
srv send r=True
srv> send 33
srv send r=True
srv> send 34
srv send r=True
srv> send 35
srv send r=True
srv> send 36
srv send r=True
srv> send 37
srv send r=True
srv> send 38
srv send r=True
srv> send 39
srv send r=True
srv> send 40
	:

クライアント側のwifiがOFFになってようがONになってようが、 全然関係ないかのように順調な表示が続いてます。

クライアント側のプログラム落としてみる

^Cキーで停止してみます。

	:
cli recv 41
cli recv 42
cli recv 43
cli recv 44
^CTraceback (most recent call last):
  File "./test_sock2.py", line 198, in <module>
    run()
  File "./test_sock2.py", line 194, in run
    quit_ev.wait()
  File "/usr/lib/python3.6/threading.py", line 551, in wait
    signaled = self._cond.wait(timeout)
  File "/usr/lib/python3.6/threading.py", line 295, in wait
    waiter.acquire()
KeyboardInterrupt

サーバ側

	:
srv> send 43
srv send r=True
srv> send 44
srv send r=True
srv> send 45
srv send r=True
srv> send 46
srv send r=False
srv> send 47
srv send r=False
srv> send 48
srv send r=False
srv> send 49
srv send r=False
srv> send 50
	:

さすがに send() False で送信失敗が返ってます。

クライアント側でソケットがクローズされるので、サーバ側のsend()が失敗します。


考察その2

ということは、この一方通行のタイプでは、 クライアント側でwifiが届かない事を、サーバ側で検知できませんね。

サーバ側で、もっともっと大量のデータを送ろうとすると、 途絶えている間のバッファリングが、やばそうです。

クライアント側からサーバ側へ、定期的に生存確認的なデータを流すなどして、 wifiが届いて無い事をサーバ側で検知させるしかなさそうですね。