Module tcpcom
[frames] | no frames]

Source Code for Module tcpcom

  1  # tcpcom.py 
  2  # AP 
  3   
  4  ''' 
  5   This software is part of the TCPCom library. 
  6   It is Open Source Free Software, so you may 
  7   - run the code for any purpose 
  8   - study how the code works and adapt it to your needs 
  9   - integrate all or parts of the code in your own programs 
 10   - redistribute copies of the code 
 11   - improve the code and release your improvements to the public 
 12   However the use of the code is entirely your responsibility. 
 13   ''' 
 14   
 15  from threading import Thread 
 16  import thread 
 17  import socket 
 18  import time 
 19  import sys 
 20   
 21  TCPCOM_VERSION = "1.26 - Jan. 17, 2018" 
22 23 # ================================== Server ================================ 24 # ---------------------- class TimeoutThread ------------------------ 25 -class TimeoutThread(Thread):
26 - def __init__(self, server, timeout):
27 Thread.__init__(self) 28 self.server = server 29 self.timeout = timeout 30 self.count = 0
31
32 - def run(self):
33 TCPServer.debug("TimeoutThread starting") 34 self.isRunning = True 35 isTimeout = False 36 while self.isRunning: 37 time.sleep(0.01) 38 self.count += 1 39 if self.count == 100 * self.timeout: 40 self.isRunning = False 41 isTimeout = True 42 if isTimeout: 43 TCPServer.debug("TimeoutThread terminated with timeout") 44 self.server.disconnect() 45 else: 46 TCPServer.debug("TimeoutThread terminated without timeout")
47
48 - def reset(self):
49 self.count = 0
50
51 - def stop(self):
52 self.isRunning = False
53
54 # ---------------------- class TCPServer ------------------------ 55 -class TCPServer(Thread):
56 ''' 57 Class that represents a TCP socket based server. 58 ''' 59 isVerbose = False 60 PORT_IN_USE = "PORT_IN_USE" 61 CONNECTED = "CONNECTED" 62 LISTENING = "LISTENING" 63 TERMINATED = "TERMINATED" 64 MESSAGE = "MESSAGE" 65
66 - def __init__(self, port, stateChanged, endOfBlock = '\0', isVerbose = False):
67 ''' 68 Creates a TCP socket server that listens on TCP port 69 for a connecting client. The server runs in its own thread, so the 70 constructor returns immediately. State changes invoke the callback 71 onStateChanged(). 72 @param port: the IP port where to listen (0..65535) 73 @param stateChange: the callback function to register 74 @param endOfBlock: character indicating end of a data block (default: '\0') 75 @param isVerbose: if true, debug messages are written to System.out, default: False 76 ''' 77 Thread.__init__(self) 78 self.port = port 79 self.endOfBlock = endOfBlock 80 self.timeout = 0 81 self.stateChanged = stateChanged 82 TCPServer.isVerbose = isVerbose 83 self.isClientConnected = False 84 self.terminateServer = False 85 self.isServerRunning = False 86 self.start()
87
88 - def setTimeout(self, timeout):
89 ''' 90 Sets the maximum time (in seconds) to wait in blocking recv() for an incoming message. If the timeout is exceeded, the link to the client is disconnected. 91 (timeout <= 0: no timeout). 92 ''' 93 if timeout <= 0: 94 self.timeout = 0 95 else: 96 self.timeout = timeout
97
98 - def run(self):
99 TCPServer.debug("TCPServer thread started") 100 HOSTNAME = "" # Symbolic name meaning all available interfaces 101 self.conn = None 102 self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 103 self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # close port when process exits 104 TCPServer.debug("Socket created") 105 try: 106 self.serverSocket.bind((HOSTNAME, self.port)) 107 except socket.error as msg: 108 print "Fatal error while creating TCPServer: Bind failed.", msg[0], msg[1] 109 sys.exit() 110 try: 111 self.serverSocket.listen(10) 112 except: 113 print "Fatal error while creating TCPServer: Port", self.port, "already in use" 114 try: 115 self.stateChanged(TCPServer.PORT_IN_USE, str(self.port)) 116 except Exception, e: 117 print "Caught exception in TCPServer.PORT_IN_USE:", e 118 sys.exit() 119 120 try: 121 self.stateChanged(TCPServer.LISTENING, str(self.port)) 122 except Exception, e: 123 print "Caught exception in TCPServer.LISTENING:", e 124 125 self.isServerRunning = True 126 127 while True: 128 TCPServer.debug("Calling blocking accept()...") 129 conn, self.addr = self.serverSocket.accept() 130 if self.terminateServer: 131 self.conn = conn 132 break 133 if self.isClientConnected: 134 TCPServer.debug("Returning form blocking accept(). Client refused") 135 try: 136 conn.shutdown(socket.SHUT_RDWR) 137 except: 138 pass 139 conn.close() 140 continue 141 self.conn = conn 142 self.isClientConnected = True 143 self.socketHandler = ServerHandler(self, self.endOfBlock) 144 self.socketHandler.setDaemon(True) # necessary to terminate thread at program termination 145 self.socketHandler.start() 146 try: 147 self.stateChanged(TCPServer.CONNECTED, self.addr[0]) 148 except Exception, e: 149 print "Caught exception in TCPServer.CONNECTED:", e 150 self.conn.close() 151 self.serverSocket.close() 152 self.isClientConnected = False 153 try: 154 self.stateChanged(TCPServer.TERMINATED, "") 155 except Exception, e: 156 print "Caught exception in TCPServer.TERMINATED:", e 157 self.isServerRunning = False 158 TCPServer.debug("TCPServer thread terminated")
159
160 - def terminate(self):
161 ''' 162 Closes the connection and terminates the server thread. 163 Releases the IP port. 164 ''' 165 TCPServer.debug("Calling terminate()") 166 if not self.isServerRunning: 167 TCPServer.debug("Server not running") 168 return 169 self.terminateServer = True 170 TCPServer.debug("Disconnect by a dummy connection...") 171 if self.conn != None: 172 self.conn.close() 173 self.isClientConnected = False 174 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 175 client_socket.connect(('localhost', self.port)) # dummy connection to get out of accept()
176
177 - def disconnect(self):
178 ''' 179 Closes the connection with the client and enters 180 the LISTENING state 181 ''' 182 TCPServer.debug("Calling Server.disconnect()") 183 if self.isClientConnected: 184 self.isClientConnected = False 185 try: 186 self.stateChanged(TCPServer.LISTENING, str(self.port)) 187 except Exception, e: 188 print "Caught exception in TCPServer.LISTENING:", e 189 TCPServer.debug("Shutdown socket now") 190 try: 191 self.conn.shutdown(socket.SHUT_RDWR) 192 except: 193 pass 194 self.conn.close()
195
196 - def sendMessage(self, msg):
197 ''' 198 Sends the information msg to the client (as String, the character endOfBlock (defaut: ASCII 0) serves as end of 199 string indicator, it is transparently added and removed) 200 @param msg: the message to send 201 ''' 202 TCPServer.debug("sendMessage() with msg: " + msg) 203 if not self.isClientConnected: 204 TCPServer.debug("Not connected") 205 return 206 try: 207 self.conn.sendall(msg + self.endOfBlock) 208 except: 209 TCPClient.debug("Exception in sendMessage()")
210
211 - def isConnected(self):
212 ''' 213 Returns True, if a client is connected to the server. 214 @return: True, if the communication link is established 215 ''' 216 return self.isClientConnected
217
218 - def loopForever(self):
219 ''' 220 Blocks forever with little processor consumption until a keyboard interrupt is detected. 221 ''' 222 try: 223 while True: 224 time.sleep(1) 225 except KeyboardInterrupt: 226 pass 227 self.terminate()
228
229 - def isTerminated(self):
230 ''' 231 Returns True, if the server is in TERMINATED state. 232 @return: True, if the server thread is terminated 233 ''' 234 return self.terminateServer
235 236 @staticmethod
237 - def debug(msg):
238 if TCPServer.isVerbose: 239 print " TCPServer-> " + msg
240 241 @staticmethod
242 - def getVersion():
243 ''' 244 Returns the library version. 245 @return: the current version of the library 246 ''' 247 return TCPCOM_VERSION
248 249 @staticmethod 250 # Hack should work on all platforms
251 - def getIPAddress():
252 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 253 try: 254 # doesn't even have to be reachable 255 s.connect(('10.255.255.255', 1)) 256 IP = s.getsockname()[0] 257 except: 258 IP = '127.0.0.1' 259 finally: 260 s.close() 261 return IP
262
263 # ---------------------- class ServerHandler ------------------------ 264 -class ServerHandler(Thread):
265 - def __init__(self, server, endOfBlock):
266 Thread.__init__(self) 267 self.server = server 268 self.endOfBlock = endOfBlock
269
270 - def run(self):
271 TCPServer.debug("ServerHandler started") 272 timeoutThread = None 273 if self.server.timeout > 0: 274 timeoutThread = TimeoutThread(self.server, self.server.timeout) 275 timeoutThread.start() 276 bufSize = 4096 277 try: 278 while True: 279 data = "" 280 reply = "" 281 isRunning = True 282 while not reply[-1:] == self.endOfBlock: 283 TCPServer.debug("Calling blocking conn.recv()") 284 reply = self.server.conn.recv(bufSize) 285 TCPServer.debug("Returned from conn.recv() with " + str(reply)) 286 if reply == None or len(reply) == 0: # Client disconnected 287 TCPServer.debug("conn.recv() returned None") 288 isRunning = False 289 break 290 data += reply 291 if not isRunning: 292 break 293 TCPServer.debug("Received msg: " + data + "; len: " + str(len(data))) 294 junk = data.split(self.endOfBlock) # more than 1 message may be received if 295 # transfer is fast. data: xxxx<eob>yyyyy<eol>zzz<eob> 296 for i in range(len(junk) - 1): 297 try: 298 self.server.stateChanged(TCPServer.MESSAGE, junk[i]) # eol is not included 299 except Exception, e: 300 print "Caught exception in TCPServer.MESSAGE:", e 301 if not self.server.isClientConnected: # Callback disconnected the client 302 if timeoutThread != None: 303 timeoutThread.stop() 304 TCPServer.debug("Callback disconnected client. ServerHandler terminated") 305 return 306 if timeoutThread != None: 307 timeoutThread.reset() 308 except: # May happen if client peer is resetted 309 TCPServer.debug("Exception from blocking conn.recv(), Msg: " + str(sys.exc_info()[0]) + \ 310 " at line # " + str(sys.exc_info()[-1].tb_lineno)) 311 self.server.disconnect() 312 if timeoutThread != None: 313 timeoutThread.stop() 314 TCPServer.debug("ServerHandler terminated")
315
316 317 # ================================== Client ================================ 318 # -------------------------------- class TCPClient -------------------------- 319 -class TCPClient():
320 ''' 321 Class that represents a TCP socket based client. 322 ''' 323 isVerbose = False 324 CONNECTING = "CONNECTING" 325 SERVER_OCCUPIED = "SERVER_OCCUPIED" 326 CONNECTION_FAILED = "CONNECTION_FAILED" 327 CONNECTED = "CONNECTED" 328 DISCONNECTED = "DISCONNECTED" 329 MESSAGE = "MESSAGE" 330
331 - def __init__(self, ipAddress, port, stateChanged, isVerbose = False):
332 ''' 333 Creates a TCP socket client prepared for a connection with a 334 TCPServer at given address and port. 335 @param host: the IP address of the host 336 @param port: the IP port where to listen (0..65535) 337 @param stateChanged: the callback function to register 338 @param isVerbose: if true, debug messages are written to System.out 339 ''' 340 self.isClientConnected = False 341 self.isClientConnecting = False 342 self.ipAddress = ipAddress 343 self.port = port 344 self.stateChanged = stateChanged 345 self.checkRefused = False 346 self.isRefused = False 347 348 TCPClient.isVerbose = isVerbose
349
350 - def sendMessage(self, msg, responseTime = 0):
351 ''' 352 Sends the information msg to the server (as String, the character \0 353 (ASCII 0) serves as end of string indicator, it is transparently added 354 and removed). For responseTime > 0 the method blocks and waits 355 for maximum responseTime seconds for a server reply. 356 @param msg: the message to send 357 @param responseTime: the maximum time to wait for a server reply (in s) 358 @return: the message or null, if a timeout occured 359 ''' 360 TCPClient.debug("sendMessage() with msg = " + msg) 361 if not self.isClientConnected: 362 TCPClient.debug("sendMessage(): Connection closed.") 363 return None 364 reply = None 365 try: 366 msg += "\0"; # Append \0 367 rc = self.sock.sendall(msg) 368 if responseTime > 0: 369 reply = self._waitForReply(responseTime) # Blocking 370 except: 371 TCPClient.debug("Exception in sendMessage()") 372 self.disconnect() 373 374 return reply
375
376 - def _waitForReply(self, responseTime):
377 TCPClient.debug("Calling _waitForReply()") 378 self.receiverResponse = None 379 startTime = time.time() 380 while self.isClientConnected and self.receiverResponse == None and time.time() - startTime < responseTime: 381 time.sleep(0.01) 382 if self.receiverResponse == None: 383 TCPClient.debug("Timeout while waiting for reply") 384 else: 385 TCPClient.debug("Response = " + self.receiverResponse + " time elapsed: " + str(int(1000 * (time.time() - startTime))) + " ms") 386 return self.receiverResponse
387
388 - def connect(self, timeout = 0):
389 ''' 390 Creates a connection to the server (blocking until timeout). 391 @param timeout: the maximum time (in s) for the connection trial (0: for default timeout) 392 @return: True, if the connection is established; False, if the server 393 is not available or occupied 394 ''' 395 if timeout == 0: 396 timeout = None 397 try: 398 self.stateChanged(TCPClient.CONNECTING, self.ipAddress + ":" + str(self.port)) 399 except Exception, e: 400 print "Caught exception in TCPClient.CONNECTING:", e 401 try: 402 self.isClientConnecting = True 403 host = (self.ipAddress, self.port) 404 if self.ipAddress == "localhost" or self.ipAddress == "127.0.0.1": 405 timeout = None # do not use timeout for local host, to avoid error message "java.net..." 406 self.sock = socket.create_connection(host, timeout) 407 self.sock.settimeout(None) 408 self.isClientConnecting = False 409 self.isClientConnected = True 410 except: 411 self.isClientConnecting = False 412 try: 413 self.stateChanged(TCPClient.CONNECTION_FAILED, self.ipAddress + ":" + str(self.port)) 414 except Exception, e: 415 print "Caught exception in TCPClient.CONNECTION_FAILED:", e 416 TCPClient.debug("Connection failed.") 417 return False 418 ClientHandler(self) 419 420 # Check if connection is refused 421 self.checkRefused = True 422 self.isRefused = False 423 startTime = time.time() 424 while time.time() - startTime < 2 and not self.isRefused: 425 time.sleep(0.001) 426 if self.isRefused: 427 TCPClient.debug("Connection refused") 428 try: 429 self.stateChanged(TCPClient.SERVER_OCCUPIED, self.ipAddress + ":" + str(self.port)) 430 except Exception, e: 431 print "Caught exception in TCPClient.SERVER_OCCUPIED:", e 432 return False 433 434 try: 435 self.stateChanged(TCPClient.CONNECTED, self.ipAddress + ":" + str(self.port)) 436 except Exception, e: 437 print "Caught exception in TCPClient.CONNECTED:", e 438 TCPClient.debug("Successfully connected") 439 return True
440
441 - def disconnect(self):
442 ''' 443 Closes the connection with the server. 444 ''' 445 TCPClient.debug("Client.disconnect()") 446 if not self.isClientConnected: 447 TCPClient.debug("Connection already closed") 448 return 449 self.isClientConnected = False 450 TCPClient.debug("Closing socket") 451 try: # catch Exception "transport endpoint is not connected" 452 self.sock.shutdown(socket.SHUT_RDWR) 453 except: 454 pass 455 self.sock.close()
456
457 - def isConnecting(self):
458 ''' 459 Returns True during a connection trial. 460 @return: True, while the client tries to connect 461 ''' 462 return self.isClientConnecting
463
464 - def isConnected(self):
465 ''' 466 Returns True of client is connnected to the server. 467 @return: True, if the connection is established 468 ''' 469 return self.isClientConnected
470 471 @staticmethod
472 - def debug(msg):
473 if TCPClient.isVerbose: 474 print " TCPClient-> " + msg
475 476 @staticmethod
477 - def getVersion():
478 ''' 479 Returns the library version. 480 @return: the current version of the library 481 ''' 482 return TCPCOM_VERSION
483
484 # -------------------------------- class ClientHandler --------------------------- 485 -class ClientHandler(Thread):
486 - def __init__(self, client):
487 Thread.__init__(self) 488 self.client = client 489 self.start()
490
491 - def run(self):
492 TCPClient.debug("ClientHandler thread started") 493 while True: 494 try: 495 junk = self.readResponse().split("\0") 496 # more than 1 message may be received 497 # if transfer is fast. data: xxxx\0yyyyy\0zzz\0 498 for i in range(len(junk) - 1): 499 try: 500 self.client.stateChanged(TCPClient.MESSAGE, junk[i]) 501 except Exception, e: 502 print "Caught exception in TCPClient.MESSAGE:", e 503 except: 504 TCPClient.debug("Exception in readResponse() Msg: " + str(sys.exc_info()[0]) + \ 505 " at line # " + str(sys.exc_info()[-1].tb_lineno)) 506 if self.client.checkRefused: 507 self.client.isRefused = True 508 break 509 try: 510 self.client.stateChanged(TCPClient.DISCONNECTED, "") 511 except Exception, e: 512 print "Caught exception in TCPClient.DISCONNECTED:", e 513 TCPClient.debug("ClientHandler thread terminated")
514
515 - def readResponse(self):
516 TCPClient.debug("Calling readResponse") 517 bufSize = 4096 518 data = "" 519 while not data[-1:] == "\0": 520 try: 521 reply = self.client.sock.recv(bufSize) # blocking 522 if len(reply) == 0: 523 TCPClient.debug("recv returns null length") 524 raise Exception("recv returns null length") 525 except: 526 TCPClient.debug("Exception from blocking conn.recv(), Msg: " + str(sys.exc_info()[0]) + \ 527 " at line # " + str(sys.exc_info()[-1].tb_lineno)) 528 raise Exception("Exception from blocking sock.recv()") 529 data += reply 530 self.receiverResponse = data[:-1] 531 return data
532
533 534 # -------------------------------- class HTTPServer -------------------------- 535 -class HTTPServer(TCPServer):
536
537 - def getHeader1(self):
538 return "HTTP/1.1 501 OK\r\nServer: " + self.serverName + "\r\nConnection: Closed\r\n"
539
540 - def getHeader2(self):
541 return "HTTP/1.1 200 OK\r\nServer: " + self.serverName + "\r\nContent-Length: %d\r\nContent-Type: text/html\r\nConnection: Closed\r\n\r\n"
542
543 - def onStop(self):
544 self.terminate()
545
546 - def __init__(self, requestHandler, serverName = "PYSERVER", port = 80, isVerbose = False):
547 ''' 548 549 Creates a HTTP server (inherited from TCPServer) that listens for a connecting client on given port (default = 80). 550 Starts a thread that handles and returns HTTP GET requests. The HTTP respoonse header reports the given server name 551 (default: "PYSERVER") 552 553 requestHandler() is a callback function called when a GET request is received. 554 Signature: msg, stateHandler = requestHandler(clientIP, filename, params) 555 556 Parameters: 557 clientIP: the client's IP address in dotted format 558 filename: the requested filename with preceeding '/' 559 params: a tuple with format: ((param_key1, param_value1), (param_key2, param_value2), ...) (all items are strings) 560 561 Return values: 562 msg: the HTTP text response (the header is automatically created) 563 stateHandler: a callback function that is invoked immediately after the reponse is sent. 564 If stateHandler = None, nothing is done. The function may include longer lasting server 565 actions or a wait time, if sensors are not immediately ready for a new measurement. 566 567 Call terminate() to stop the server. The connection is closed by the server at the end of each response. If the client connects, 568 but does not send a request within 5 seconds, the connection is closed by the server. 569 ''' 570 try: 571 registerStopFunction(self.onStop) 572 except: 573 pass # registerStopFunction not defined (e.g. on Raspberry Pi) 574 575 TCPServer.__init__(self, port, stateChanged = self.onStateChanged, endOfBlock = '\n', isVerbose = isVerbose) 576 self.serverName = serverName 577 self.requestHandler = requestHandler 578 self.port = port 579 self.verbose = isVerbose 580 self.timeout = 5 581 self.clientIP = ""
582
583 - def getClientIP(self):
584 ''' 585 Returns the dotted IP of a connected client. If no client is connected, returns empty string. 586 ''' 587 return self.clientIP
588
589 - def onStateChanged(self, state, msg):
590 if state == "CONNECTED": 591 self.clientIP = msg 592 self.debug("Client " + msg + " connected.") 593 elif state == "DISCONNECTED": 594 self.clientIP = "" 595 self.debug("Client disconnected.") 596 elif state == "LISTENING": 597 self.clientIP = "" 598 self.debug("LISTENING") 599 elif state == "MESSAGE": 600 self.debug("request: " + msg) 601 if len(msg) != 0: 602 filename, params = self._parseURL(msg) 603 if filename == None: 604 self.sendMessage(self.getHeader1()) 605 else: 606 text, stateHandler = self.requestHandler(self.clientIP, filename, params) 607 self.sendMessage(self.getHeader2() % (len(text))) 608 self.sendMessage(text) 609 if stateHandler != None: 610 try: 611 stateHandler() 612 except: 613 print "Exception in stateHandler()" 614 else: 615 self.sendMessage(self.getHeader1()) 616 self.disconnect()
617
618 - def _parseURL(self, request):
619 lines = request.split('\n') # split lines 620 params = [] 621 for line in lines: 622 if line[0:4] == 'GET ': # only GET request 623 url = line.split()[1].strip() # split at spaces and take second item 624 i = url.find('?') # check for params 625 if i != -1: # params given 626 filename = url[0:i].strip() # include leading / 627 params = [] 628 urlParam = url[i + 1:] 629 for item in urlParam.split('&'): # split parameters 630 i = item.find('=') 631 key = item[0:i] 632 value = item[i+1:] 633 params.append((key, value)) 634 return filename, tuple(params) 635 return url.strip(), tuple([]) 636 return None, tuple([])
637
638 - def debug(self, msg):
639 if self.verbose: 640 print(" HTTPServer-> " + msg)
641 642 @staticmethod
643 - def getServerIP():
644 ''' 645 Returns the server's IP address (static method). 646 ''' 647 return TCPServer.getIPAddress()
648