-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.py
More file actions
306 lines (256 loc) · 13 KB
/
client.py
File metadata and controls
306 lines (256 loc) · 13 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
296
297
298
299
300
301
302
303
304
305
306
import math
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_3 = False # flag for case 3
CASE_4 = False # flag for case 4
"""
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 configuration file for the client.
- Ensures all required keys are present.
**Parameters**:
- `filename` (str): Path to the configuration file.
**Returns**:
`dict` - Configuration data with keys `message`, `maximum_msg_size`, `window_size`, and `timeout`.
"""
def read_input_file(filename):
required_keys = ['message', 'maximum_msg_size', 'window_size', 'timeout']
config = {}
with open(filename, 'r') as file:
for line in file:
key, value = line.split(":")
config[key.strip()] = value.strip()
for key in required_keys:
if key not in config:
raise ValueError(f"Missing required key: {key}")
return config
"""
This is the main function in the client program, hear the client sends the message to the server.
- Splits the message into segments based on `max_msg_size`.
- Constructs packets with headers and payloads.
- Implements sliding window and retransmission strategies.
- Handles cases such as timeouts and duplicate ACKs.
**Parameters**:
- `client_socket` (socket): The socket object connected to the server.
- `message` (str): The message to be sent.
- `max_msg_size` (int): Maximum size of each message segment.
- `window_size` (int): Number of packets that can be sent without waiting for ACKs.
- `timeout` (float): Timeout duration in seconds.
"""
def send_message(client_socket, message, max_msg_size, window_size, timeout):
message_segments = []
header_segments = []
i=0
num_of_packets = math.ceil(len(message) / max_msg_size) # Circle up
# Splitting the message into segments of size max_msg_size.
for seq_num in range(0, len(message),max_msg_size):
step = min(max_msg_size,len(message) - seq_num)
message_segments.append(message[seq_num:min(seq_num + max_msg_size,len(message))])
header = build_header(i, num_of_packets, step,0)
header_segments.append(header)
i=i+1
print("message_segments:", message_segments) #Prints the message in parts according to the maximum size that can be sent
print("")
#################
last_ack = -1 # Last acknowledged packet.
dupilcate_acks = 0 # Count of duplicate ACKs.
current_window_start = 0
current_window_end = min(window_size, len(message_segments))
last_new_msg_sent = -1 # Last new message sent.
# Adjustments for special cases.
step = 1 # default
if CASE_3:
current_window_start = 1
if CASE_4:
step = 2
# Initial packet transmission within the sliding window.
for i in range(current_window_start, current_window_end,step):
packege= header_segments[i]+message_segments[i].encode('utf-8') # Combine header and segment.
client_socket.send(packege)
last_new_msg_sent = i # Update last new message sent.
packet_number, _, _, _ = parse_header(header_segments[i])
print(f"Sending: M{packet_number}:{message_segments[i]}")
# Main loop for sendings segments, receiving ACKs and managing retransmissions.
while last_ack < len(message_segments)-1:
start_time = time.time() # Start timeout timer.
while time.time() - start_time < timeout: # Listen for incoming ACKs until timeout.
try:
acks_data = client_socket.recv(SIZE)
if acks_data != "":
while acks_data is not None and len(acks_data) >= HEADERSIZE: #extract acks from buffer(acks_data)
packet_number, total_packets, bytes_to_read, ack = parse_header(acks_data[:HEADERSIZE])
if ack == (2 ** 32)-1: # handle the case that the M0 didn't send
ack = -1
acks_data = acks_data[HEADERSIZE:] # Move to the next portion of the data.
print(f"Received: ACK{ack}\n")
# Handling duplicate ACKs(until 3).
# if there is duplicate ack, it's resending the next packet from the last ack
if last_ack == ack:
dupilcate_acks += 1
if dupilcate_acks == 3:
print("We got 3 dupilcate acks! ")
packege = header_segments[last_ack+1] + message_segments[last_ack+1].encode('utf-8')
client_socket.send(packege)
print(f"Resending: M{last_ack+1}:{message_segments[last_ack+1]}")
dupilcate_acks = 0
# If a new ACK is received.
else:
last_ack = ack
dupilcate_acks = 0
current_window_start = last_ack + 1
current_window_end = min(current_window_start + window_size, len(message_segments))
for i in range(last_new_msg_sent+1, current_window_end): # Sending new packets within the updated window.
packege = header_segments[i] + message_segments[i].encode('utf-8')
client_socket.send(packege)
start_time = time.time() # Reset timeout timer.
last_new_msg_sent = i
print(f"Sending: M{i}:{message_segments[i]}")
except Exception as e:
break
# Handling timeout (not finish to send all the packet): retransmitting all packets in the current window.
if last_ack < len(message_segments)-1 and time.time() - start_time >= timeout:
print("Timeout occurred, resending all message in the window...\n")
for i in range(current_window_start, current_window_end):
packege = header_segments[i] + message_segments[i].encode('utf-8')
client_socket.send(packege)
last_new_msg_sent = i
print(f"Sending: M{i}:{message_segments[i]}")
# Gradually increasing the timeout for subsequent attempts.
timeout = min(timeout + 1, 5)
client_socket.settimeout(timeout)
print("")
print(f"The new timeout is {timeout}s\n")
# Completion of the process.
print("")
if last_ack == len(message_segments)-1:
print("All the ACK messages received , the massage pass successfully")
else:
print("The server failed in the processing")
"""
This function gets all parameters to send to server a message by using "send_message" function the server and sending the message.
- Establishes a TCP connection with the server.
- Loads configuration from a file or prompts the user.
- Calls `send_message` to transmit the message.
- Closes the connection upon completion.
**Parameters**:
- `config_file` (str, optional): Path to the configuration file.
"""
def client(config_file=None) -> None:
# connection to srever
server_prefix = f"{{{HOST}:{PORT}}}"
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))
print(f"{server_prefix} Connection established")
# config by file text or by the user (the server send max_msg_size)
if config_file:
config = read_input_file(config_file)
else:
print("Now the server need to enter what is the max size of bytes that he can receive")
client_socket.send('MAX_MSG_SIZE_REQUEST'.encode())
maximum_msg_size = int(client_socket.recv(SIZE).decode())
print(f"The server sent that the maximum_msg_size is: {maximum_msg_size}")
window_size = int(input("Enter the window size: "))
timeout = float(input("Enter the timeout in seconds: "))
message = input("Enter the message to send: ")
config = {'message': message, 'maximum_msg_size': maximum_msg_size, 'window_size': window_size,
'timeout': timeout}
# Set the timeout for the client socket
client_socket.settimeout(float(config['timeout']))
# Send the message using the send_message function
send_message(client_socket, config['message'], int(config['maximum_msg_size']), int(config['window_size']),
float(config['timeout']))
print("Connection closed")
client_socket.close()
"""
This is the main entry point for the client. It performs the following actions:
1. Displays a menu to the user to select one of the test cases.
2. Sets the appropriate flags (`CASE_3` or `CASE_4`) based on the user's input.
3. Prompts the user to input a configuration file name (if any). If the file exists, it's read and passed to the `client` function.
If not, user need to input manual in `client` function.
-Ensures the inputs (text file name) exist in the folder.
"""
if __name__ == '__main__':
a = int(menu())
if a == 3:
CASE_3 = True
if a == 4:
CASE_4 = True
while True:
config_file = input("Enter the text file name to read (for example: text2), or press Enter to enter manual: \n")
if config_file:
try:
with open(config_file, 'r') as file:
print(file.read())
client(config_file)
break
except FileNotFoundError:
print("The file does not exist. Please try again.")
else:
client() # Call client function without a file
break