Welcome to my blog’s first tutorial series!
How you doing today?
I hope you’re well because this video is about to bang you on the head!
Not in a bad way of course, we’re about to learn a lot of cool stuff; but, I must warn you, grab your drink in advance and strap in that chair. 🙂
In this episode, as promised, we’re going to merge both the server and client script into one and add a bunch of functionality as well.
Analysis Before Coding
Since there’s a lot of ground to cover, let’s take a deep breath first and think about some components before diving into the code.
In the last part, we concluded the server thread and I mentioned we would work on the client thread this time. Which we will. However, before that we must port over some functions from the client script into the server script.
For example, the connect back functionality from the client script will be ported over and improved upon to allow the user to either start a new connection or be used programmatically in the main server.
Only then we’ll be able to consider firing up those client threads…
Now about the main server: we’ll have to place it inside its own function. The main reason is so we can run it in the background as a thread. This will allow us to shut it down once we finish establishing a connection and quickly fire it up again to listen for the next client.
Finally we’ll implement the text-based user interface since all of our main components are now running in the background as daemons: the server threads, the client threads and the main server.
I think that’s enough context for now so let’s jump into some code!
Dive Into The Code
I will go into the main components we talked about above.
# Function to connect to a client def Connect(clientIP, clientPort, ptav): if len(clientIP) > 0: CLIENT = clientIP else: CLIENT = raw_input("\n[" + GetTime() + "] What address are you connecting to? ") if clientPort: PORT = clientPort else: PORT = raw_input("\n[" + GetTime() + "] What port are you connecting to? ") # Allocate a new port if ptav = 0 if ptav == 0: AllocAddress() time.sleep(interval) ptav = PTAV s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((CLIENT, int(PORT))) time.sleep(interval) Send(s, "User " + USER) time.sleep(interval*2) Send(s, "port " + str(ptav))
Here is the connect() function which includes some lines of code from the client script (mainly the user prompts for address and port). At the same time we are sending the username and available port (to receive the client thread). Note how this function allows arguments so that it can be called by the main server but also allows the user to initiate a connection.
# Client Handler def ClientHandler(clientIP, clientPort): ps = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ps.connect((str(clientIP), int(clientPort))) time.sleep(interval) Send(ps, "Finally we are on the private chat.") socks.append(ps)
Onto the client handler, we simply put the connect back code inside of a function and send a message to signalize that once the client thread is connected we are basically finished establishing the connection.
Let’s jump into some of the text-based user interface components now:
# Main control screen (text user interface) def Refresh(): clear() print ' - Python Chat Server\n' print '\nWelcome ' + USER + '! ' + str(len(clients)) + ' Clients active;\n' print '\nListening for clients...\n' if len(clients) > 0: for j in range(0, len(clients)): print '[' + str((j+3)) + '] Client: ' + clients[j] + '\n' else: print '...\n' print '---\n' print ' Connect \n' print ' Status \n' print ' Exit \n' print '\nPress Ctrl+C to interact.' print '\n------------------------------\n'
This function will simply output the text-based user interface, along with the options for our user to interact with – by pressing Ctrl+C.
What are those options?
The user can press 1 to initiate a new connection, which will basically fire up the Connect() function mentioned previously.
Option 2 will print all the system logs and connection status. This function was mostly used by me when programming the code initially to see if all the threads were opening properly among other details.
When pressing 0 we’ll go ahead and exit out of the script.
Besides those options, the user can press any number greater than 2 to begin interacting with a previously established connection.
# Show status of connections/threads def StatusCon(): clear() print ' - Python Chat Server\n' print '\n------------------------------\n' print ' Servers \n' for x in servers: print str(x) + '\n' print ' Clients \n' for y in clients: print str(y) + '\n' print ' Sockets \n' for s in socks: print str(s) print '\n------------------------------\n' for l in logtext: print l time.sleep(interval*14)
This is the status connection function mentioned above (option 2), we simply go through all of our lists and output the information.
# Main Server thread def MainServer(serverIP, serverPort): global MSVR # Initialize the server socket if CheckAddr(PORT, True): c = socket.socket(socket.AF_INET, socket.SOCK_STREAM) c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) c.bind((HOST, PORT)) c.listen(CLTS) # Listen for total of X clients printl("\n[" + GetTime() + "] Listening on " + HOST + " (" + str(PORT) + ") for a total of " + str(CLTS) + " clients.") else: printl("\n[" + GetTime() + "] Error - " + HOST + " (" + str(PORT) + ") is already in use.") # Check for IP and Port for next server thread # -> Will store value inside clients list. for f in clients: if f == PTAV: AllocAddress() # Main server begins here s,a = c.accept() if (s): printl("\n[" + GetTime() + "] Connection from " + str(a)) try: while True: msg = Receive(s) if len(str(msg)) and msg[:4] != "User": printl("\n[" + GetTime() + "] " + msg) # First part of negotiating the connection # We'll receive username and fire server thread if msg[:4] == "User": printl("\n[" + GetTime() + "] Negotiating private connection.") time.sleep(interval) printl("\n[" + GetTime() + "] Connecting back to client: " + str(a) + " (" + str(PORT) + ")") Connect(str(a), int(PORT), int(PTAV)) # Add client to the clients list ('username', IP, Port) clients.append((msg[-(len(msg)-5):], str(a), PTAV)) time.sleep(interval) # Let's start a new server thread to listen for single connection shtname = 'ServerT' + str(PTAV) printl("\n[" + GetTime() + "] Starting server thread " + shtname) sht = threading.Thread(name=shtname, target=ServerHandler, args=(HOST, PTAV, a)) servers.append(sht) sht.setDaemon(True) sht.start() time.sleep(interval) printl("\n[" + GetTime() + "] Server thread " + shtname + " open.") time.sleep(interval) # Second part, receive port to connect to # Fire up the client thread on that port. if msg[:4] == "port": # Start a new client thread chtname = 'ClientT' + str(msg[-5:]) printl("\n[" + GetTime() + "] Starting client thread " + chtname) cht = threading.Thread(name=chtname, target=ClientHandler, args=(str(a), int(msg[-5:]))) cht.setDaemon(True) cht.start() # Here we'll shutdown the main server thread try: s.shutdown(socket.SHUT_RDWR) s.close() printl("\n[" + GetTime() + "] Closing main server connection.") time.sleep(interval) MSVR = False sys.exit() except Exception: pass except KeyboardInterrupt: print ">>> Finished."
This scary beast here is definitely better explained through the video, regardless, I will do my best to break it down here.
There are three main factors here which weren’t covered in previous parts:
First, we have changed the initial condition to start interacting upon receiving the keyword “User”, which then fires a connection back to exchange information with the client (available port for server thread).
Second, once we receive the available port for the server thread, its time to initialize the client thread as a daemon, which will ultimately conclude the connection both ways. This concludes the main server’s job.
Lastly, we’ll be ready to shutdown the main server (so it can go back into listening mode). Notice that since we are now running it in the background we can simply exit the function to wrap it up.
# Main script loop (controls user interaction/active chat) while True: # Check on the main server thread if MSVR == False: mainServer = threading.Thread(name='mainServer', target=MainServer, args=(HOST, int(PORT))) mainServer.setDaemon(True) mainServer.start() MSVR = True try: Refresh() time.sleep(interval*6) except KeyboardInterrupt: act = raw_input("\nEnter action: ") if int(act) == 1: Connect('', '', 0) elif int(act) == 2: StatusCon() elif int(act) == 0: quit() elif int(act) > 2: # Enter chat mode act = int(int(act)-3) if len(socks): while True: ms = raw_input("\n[" + GetTime() + "] Enter message: ") if ms == 'quit': break else: Send(socks[act], ms) else: print '\nNo clients found.\n' time.sleep(interval*6) else: pass
Now that all of our components are running in the background, this block of code is essential to control interaction between user and the program.
At first we’ll check if we need to restart the main server. Then we’ll present the main text-based user interface (Refresh() function) which will take the input from the user and await further instructions.
We have while loop to interact with the client for now, but in the next two parts we’ll work on a separate user chat screen as shown in the overview.
Feel free to download the code, digest it slowly, compare it with previous scripts, perhaps try to induce some errors. Like always, feel free to ask any questions about a specific part you might have trouble with.
In the final part, we’ll be looking to try to correct some user input errors and work on closing some of the sockets and threads since I haven’t really dealt with that thus far. I know all of that is important but after that hour-long video marathon its just gonna have to wait. 😉
So that’s it guys, I’m heading out for a drink right now, it’s been a long one!