[ Part 1 (Overview)Part 2Part 3Part 4Part 5Part 6 – Part 7 ]

Welcome to my blog’s first tutorial series!

Check out the video for details on the code!

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

Just like the previous part, there are so many lines of code being added that it would be too cumbersome to explain everything here – the video this time is actually over one hour long, give or take.

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][0] + '\n'
	else:
		print '...\n'
	print '---\n'
	print '[1] Connect \n'
	print '[2] Status \n'
	print '[0] 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 '[1] Servers \n'
	for x in servers:
		print str(x) + '\n'
	print '[2] Clients \n'
	for y in clients:
		print str(y) + '\n'
	print '[3] 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[2] == 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[0]), int(PORT), int(PTAV))
					# Add client to the clients list ('username', IP, Port)
					clients.append((msg[-(len(msg)-5):], str(a[0]), 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[0]))
					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[0]), 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.

Download Code

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!

Cheers!

Share: