1+ import socket
2+ import select
3+ 4+ HEADER_LENGTH = 10
5+ 6+ IP = "192.168.204.1"
7+ PORT = 5052
8+ 9+ # Create a socket
10+ # socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
11+ # socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
12+ server_socket = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
13+ 14+ # SO_ - socket option
15+ # SOL_ - socket option level
16+ # Sets REUSEADDR (as a socket option) to 1 on socket
17+ server_socket .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
18+ 19+ # Bind, so server informs operating system that it's going to use given IP and port
20+ # For a server using 0.0.0.0 means to listen on all available interfaces, useful to connect locally to 127.0.0.1 and remotely to LAN interface IP
21+ server_socket .bind ((IP , PORT ))
22+ 23+ # This makes server listen to new connections
24+ server_socket .listen ()
25+ 26+ # List of sockets for select.select()
27+ sockets_list = [server_socket ]
28+ 29+ # List of connected clients - socket as a key, user header and name as data
30+ clients = {}
31+ 32+ print (f'Listening for connections on { IP } :{ PORT } ...' )
33+ 34+ # Handles message receiving
35+ def receive_message (client_socket ):
36+ 37+ try :
38+ 39+ # Receive our "header" containing message length, it's size is defined and constant
40+ message_header = client_socket .recv (HEADER_LENGTH )
41+ 42+ # If we received no data, client gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
43+ if not len (message_header ):
44+ return False
45+ 46+ # Convert header to int value
47+ message_length = int (message_header .decode ('utf-8' ).strip ())
48+ 49+ # Return an object of message header and message data
50+ return {'header' : message_header , 'data' : client_socket .recv (message_length )}
51+ 52+ except :
53+ 54+ # If we are here, client closed connection violently, for example by pressing ctrl+c on his script
55+ # or just lost his connection
56+ # socket.close() also invokes socket.shutdown(socket.SHUT_RDWR) what sends information about closing the socket (shutdown read/write)
57+ # and that's also a cause when we receive an empty message
58+ return False
59+ 60+ while True :
61+ 62+ # Calls Unix select() system call or Windows select() WinSock call with three parameters:
63+ # - rlist - sockets to be monitored for incoming data
64+ # - wlist - sockets for data to be send to (checks if for example buffers are not full and socket is ready to send some data)
65+ # - xlist - sockets to be monitored for exceptions (we want to monitor all sockets for errors, so we can use rlist)
66+ # Returns lists:
67+ # - reading - sockets we received some data on (that way we don't have to check sockets manually)
68+ # - writing - sockets ready for data to be send thru them
69+ # - errors - sockets with some exceptions
70+ # This is a blocking call, code execution will "wait" here and "get" notified in case any action should be taken
71+ read_sockets , _ , exception_sockets = select .select (sockets_list , [], sockets_list )
72+ 73+ 74+ # Iterate over notified sockets
75+ for notified_socket in read_sockets :
76+ 77+ # If notified socket is a server socket - new connection, accept it
78+ if notified_socket == server_socket :
79+ 80+ # Accept new connection
81+ # That gives us new socket - client socket, connected to this given client only, it's unique for that client
82+ # The other returned object is ip/port set
83+ client_socket , client_address = server_socket .accept ()
84+ 85+ # Client should send his name right away, receive it
86+ user = receive_message (client_socket )
87+ 88+ # If False - client disconnected before he sent his name
89+ if user is False :
90+ continue
91+ 92+ # Add accepted socket to select.select() list
93+ sockets_list .append (client_socket )
94+ 95+ # Also save username and username header
96+ clients [client_socket ] = user
97+ 98+ print ('Accepted new connection from {}:{}, username: {}' .format (* client_address , user ['data' ].decode ('utf-8' )))
99+ 100+ # Else existing socket is sending a message
101+ else :
102+ 103+ # Receive message
104+ message = receive_message (notified_socket )
105+ 106+ # If False, client disconnected, cleanup
107+ if message is False :
108+ print ('Closed connection from: {}' .format (clients [notified_socket ]['data' ].decode ('utf-8' )))
109+ 110+ # Remove from list for socket.socket()
111+ sockets_list .remove (notified_socket )
112+ 113+ # Remove from our list of users
114+ del clients [notified_socket ]
115+ 116+ continue
117+ 118+ # Get user by notified socket, so we will know who sent the message
119+ user = clients [notified_socket ]
120+ 121+ print (f'Received message from { user ["data" ].decode ("utf-8" )} : { message ["data" ].decode ("utf-8" )} ' )
122+ 123+ # Iterate over connected clients and broadcast message
124+ for client_socket in clients :
125+ 126+ # But don't sent it to sender
127+ if client_socket != notified_socket :
128+ 129+ # Send user and message (both with their headers)
130+ # We are reusing here message header sent by sender, and saved username header send by user when he connected
131+ client_socket .send (user ['header' ] + user ['data' ] + message ['header' ] + message ['data' ])
132+ 133+ # It's not really necessary to have this, but will handle some socket exceptions just in case
134+ for notified_socket in exception_sockets :
135+ 136+ # Remove from list for socket.socket()
137+ sockets_list .remove (notified_socket )
138+ 139+ # Remove from our list of users
140+ del clients [notified_socket ]
0 commit comments