web-dev-qa-db-ja.com

送受信せずにソケットのハングアップを検出しますか?

TCPサーバーは、特定の要求への応答の本文の生成を開始するのに15秒以上かかる可能性があります。一部のクライアントは、応答にさらに時間がかかる場合、最後に接続を閉じることを望んでいます。完了するまでに数秒かかります。

応答の生成はCPUを集中的に使用するため、クライアントが接続を閉じるとすぐにタスクを停止したいと思います。現時点では、最初のペイロードを送信してさまざまなハングアップエラーを受信するまで、これはわかりません。

ピアがデータを送受信せずに接続を閉じたことをどのように検出できますか?つまり、recvの場合、すべてのデータがカーネルに残るか、sendの場合、実際にはデータが送信されません。

25
Matt Joiner

送信用と受信用に別々のTCPリンクがある機器との通信で繰り返し問題が発生しました。基本的な問題は、TCPスタックが一般的に単に読み込もうとしているときにソケットが閉じていることを通知します-リンクのもう一方の端がドロップされたことを通知するには、書き込みを行う必要があります。部分的には、TCP設計されています(読み取りはパッシブです)。

私は、ブレアの答えがソケットがもう一方の端でうまくシャットダウンされた場合(つまり、適切な切断メッセージを送信した場合)に機能すると思いますが、もう一方の端が無作法にリッスンを停止しただけの場合はそうではありません。

メッセージの最初に、完全な応答の準備ができる前に送信することから始めることができるかなり固定形式のヘッダーがありますか?例えばXML doctype?また、メッセージのいくつかのポイントでいくつかの余分なスペースを送信することで回避できますか-ソケットがまだ開いていることを確認するために出力できるいくつかのnullデータだけですか?

17
asc99c

select モジュールには、必要なものが含まれています。 Linuxサポートのみが必要で、十分に新しいカーネルがある場合は、select.epoll()が必要な情報を提供します。ほとんどのUnixシステムはselect.poll()をサポートします。

クロスプラットフォームのサポートが必要な場合の標準的な方法は、select.select()を使用して、ソケットに読み取り可能なデータがあるとマークされているかどうかを確認することです。そうであるがrecv()が0バイトを返す場合、もう一方の端はハングアップしています。

私は常に Beej's Guide to Network Programming 良い(C向けに書かれているが、一般に標準のソケット操作に適用できることに注意)一方で、 Socket Programming How-To まともなPython概要があります。

Edit:次の例は、単純なサーバーが着信コマンドをキューに入れ、接続が閉じられたことを検出するとすぐに処理を終了する方法を示しています。リモートエンド。

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()

これを使用するには、スクリプトを実行し、別の端末telnetでlocalhost、ポート7557にアクセスします。実行例の出力は、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.

代替案

別の編集: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)

:これは適切なシャットダウンのみを検出します。リモートエンドが適切なメッセージを送信せずにリッスンを停止しただけの場合、書き込みを試みてエラーが発生するまではわかりません。それを確認することは、読者のための練習問題として残されています。また、ループ全体にエラーチェックを実行して、内部で何かが壊れた場合にサーバー自体を適切にシャットダウンすることもできます。

27
Blair

ソケットの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>
12
ninjalj

同様の問題に取り組んだ後、私にとってはうまくいく解決策を見つけましたが、ノンブロッキングモードでrecv()を呼び出して、次のようにデータを読み取ろうとする必要があります。

_bytecount=recv(connectionfd,buffer,1000,MSG_NOSIGNAL|MSG_DONTWAIT);
_

Nosignalはエラー時にプログラムを終了しないように指示し、dontwaitはブロックしないように指示します。このモードでは、recv()は3つの可能な応答タイプのいずれかを返します。

  • _-1_読み取るデータがない場合、またはその他のエラーの場合。
  • _0_もう一方の端がうまく切れた場合
  • 待機中のデータがあった場合は_1_以上。

したがって、戻り値をチェックすることにより、それが0の場合、それはもう一方の端が電話を切ったことを意味します。 _-1_の場合、errnoの値を確認する必要があります。 errnoEAGAINまたはEWOULDBLOCKと等しい場合、接続はサーバーのtcpスタックによってまだ生きていると考えられます。

このソリューションでは、recv()への呼び出しを集中的なデータ処理ループ内、またはコード内の1秒間に10回または任意の場所で呼び出す必要があるため、プログラムに次の知識を与える必要があります。電話を切る相手。

もちろん、これは正しい接続シャットダウンシーケンスを実行せずに立ち去るピアには効果がありませんが、適切に実装されたTCPクライアントは接続を正しく終了します。

また、クライアントが大量のデータを送信して電話を切った場合、recv()は、空の読み取りを取得する前に、そのデータをすべてバッファーから読み取る必要があることにも注意してください。

3
Jesse Gordon