送信用と受信用に別々の TCP リンクを持つ機器との通信で、繰り返し問題が発生しました。基本的な問題は、TCP スタックは通常、読み取りを試みているときにソケットが閉じていることを通知しないことです。リンクの反対側が切断されたことを通知するには、試行して書き込みを行う必要があります。部分的には、それが TCP の設計方法です (読み取りは受動的です)。
ブレアの答えは、相手側でソケットが適切にシャットダウンされた場合 (つまり、適切な切断メッセージが送信された場合) には機能すると思いますが、相手側が無礼にリッスンを停止した場合には機能しません。
メッセージの先頭に、応答全体の準備が整う前に送信することから始められるかなり固定された形式のヘッダーがありますか?例えばXML doctype?また、メッセージ内のいくつかのポイントで余分なスペースを送信しても問題を解決できますか? ソケットがまだ開いていることを確認するために出力できる null データだけですか?
select モジュールには、必要なものが含まれています。 Linux サポートのみが必要で、十分に新しいカーネルがある場合は、 select.epoll()
必要な情報を提供する必要があります。ほとんどの Unix システムは select.poll()
をサポートします .
クロスプラットフォームのサポートが必要な場合、標準的な方法は select.select()
を使用することです ソケットが読み取り可能なデータを持っているとマークされているかどうかを確認します。もしそうなら、しかし recv()
0 バイトを返します。相手側は電話を切りました。
Beej の Guide to Network Programming (C 向けに書かれていますが、一般的には標準的なソケット操作に適用できることに注意してください) は良いものだといつも思っていましたが、Socket Programming How-To には適切な Python の概要があります。
編集 :以下は、着信コマンドをキューに入れるが、接続がリモート エンドで閉じられたことを検出するとすぐに処理を終了するように単純なサーバーを作成する方法の例です。
import select
import socket
import time
# Create the server.
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), 7557))
serversocket.listen(1)
# Wait for an incoming connection.
clientsocket, address = serversocket.accept()
print 'Connection from', address[0]
# Control variables.
queue = []
cancelled = False
while True:
# If nothing queued, wait for incoming request.
if not queue:
queue.append(clientsocket.recv(1024))
# Receive data of length zero ==> connection closed.
if len(queue[0]) == 0:
break
# Get the next request and remove the trailing newline.
request = queue.pop(0)[:-1]
print 'Starting request', request
# Main processing loop.
for i in xrange(15):
# Do some of the processing.
time.sleep(1.0)
# See if the socket is marked as having data ready.
r, w, e = select.select((clientsocket,), (), (), 0)
if r:
data = clientsocket.recv(1024)
# Length of zero ==> connection closed.
if len(data) == 0:
cancelled = True
break
# Add this request to the queue.
queue.append(data)
print 'Queueing request', data[:-1]
# Request was cancelled.
if cancelled:
print 'Request cancelled.'
break
# Done with this request.
print 'Request finished.'
# If we got here, the connection was closed.
print 'Connection closed.'
serversocket.close()
これを使用するには、スクリプトを実行し、別のターミナルで localhost のポート 7557 に telnet で接続します。実行した例の出力では、3 つの要求をキューに入れましたが、3 つ目の要求の処理中に接続を閉じました:
Connection from 127.0.0.1
Starting request 1
Queueing request 2
Queueing request 3
Request finished.
Starting request 2
Request finished.
Starting request 3
Request cancelled.
Connection closed.
epoll 代替
別の編集: select.epoll
を使用して別の例を作成しました イベントを監視します。リモート エンドがハングアップしたときにイベントを受信する方法が見当たらないため、元の例よりもはるかに優れているとは思いません。データ受信イベントを監視し、長さゼロのメッセージをチェックする必要があります (繰り返しますが、このステートメントが間違っていることを証明したいと思います)。
import select
import socket
import time
port = 7557
# Create the server.
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), port))
serversocket.listen(1)
serverfd = serversocket.fileno()
print "Listening on", socket.gethostname(), "port", port
# Make the socket non-blocking.
serversocket.setblocking(0)
# Initialise the list of clients.
clients = {}
# Create an epoll object and register our interest in read events on the server
# socket.
ep = select.epoll()
ep.register(serverfd, select.EPOLLIN)
while True:
# Check for events.
events = ep.poll(0)
for fd, event in events:
# New connection to server.
if fd == serverfd and event & select.EPOLLIN:
# Accept the connection.
connection, address = serversocket.accept()
connection.setblocking(0)
# We want input notifications.
ep.register(connection.fileno(), select.EPOLLIN)
# Store some information about this client.
clients[connection.fileno()] = {
'delay': 0.0,
'input': "",
'response': "",
'connection': connection,
'address': address,
}
# Done.
print "Accepted connection from", address
# A socket was closed on our end.
elif event & select.EPOLLHUP:
print "Closed connection to", clients[fd]['address']
ep.unregister(fd)
del clients[fd]
# Error on a connection.
elif event & select.EPOLLERR:
print "Error on connection to", clients[fd]['address']
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Incoming data.
elif event & select.EPOLLIN:
print "Incoming data from", clients[fd]['address']
data = clients[fd]['connection'].recv(1024)
# Zero length = remote closure.
if not data:
print "Remote close on ", clients[fd]['address']
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Store the input.
else:
print data
clients[fd]['input'] += data
# Run when the client is ready to accept some output. The processing
# loop registers for this event when the response is complete.
elif event & select.EPOLLOUT:
print "Sending output to", clients[fd]['address']
# Write as much as we can.
written = clients[fd]['connection'].send(clients[fd]['response'])
# Delete what we have already written from the complete response.
clients[fd]['response'] = clients[fd]['response'][written:]
# When all the the response is written, shut the connection.
if not clients[fd]['response']:
ep.modify(fd, 0)
clients[fd]['connection'].shutdown(socket.SHUT_RDWR)
# Processing loop.
for client in clients.keys():
clients[client]['delay'] += 0.1
# When the 'processing' has finished.
if clients[client]['delay'] >= 15.0:
# Reverse the input to form the response.
clients[client]['response'] = clients[client]['input'][::-1]
# Register for the ready-to-send event. The network loop uses this
# as the signal to send the response.
ep.modify(client, select.EPOLLOUT)
# Processing delay.
time.sleep(0.1)
注意 :これは適切なシャットダウンのみを検出します。リモート エンドが適切なメッセージを送信せずにリッスンを停止した場合は、書き込みを試みてエラーが発生するまでわかりません。それを確認することは、読者の演習として残されています。また、ループ全体で何らかのエラー チェックを実行して、内部で何かが壊れた場合にサーバー自体を適切にシャットダウンすることをお勧めします。
ソケット KEEPALIVE オプションを使用すると、この種の「相手に通知せずに接続を切断する」シナリオを検出できます。
SO_KEEPALIVE オプションを SOL_SOCKET レベルで設定する必要があります。 Linux では、TCP_KEEPIDLE (キープアライブ プローブを送信する前の秒数)、TCP_KEEPCNT (キープアライブ プローブが失敗した後、もう一方の端が死んでいると宣言する)、および TCP_KEEPINTVL (キープアライブ プローブ間の秒単位の間隔) を使用して、ソケットごとのタイムアウトを変更できます。
Python の場合:
import socket
...
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1)
s.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 5)
netstat -tanop
ソケットがキープアライブ モードであることを示します:
tcp 0 0 127.0.0.1:6666 127.0.0.1:43746 ESTABLISHED 15242/python2.6 keepalive (0.76/0/0)
tcpdump
の間 キープアライブ プローブが表示されます:
01:07:08.143052 IP localhost.6666 > localhost.43746: . ack 1 win 2048 <nop,nop,timestamp 848683438 848683188>
01:07:08.143084 IP localhost.43746 > localhost.6666: . ack 1 win 2050 <nop,nop,timestamp 848683438 848682438>
01:07:09.143050 IP localhost.6666 > localhost.43746: . ack 1 win 2048 <nop,nop,timestamp 848683688 848683438>
01:07:09.143083 IP localhost.43746 > localhost.6666: . ack 1 win 2050 <nop,nop,timestamp 848683688 848682438>