Skip to content

Commit df8e811

Browse files
committed
Create Chat Room With Python Socket
Create Chat Room With Python Socket
1 parent eac29b8 commit df8e811

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

client.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import sys
2+
import socket
3+
import select
4+
import errno
5+
6+
7+
HEADER_LENGTH = 10
8+
9+
IP = "192.168.204.1"
10+
PORT = 5052
11+
my_username = input("Username: ")
12+
13+
# Create a socket
14+
# socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
15+
# socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
16+
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17+
18+
# Connect to a given ip and port
19+
client_socket.connect((IP, PORT))
20+
21+
# Set connection to non-blocking state, so .recv() call won;t block, just return some exception we'll handle
22+
client_socket.setblocking(False)
23+
24+
# Prepare username and header and send them
25+
# We need to encode username to bytes, then count number of bytes and prepare header of fixed size, that we encode to bytes as well
26+
username = my_username.encode('utf-8')
27+
username_header = f"{len(username):<{HEADER_LENGTH}}".encode('utf-8')
28+
client_socket.send(username_header + username)
29+
30+
while True:
31+
32+
# Wait for user to input a message
33+
message = input(f'{my_username} > ')
34+
35+
# If message is not empty - send it
36+
if message:
37+
38+
# Encode message to bytes, prepare header and convert to bytes, like for username above, then send
39+
message = message.encode('utf-8')
40+
message_header = f"{len(message):<{HEADER_LENGTH}}".encode('utf-8')
41+
client_socket.send(message_header + message)
42+
43+
try:
44+
# Now we want to loop over received messages (there might be more than one) and print them
45+
while True:
46+
47+
# Receive our "header" containing username length, it's size is defined and constant
48+
username_header = client_socket.recv(HEADER_LENGTH)
49+
50+
# If we received no data, server gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
51+
if not len(username_header):
52+
print('Connection closed by the server')
53+
sys.exit()
54+
55+
# Convert header to int value
56+
username_length = int(username_header.decode('utf-8').strip())
57+
58+
# Receive and decode username
59+
username = client_socket.recv(username_length).decode('utf-8')
60+
61+
# Now do the same for message (as we received username, we received whole message, there's no need to check if it has any length)
62+
message_header = client_socket.recv(HEADER_LENGTH)
63+
message_length = int(message_header.decode('utf-8').strip())
64+
message = client_socket.recv(message_length).decode('utf-8')
65+
66+
# Print message
67+
print(f'{username} > {message}')
68+
69+
except IOError as e:
70+
# This is normal on non blocking connections - when there are no incoming data error is going to be raised
71+
# Some operating systems will indicate that using AGAIN, and some using WOULDBLOCK error code
72+
# We are going to check for both - if one of them - that's expected, means no incoming data, continue as normal
73+
# If we got different error code - something happened
74+
if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
75+
print('Reading error: {}'.format(str(e)))
76+
sys.exit()
77+
78+
# We just did not receive anything
79+
continue
80+
81+
except Exception as e:
82+
# Any other exception - something happened, exit
83+
print('Reading error: '.format(str(e)))
84+
sys.exit()

server.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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

Comments
 (0)