-
Notifications
You must be signed in to change notification settings - Fork 0
/
ServerNetworking.py
304 lines (246 loc) · 9.76 KB
/
ServerNetworking.py
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
import socket
import threading
from datetime import datetime
import json
from time import time
class Server:
"""
Use this class to set up an server.
"""
serverActive = False
# connectedClients model
# {
# clientIpAddress: {
# "address": address:(ipAddress, port),
# "connection": connObj
# }
# }
connectedClients = {}
# standart Properties
encoding = "utf-8"
disconnectMessage = "!DISCONNECT"
# all Listeners
__recvListener = []
__onConnectListener = []
__onDisconnectListener = []
def __init__(self, host: str = socket.gethostbyname(socket.gethostname()), port: int = 5000, maxClients: int = 1, standartBufferSize: int = 64, messageTerminatorChar: str = "|") -> None:
# the messageTerminatorChar has to be just one char long
if not len(messageTerminatorChar) == 1:
raise Exception(
"messageTerminatorChar should be one char that dont ever exists in your messages that you want to send!")
self.address = (host, port)
self.maxClients = maxClients
self.standartBufferSize = standartBufferSize
self.messageTerminatorChar = messageTerminatorChar
def start(self) -> None:
"""
Start the server instance so that clients can join.
Return bool if server is started.
"""
self.__logMessage("Server is starting...")
# create the socket and make it ready to connect to
self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.serverSocket.bind(self.address)
self.serverSocket.listen(self.maxClients)
except:
return False
self.serverActive = True
threading.Thread(target=self.__handleNewConnections).start()
self.__logMessage(f"Server is listening on {self.address}")
return True
def stop(self):
"""
Stop the server and disconnect all clients.
"""
# send the diconnect message and close connection
self.send(self.disconnectMessage)
self.serverActive = False
t = time()
while time() - t < 3:
pass
self.serverSocket.close()
self.__logMessage("Server is stopped!")
def send(self, message: dict = {}, clients: list = []):
"""
Send a dict to the clients.
message should be a dict.
clients is a list of addresses to send the data. When empty then send to all clients.
"""
if len(self.connectedClients) == 0:
return
# get the clients the message should be send to
clientsToSend = []
connectedClients = dict(self.connectedClients)
if clients == []:
clientsToSend = [address for address in connectedClients]
else:
clientsToSend = [
address for address in connectedClients if address in clients]
# when message type is a dict then convert to json string when it is str then it is the diconnect message
if type(message) == dict:
message = json.dumps(message)
else:
message = str(message)
# append the messageTerminatorChar to the end
message = f"{message}{self.messageTerminatorChar}"
# calculate the send lenght and convert it to binary
msg = message.encode(self.encoding)
msgLen = len(msg)
# append the messageTerminatorChar to the end of the message
sendLen = f"{str(msgLen)}{self.messageTerminatorChar}".encode(
self.encoding)
sendLen += b' ' * (self.standartBufferSize - len(sendLen))
# for each address that should get the message send the lenght of the message and then the message
clientAdresses = []
for address in clientsToSend:
self.connectedClients[address]["connection"].send(sendLen)
self.connectedClients[address]["connection"].send(msg)
clientAdresses.append(address)
self.__logMessage(f"Sended message to {clientAdresses}")
def getAllClients(self) -> list:
"""
Get all clients currently connected.
"""
# copy the dict so that no error occures when writing something while iterating
connectedClients = dict(self.connectedClients)
# list comprehension to return all client addresses
return [address for address in connectedClients]
def __handleNewConnections(self):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__handleNewConnections wait for an incoming connection.
"""
while self.serverActive:
try:
conn, address = self.serverSocket.accept()
except:
continue
threading.Thread(target=self.__handleClient,
args=(conn, address)).start()
def __handleClient(self, connection: socket.socket, address):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__handleClient handle the client.
"""
# add the client to the list
self.connectedClients[address[0]] = {
"address": address, "connection": connection}
self.__logMessage(f"Got connection from {address}")
self.__logMessage(f"Active connections {len(self.connectedClients)}")
self.__fireOnConnect(address)
while address[0] in self.connectedClients:
# this is in try except because if client diconnects then it would throw an error but like this it obviously doesnt
try:
# receive the message lenght while the messageTerminatorChar is not present
msgLen = ""
while not self.messageTerminatorChar in msgLen:
msgLen += connection.recv(
self.standartBufferSize).decode(self.encoding)
# remove the messageTerminatorChar
msgLen = msgLen.replace(self.messageTerminatorChar, "")
except:
continue
if msgLen == "":
continue
try:
msgLen = int(msgLen)
except:
self.__logMessage(
f"Invalid message lenght from {address[0]}!\n{msgLen}")
continue
# this is in try except because if client diconnects then it would throw an error but like this it obviously doesnt
try:
# receive the message lenght while the messageTerminatorChar is not present
msg = ""
while not self.messageTerminatorChar in msg:
msg += connection.recv(msgLen).decode(self.encoding)
# remove the messageTerminatorChar
msg = msg.replace(self.messageTerminatorChar, "")
except:
continue
self.__logMessage(f"Got message from {address[0]}!")
# check if message is an system message
if msg == self.disconnectMessage:
self.connectedClients.pop(address[0])
continue
# load the message to a dict
try:
msg = json.loads(msg)
except:
self.__logMessage(
f"Message from {address[0]} could not be handled!\n{msg}")
continue
self.__fireOnRecv(msg, address)
connection.close()
self.__fireOnDisconnect(address)
self.__logMessage(f"{address[0]} disconnected from the server!")
self.__logMessage(f"Active connections {len(self.connectedClients)}")
#
# decorators
#
def onRecv(self, func):
"""
This decorator returns every received message.
@Server.onRecv
def onRecv(message: dict, address: tuple):
# code
"""
if func in self.__recvListener:
return
self.__recvListener.append(func)
def __fireOnRecv(self, msg: dict, address: tuple | None = None):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__fireRecv calls every function that used the onRecv decorator and gives the msg as an argument.
"""
for func in self.__recvListener:
threading.Thread(target=func, args=[msg, address]).start()
def onConnect(self, func):
"""
This decorator calls the function when a client connects.
@Server.onConnect
def onConnect(address: tuple):
# code
"""
if func in self.__onConnectListener:
return
self.__onConnectListener.append(func)
def __fireOnConnect(self, address: tuple):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__fireOnConnect calls every function that uses the onConnect decorator.
"""
for func in self.__onConnectListener:
threading.Thread(target=func, args=[address]).start()
def onDisconnect(self, func):
"""
This decorator calls the function when a client disconnects.
@Server.onDisconnect
def onDisconnect(address: tuple):
# code
"""
if func in self.__onDisconnectListener:
return
self.__onDisconnectListener.append(func)
def __fireOnDisconnect(self, address: tuple):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__fireOnDisconnect calls every function that uses the onDisconnect decorator.
"""
for func in self.__onDisconnectListener:
threading.Thread(target=func, args=[address]).start()
def __logMessage(self, msg):
"""
DO NOT USE OUTSIDE OF SERVER CLASS
-----
__logMessage print a log message.
"""
now = datetime.now()
currTime = now.strftime("%Y-%m-%d, %H:%M:%S")
print(f"[{currTime}] {msg}")