selfboot / AnnotatedShadowSocks

Annotated shadowsocks(python version)
Other
3 stars 1 forks source link

Details about select.poll() #15

Open selfboot opened 7 years ago

selfboot commented 7 years ago

The poll() function provides similar features to #11 select(), but the underlying implementation is more efficient.

Poll echo server

An echo server built on poll() starts with the same socket configuration code used in the example #11 . Python implements poll() with a class that manages the registered data channels being monitored. Channels are added by calling register() with flags indicating which events are interesting for that channel. The full set of flags is:

Event Description
POLLIN Input ready
POLLPRI Priority input ready
POLLOUT Able to receive output
POLLERR Error
POLLHUP Channel closed
POLLNVAL Channel not open

The echo server will be setting up some sockets just for reading, and others to be read from or written to. The appropriate combinations of flags are saved to the local variables READ_ONLY and READ_WRITE.

Since poll() returns a list of tuples containing the file descriptor for the socket and the event flag, a mapping from file descriptor numbers to objects is needed to retrieve the socket to read or write from it.

Then:

  1. The server socket is registered so that any incoming connections or data triggers an event.
  2. The server’s loop calls poll(), then processes the “events” returned by looking up the socket and taking action based on the flag in the event.
  3. As with select(), when the main server socket is “readable,” that really means there is a pending connection from a client. The new connection is registered with the READ_ONLY flags to watch for new data to come through it.
  4. Sockets other than the server are existing clients, and recv() is used to access the data waiting to be read.
    • If recv() returns any data, it is placed into the outgoing queue for the socket and the flags for that socket are changed using modify() so poll() will watch for the socket to be ready to receive data.
    • An empty string returned by recv() means the client disconnected, so unregister() is used to tell the poll object to ignore the socket.
  5. The POLLHUP flag indicates a client that “hung up” the connection without closing it cleanly. The server stops polling clients that disappear.
  6. The handling for writable sockets looks like the version used in the example for select(), except that modify() is used to change the flags for the socket in the poller, instead of removing it from the output list.
  7. Finally, any events with POLLERR cause the server to close the socket.
poll_server.py is here. (click to unfold) ```python #! /usr/bin/env python # -*- coding: utf-8 -*- # py 2.X from __future__ import print_function import socket import select import sys import Queue # Create a TCP/IP socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking(0) # Bind the socket to the port server_address = ('localhost', 10000) print('starting up on %s port %s' % server_address) server.bind(server_address) # Listen for incoming connections server.listen(5) # Keep up with the queues of outgoing messages message_queues = {} # Do not block forever(milliseconds) TIMEOUT = 1000 # Commonly used flag setes READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR READ_WRITE = READ_ONLY | select.POLLOUT # Set up the poller poller = select.poll() poller.register(server, READ_ONLY) # Map file description to socket objects fd_to_sockets = {server.fileno(): server} while True: # Wait for at least one of the sockets to be ready for processing print('\nWaiting for the next event') events = poller.poll(TIMEOUT) for fd, flag in events: # Retrieve the actual socket from its file descriptor s = fd_to_sockets[fd] # Handle inputs if flag & (select.POLLIN | select.POLLPRI): # A readable server is ready to accept a connection if s is server: connection, client_address = s.accept() print('new connection from', client_address) connection.setblocking(0) fd_to_sockets[connection.fileno()] = connection poller.register(connection, READ_ONLY) # Give the connection a queue for the data we want to send message_queues[connection] = Queue.Queue() else: data = s.recv(1024) # A readable client socket has data if data: print('received "%s" from %s' % (data, s.getpeername())) # Add output channel for response message_queues[s].put(data) poller.modify(s, READ_WRITE) else: # Interpret empty result as closed connection print('closing ', client_address, 'after reading no data') # Stop listening for input on the connection poller.unregister(s) s.close() # Remove the message queue del message_queues[s] elif flag & select.POLLHUP: # Client hung up print('closing', client_address, ' after receiving HUP') # Stop listening for input on the connection poller.unregister(s) s.close() elif flag & select.POLLOUT: # Socket is ready to send data, if there is any to send try: next_msg = message_queues[s].get_nowait() except Queue.Empty: # No message waiting so stop checking for writability print('output queue for', s.getpeername(), 'is empty') poller.modify(s, READ_ONLY) else: print('sending "%s" to "%s"' % (next_msg, s.getpeername())) s.send(next_msg) elif flag & select.POLLERR: print('handling exceptional condition for', s.getpeername()) poller.unregister(s) s.close() # Remove message queue del message_queues[s] ```

Client is just like the #11 client.py.

Run Result

Run the server.py. (click to unfold) ```bash $ python server.py Waiting for the next event Waiting for the next event new connection from ('127.0.0.1', 45096) Waiting for the next event new connection from ('127.0.0.1', 45098) Waiting for the next event received "This is the message." from ('127.0.0.1', 45096) received "This is the message." from ('127.0.0.1', 45098) Waiting for the next event sending "This is the message." to "('127.0.0.1', 45096)" sending "This is the message." to "('127.0.0.1', 45098)" Waiting for the next event output queue for ('127.0.0.1', 45096) is empty output queue for ('127.0.0.1', 45098) is empty Waiting for the next event received "It will be sent" from ('127.0.0.1', 45096) received "It will be sent" from ('127.0.0.1', 45098) Waiting for the next event sending "It will be sent" to "('127.0.0.1', 45096)" sending "It will be sent" to "('127.0.0.1', 45098)" Waiting for the next event output queue for ('127.0.0.1', 45096) is empty output queue for ('127.0.0.1', 45098) is empty Waiting for the next event received "in parts" from ('127.0.0.1', 45096) received "in parts" from ('127.0.0.1', 45098) Waiting for the next event sending "in parts" to "('127.0.0.1', 45096)" sending "in parts" to "('127.0.0.1', 45098)" Waiting for the next event output queue for ('127.0.0.1', 45096) is empty output queue for ('127.0.0.1', 45098) is empty Waiting for the next event closing ('127.0.0.1', 45098) after reading no data closing ('127.0.0.1', 45098) after reading no data Waiting for the next event Waiting for the next event ```
Run the client.py. (click to unfold) ```bash $ python client.py connecting to localhost port 10000 ('127.0.0.1', 45096): sending "This is the message." ('127.0.0.1', 45098): sending "This is the message." ('127.0.0.1', 45096): received "This is the message." ('127.0.0.1', 45098): received "This is the message." ('127.0.0.1', 45096): sending "It will be sent" ('127.0.0.1', 45098): sending "It will be sent" ('127.0.0.1', 45096): received "It will be sent" ('127.0.0.1', 45098): received "It will be sent" ('127.0.0.1', 45096): sending "in parts" ('127.0.0.1', 45098): sending "in parts" ('127.0.0.1', 45096): received "in parts" ('127.0.0.1', 45098): received "in parts" ```

Ref

select – Wait for I/O Efficiently--poll

selfboot commented 6 years ago

poll is not supported by all operating systems, OS X does not support poll.

Instead of using poll, use select.kqueue() on OSX. It's similar to 'epoll' on Linux in that you can more efficiently register for types of file-descriptor / file system events which can be used in asynchronous code. Much more efficient than polling.