def broadcast_message(server_socket, sock, conn_list, message): for client in conn_list: # don't broadcast to self and the server if client != server_socket and client != sock: try: client.send(message) # todo: be smarter here https://docs.python.org/3/library/socket.html#exceptions except: log.warn("Client (%s, %s) is not connected." % client) client.close() conn_list.remove(client)
def serve(POISON_PILL): sock_tcp = None rx_sockets = [] tx_sockets = [] err_sockets = [] while True: # build a connection if we don't have one. check none specifically. don't make this a complex with # POISON_PILL -- it's possible we may need to have a listener for clients to drain their buffers if sock_tcp is None: sock_tcp = make_server_socket() try: # rlist - sockets to try reading; return from select is subset of actually readable (has data in buffer) # wlist - sockets to try writing to; return from select is subset of actually writable (outbound buffer available) # xlist - sockets to check for errors; return from select is subset of actually errored # select is blocking, so give it a timeout # "server" goes to rlist # socket to .connect() to clients gets appended into the wlist # see man select for more details try: rx_sockets, tx_sockets, err_sockets = select.select(SOCKET_CONNECTIONS, [], [], API.SOCKET_TIMEOUT_SECONDS) except KeyboardInterrupt: POISON_PILL = True except select.error as e: log.fatal("Could not select() on sockets. Closing socket. %s " % e) # break here will jump to the close break # Please double-check the logic here # check for poison pill. bool is very slightly faster than constant int 1 comparison in py3.5 if POISON_PILL: log.info("Received poison pill for shutdown.") # if we didn't have any clients between startup and shutdown, rx_sockets will be empty. if rx_sockets: for sock in rx_sockets: try: sock.shutdown(1) # give it a little time for the socket to process the shutdown time.sleep(.2) sock.close() except OSError: # socket probably wasn't connected at .shutdown() pass SOCKET_CONNECTIONS.remove(sock) raise SystemExit('YAMS Socket Server exiting due to a poison pill.') # to respond to conversation initiated by a client (recv()) for sock in rx_sockets: if sock == sock_tcp: # The server socket is responsible for accepting incoming connections and responding with its own client # connection socket. We know we have a new client conn when data comes into our server socket buffer. # There shouldn't be an issue with this strategy for select/async with non-blocking sockets, but if so, this # would be where to dispatch a new thread/native thread socket_fd, address = sock_tcp.accept() SOCKET_CONNECTIONS.append(socket_fd) log.info("Client (%s, %s) connected" % address) else: # rx'd from a client try: # message from the client here rx_data = sock.recv(API.SOCKET_RX_BUFFER_BYTES) # no exception, we have an rx_data if rx_data: # todo: process the data. if way too large, disconnect client -- something is wrong.. # socket.send returns the number of bytes sent, so this should go to the metrics gather-er # sendall returns None on success. there is no way to determine how much data was sent on err, count bytes for tx size # check for quit messages, evaluate based on the different yams event types # socket.send returns the number of bytes sent, so this should go to the metrics gather-er # sendall returns None on success. there is no way to determine how much data was sent on err, count bytes for tx size if not API.DEBUG: # 1) Include zlib library and metadata about the request (+= zlib.ZLIB_VERSION) # e.g. metadata: zlib=1.2.5 # 2) Checksum and store in checksum field (md5 or fletchers? fast, cheap - SSL does the security) # 3) Compress (zlib.compress()) # 4) Encrypt # with this order, if there's a corruption over the wire, zlib will see it as corrupted data # then we'll have the checksum to compare the uncompressed payload against. pass # Else: Checksum, store checksum in message sock.send(bytes_encoder("RECEIVED DATA: %s" % rx_data)) # Probably errored on rx_data -- did something else deplete our buffer? # this next exception type is an educated guess -- couldn't get timing down for broken socket (e.g. ^c) except select.error as e: # Probably fine that the connection is no longer there after initial connection. # see man 2 accept log.debug(str(e)) log.warn("Client socket <%s> is not connected." % sock) # this could be harmless, but could be a sign of unintended clamping of client conns # just close the conn and then remove it from our candidate FDs. don't bother with .shutdown(), but # do close explicitly instead of assuming it will get GC'd sock.close() try: log.info("Removing a socket due to a select error.") SOCKET_CONNECTIONS.remove(sock) except ValueError: # sock wasn't in socket_connections. that's what we want, so that's fine. continue # to initiate a conversation with a client (connect()) for socket in tx_sockets: pass for socket in err_sockets: pass if API.DEBUG: log.debug("rlist: %s" % rx_sockets) log.debug("wlist: %s" % tx_sockets) log.debug("xlist: %s" % err_sockets) time.sleep(API.SOCKET_THREAD_YIELD_EPSILON) # poll for server shutdown on some interval too. if we reach shutdown, tear down connections from clients # no longer True, exception raised, or we exited sock_tcp.close()
def serve(POISON_PILL): sock_tcp = None rx_sockets = [] tx_sockets = [] err_sockets = [] while True: # build a connection if we don't have one. check none specifically. don't make this a complex with # POISON_PILL -- it's possible we may need to have a listener for clients to drain their buffers if sock_tcp is None: sock_tcp = make_server_socket() try: # rlist - sockets to try reading; return from select is subset of actually readable (has data in buffer) # wlist - sockets to try writing to; return from select is subset of actually writable (outbound buffer available) # xlist - sockets to check for errors; return from select is subset of actually errored # select is blocking, so give it a timeout # "server" goes to rlist # socket to .connect() to clients gets appended into the wlist # see man select for more details try: rx_sockets, tx_sockets, err_sockets = select.select( SOCKET_CONNECTIONS, [], [], API.SOCKET_TIMEOUT_SECONDS) except KeyboardInterrupt: POISON_PILL = True except select.error as e: log.fatal("Could not select() on sockets. Closing socket. %s " % e) # break here will jump to the close break # Please double-check the logic here # check for poison pill. bool is very slightly faster than constant int 1 comparison in py3.5 if POISON_PILL: log.info("Received poison pill for shutdown.") # if we didn't have any clients between startup and shutdown, rx_sockets will be empty. if rx_sockets: for sock in rx_sockets: try: sock.shutdown(1) # give it a little time for the socket to process the shutdown time.sleep(.2) sock.close() except OSError: # socket probably wasn't connected at .shutdown() pass SOCKET_CONNECTIONS.remove(sock) raise SystemExit( 'YAMS Socket Server exiting due to a poison pill.') # to respond to conversation initiated by a client (recv()) for sock in rx_sockets: if sock == sock_tcp: # The server socket is responsible for accepting incoming connections and responding with its own client # connection socket. We know we have a new client conn when data comes into our server socket buffer. # There shouldn't be an issue with this strategy for select/async with non-blocking sockets, but if so, this # would be where to dispatch a new thread/native thread socket_fd, address = sock_tcp.accept() SOCKET_CONNECTIONS.append(socket_fd) log.info("Client (%s, %s) connected" % address) else: # rx'd from a client try: # message from the client here rx_data = sock.recv(API.SOCKET_RX_BUFFER_BYTES) # no exception, we have an rx_data if rx_data: # todo: process the data. if way too large, disconnect client -- something is wrong.. # socket.send returns the number of bytes sent, so this should go to the metrics gather-er # sendall returns None on success. there is no way to determine how much data was sent on err, count bytes for tx size # check for quit messages, evaluate based on the different yams event types # socket.send returns the number of bytes sent, so this should go to the metrics gather-er # sendall returns None on success. there is no way to determine how much data was sent on err, count bytes for tx size if not API.DEBUG: # 1) Include zlib library and metadata about the request (+= zlib.ZLIB_VERSION) # e.g. metadata: zlib=1.2.5 # 2) Checksum and store in checksum field (md5 or fletchers? fast, cheap - SSL does the security) # 3) Compress (zlib.compress()) # 4) Encrypt # with this order, if there's a corruption over the wire, zlib will see it as corrupted data # then we'll have the checksum to compare the uncompressed payload against. pass # Else: Checksum, store checksum in message sock.send(bytes_encoder("RECEIVED DATA: %s" % rx_data)) # Probably errored on rx_data -- did something else deplete our buffer? # this next exception type is an educated guess -- couldn't get timing down for broken socket (e.g. ^c) except select.error as e: # Probably fine that the connection is no longer there after initial connection. # see man 2 accept log.debug(str(e)) log.warn("Client socket <%s> is not connected." % sock) # this could be harmless, but could be a sign of unintended clamping of client conns # just close the conn and then remove it from our candidate FDs. don't bother with .shutdown(), but # do close explicitly instead of assuming it will get GC'd sock.close() try: log.info("Removing a socket due to a select error.") SOCKET_CONNECTIONS.remove(sock) except ValueError: # sock wasn't in socket_connections. that's what we want, so that's fine. continue # to initiate a conversation with a client (connect()) for socket in tx_sockets: pass for socket in err_sockets: pass if API.DEBUG: log.debug("rlist: %s" % rx_sockets) log.debug("wlist: %s" % tx_sockets) log.debug("xlist: %s" % err_sockets) time.sleep(API.SOCKET_THREAD_YIELD_EPSILON) # poll for server shutdown on some interval too. if we reach shutdown, tear down connections from clients # no longer True, exception raised, or we exited sock_tcp.close()