Home > Mobile >  How can I get a socket's .recv() not to block?
How can I get a socket's .recv() not to block?

Time:12-13

I'm trying to write a simple daemon that listens for orders on a Unix socket. The following works, but the connection.recv(1024) line blocks, meaning I can't kill the server gracefully:

import socket, os

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
    server.bind("/tmp/sock")
    server.listen()
    connection, __ = server.accept()
    with connection:
        while True:
            data = connection.recv(1024)
            print("Hi!")  # This line isn't executed 'til data is sent
            if data:
                print(data.decode())

Ideally, I'd like to place all of this inside a Thread that checks a self.should_stop property every self.LOOP_TIME seconds, and if that value is set to True, then exit. However, as that .recv() line blocks, there's no way for my program to be doing anything other than waiting at any given time.

Surely there's a proper way to do this, but as I'm new to sockets, I have no idea what that is.

CodePudding user response:

Use a timeout identical to your LOOP_TIME like so:

import socket, os

LOOP_TIME = 10
should_stop = False

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
    server.bind("/tmp/sock")
    server.listen()
    connection, __ = server.accept()
    connection.settimeout(LOOP_TIME)
    with connection:
        while not should_stop:
            try:
                data = connection.recv(1024)
            except socket.timeout:
                continue
            print("Hi!")  # This line isn't executed 'til data is sent
            if data:
                print(data.decode())

You may use select, but if it's only a single simple socket, this way is a bit less complicated.

You can choose to place it in a different thread with a self.should_stop or just at the main - it will now listen to the KeyboardInterrupt.

CodePudding user response:

Using arbitrary-length timeouts is always a bit unsatisfactory -- either you set the timeout-value to a relatively long time, in which case your program becomes slow to react to the quit-request, because it is pointlessly waiting for timeout period to expire... or you set the timeout-value to a relatively short time, in which case your program is constantly waking up to see if it should quit, wasting CPU power 24/7 to check for an event which might never arrive.

A more elegant way to deal with the problem is to create a pipe, and send a byte on the pipe when you want your event-loop to exit. Your event loop can simultaneously "watch" both the pipe's reading-end file-descriptor and your networking-socket(s) via select(), and when that file-descriptor indicates it is ready-for-read, your event loop can respond by exiting. This approach is entirely event-driven, so it requires no CPU wakeups except when there is actually something to do.

Below is an example version of your program that implements a signal-handler for SIGINT (aka pressing Control-C) to sends the please-quit-now byte on the pipe:

import socket, os
import select
import signal, sys

# Any bytes written to (writePipeFD) will become available for reading on (readPipeFD)
readPipeFD, writePipeFD = os.pipe()

# Set up a signal-handler to handle SIGINT (aka Ctrl C) events by writing a byte to the pipe
def signal_handler(sig, frame):
    print("signal_handler() is executing -- SIGINT detected!")
    os.write(writePipeFD, b"\0")  # doesn't matter what we write; a single 0-byte will do
signal.signal(signal.SIGINT, signal_handler)

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as serverSock:
    serverSock.bind("/tmp/sock")
    serverSock.listen()

    # Wait for incoming connection (or the please-quit signal, whichever comes first)
    connection = None
    while True:
       readReady,writeReady,exceptReady = select.select([readPipeFD,serverSock], [], [])
       if (readPipeFD in readReady):
          print("accept-loop: Someone wrote a byte to the pipe; time to go away!");
          break
       if (connection in readReady):
          connection, __ = serverSock.accept()
          break

    # Read data from incoming connection (or the please-quit signal, whichever comes first)
    if connection:
       with connection:
          while True:
             readReady,writeReady,exceptReady = select.select([readPipeFD,connection], [], [])
             if (readPipeFD in readReady):
                print("Connection-loop: Someone wrote a byte to the pipe; time to go away!");
                break
             if (connection in readReady):
                data = connection.recv(1024)
                print("Hi!")  # This line isn't executed 'til data is sent
                if data:
                   print(data.decode())

    print("Bye!")
  • Related