-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
295 lines (241 loc) · 11.5 KB
/
server.py
File metadata and controls
295 lines (241 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import socket
import struct
import time
HOST = "127.0.0.1" # The default host for the server
PORT = 9999 # The default port for the server
SIZE = 1024 #first size of the recieve socket
HEADERSIZE =16 # the size of the header in bytes
CASE_2 = False # flag for case 2
CASE_5 = False # flag for case 5
second_time = False # flag for case 5
"""
This function show the menu to the user to choose a case from the options (1-5)
**Returns**:
`int` - The chosen case number (1-5).
"""
def menu():
while True:
try:
ans = int(input("There are 5 cases in the program. Please enter your choice (same in the server and the client running):\n"
"1. For the regular case, press 1\n"
"2. For the case with timeout, press 2\n"
"3. For the case with lose of the first packet, press 3\n"
"4. For the case with lose of some packet (in this case will be M1), press 4\n"
"5. For the case with lose of some ACK (in this case will be ACK5), press 5\n"))
if ans in [1, 2, 3, 4, 5]:
return ans
else:
print("Invalid choice. Please choose a number between 1 and 5.")
except ValueError:
print("Invalid input. Please enter a valid number between 1 and 5.")
"""
This function build the header for each packet,
It consists of 4 units of 4 bits (total 16 bits), where the division is as follows:
First 4 bits for the seq_number (we made a particularly large number for the amount: 2**32)
Second 4 bits for the amount of packets (how many in total there are for this message)
Third 4 bits for the amount of bits that need to be read for each packet
Last 4 bits are for the ACK (here we also made a particularly large number for the amount: 2**32)
-Ensures all inputs are within valid ranges (0 to 2^32-1).
-Uses `struct.pack` to create a header with the format `!IIII`.
(struct.pack('!IIII') converts four unsigned integers into a 16-byte binary representation in big-endian order (!).)
**Parameters**:
- `packet_number` (int): Sequence number of the packet.
- `total_packets` (int): Total number of packets in the message.
- `bytes_to_read` (int): Size of the payload in the packet to read.
- `ack` (int): Acknowledgment number.
**Returns**:
`bytes` - A packed binary header.
"""
def build_header(packet_number, total_packets, bytes_to_read, ack):
if not (0 <= packet_number < 2 ** 32):
raise ValueError("Packet number must be between 0 and 2^32-1")
if not (0 <= total_packets < 2 ** 32):
raise ValueError("Total packets must be between 0 and 2^32-1")
if not (0 <= bytes_to_read < 2 ** 32):
raise ValueError("Bytes to read must be between 0 and 2^32-1")
if not (0 <= ack < 2 ** 32):
raise ValueError("ack must be between 0 and 2^32-1")
header = struct.pack("!IIII", packet_number, total_packets, bytes_to_read, ack)
return header
"""
This function Parses a 16-byte header and extracts its components.
- Validates the header size.
- Uses `struct.unpack` to decode the header.
**Parameters**:
- `header` (bytes): The binary header to parse.
**Returns**:
`tuple` - (`packet_number`, `total_packets`, `bytes_to_read`, `ack`).
"""
def parse_header(header):
if len(header) != HEADERSIZE:
raise ValueError(f"Header must be exactly {HEADERSIZE} bytes")
packet_number, total_packets, bytes_to_read, ack = struct.unpack("!IIII", header)
return packet_number, total_packets, bytes_to_read, ack
"""
This function reads the maximum message size from a given configuration file.
**Parameters**:
- `filename` (str): The name of the configuration file.
**Returns**:
`int` - The maximum message size as read from the file.
"""
def read_maximum_msg_size(filename):
with open(filename, 'r') as file:
for line in file:
if line.startswith("maximum_msg_size:"):
_, value = line.split(":")
return int(value.strip())
raise ValueError("maximum_msg_size not found in the file")
"""
This function handles the request from the client for the maximum message size.
It prompts the server administrator to input the max size and sends it to the client.
**Parameters**:
- `client_socket` (socket): The socket connected to the client.
**Returns**:
`int` - The maximum message size sent to the client.
"""
def handle_max_msg_size_request(client_socket):
max_msg_size = int(input("The client want to know what is the max size of massage that you can receive \nPlease enter the max size of the message:"))
client_socket.send(str(max_msg_size).encode())
return max_msg_size
"""
This is the main function in the server program,its handles communication with a connected client.
It receives packets, processes them, and sends acknowledgment (ACK) packets back to the client.
**Parameters**:
- `client_socket` (socket.socket): The socket connected to the client.
- `client_address` (tuple): The address of the client (IP, port).
- `max_size` (int, optional): The maximum size of the message that the server can handle.
"""
def client_handler(client_socket: socket.socket, client_address: tuple[str, int], max_size) -> None:
client_addr = f"{client_address[0]}:{client_address[1]}"
print(f"Connection established with {client_addr}\n")
# Set the size limit for the recv function (either from config file or manual input)
size = 0
if max_size is not None:
size = max_size
else:
message = client_socket.recv(SIZE).decode()
if message == 'MAX_MSG_SIZE_REQUEST':
size = handle_max_msg_size_request(client_socket)
print("waiting for the massage...\n")
accept_massage = {} # Dictionary to store the received message parts
current = 0 # The next expected packet number
try:
buffer = b"" # Buffer to store incoming data temporarily
while True:
message = client_socket.recv(size+HEADERSIZE)
if not message:
break
buffer += message # Append received data to the buffer
# Process packets from the buffer if it contains enough data
while len(buffer) >= HEADERSIZE:
packet_number, total_packets, bytes_to_read, ack=parse_header(buffer[:HEADERSIZE])
if len(buffer) < HEADERSIZE + bytes_to_read:
break
content=buffer[HEADERSIZE:HEADERSIZE+bytes_to_read].decode()
buffer = buffer[HEADERSIZE+bytes_to_read:]
# Process the packet based on its sequence number
# if it's the packet number I expected to, its add to accept_message dic in the right place
if current == packet_number:
print(f"Received: M{packet_number}:{content}")
accept_massage[packet_number] = content
# Check if all previous messages in sequence have been received
i = current
while (i + 1) in accept_massage:
i += 1
# Build and send ACK for the last received message
ack_header = build_header(0, 1, 0, i)
# Adjustments for special cases.
global second_time
if CASE_2 == True:
time.sleep(1)
if CASE_5 == True and i == 5 and second_time == False :
current = i + 1
print(f"#####but now we will lose ACK{i}######")
second_time = True
break
client_socket.send(ack_header)
print(f"Sent: ACK{i}")
print("")
current = i + 1
# If the packet is out of order (higher sequence number), save it but request the missing packet
elif packet_number > current:
print(f"Received: M{packet_number}:{content}")
accept_massage[packet_number] = content
print(f"Didn't get yet M{current}")
# Send an acknowledgment for the last properly received packet
if(current==0):
ack_header = build_header(0, 1, 0, (2 ** 32)-1)
else:
ack_header = build_header(0, 1, 0, current-1)
client_socket.send(ack_header)
print(f"Sent: ACK{current-1}")
print("")
# If the packet has already been received, log it but do nothing
else:
print(f"Received: M{packet_number}:{content} but already receive")
except Exception as e:
print(f"Error handling client {client_addr}: {e}")
finally:
print(f"Connection closed by {client_addr}")
client_socket.close()
print("")
the_massege =""
for massage_segment in accept_massage.values():
the_massege = the_massege + massage_segment
print(f"The massage that accept from the client is: {the_massege}")
"""
This function sets up the server to accept incoming client connections and handle them.
It listens on the specified host and port and calls `client_handler` to process client requests.
If a configuration file is provided, it reads the maximum message size from it.
**Parameters**:
- `config_file` (str, optional): The name of the configuration file.
"""
def server(config_file=None) -> None:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)# Allow reuse of the address
server_socket.bind((HOST, PORT))
server_socket.listen()
print(f"Listening on {HOST}:{PORT}")
max_size = None
if config_file:
max_size = read_maximum_msg_size(config_file)
while True:
client_socket,address = server_socket.accept()
print("Client connected")
client_handler(client_socket, address, max_size)
print("")
close = input("Do you want to close the server? press 1 for yes \n")
if close.strip() == '1':
break
else:
continue
print("server closed")
server_socket.close()
"""
This is the main entry point of the program. It executes the following tasks:
1. Displays a menu to the user to choose one of the five cases.
2. Based on the user's input, sets the appropriate flags (`CASE_2` or `CASE_5`).
3. Prompts the user for a configuration file (if any) that contains the maximum message size.
If not, user need to input manual in `server` function.
-Ensures the inputs (text file name) exist in the folder.
"""
if __name__ == '__main__':
a = int(menu())
if a == 2:
CASE_2 = True
if a== 5:
CASE_5 = True
while True:
config_file = input(
"Enter the text file name to read the Max_mas_size (for example: text2), or press Enter to enter manual: \n")
if config_file:
try:
with open(config_file, 'r') as file:
print(file.read())
server(config_file)
break
except FileNotFoundError:
print("The file does not exist. Please try again.")
else:
server()
break