Multi-User Chat Client and Server
#39 server sub we' client end control connection
I've done a little investigation on the web and as it turns out it's very easy to find applications which describe client server interaction between a single client and single server. It's NOT so easy to find examples of chat applications that cater for MULTIPLE connections to the same machine. I'm talking specifically now about CHAT applications. What's the point of building a communication application if only 2 people can communicate. We want EVERYONE involved. So I decided to throw some code together to see what's involved in building this thing.
What's a chat application? Well it can be many things. When I think of a chat I think of Yahoo Messenger, AOL instant messenger or IRC. For anyone who's used NetMeeting you'll know you can also draw, type etc and send information such as files back and forward. That's all pretty advanced but built around a few simple concepts. For our purposes I'll try to keep things simple and just deal with text. Later on we'll discuss how you'd expand the applications we're going to build here to incorporate more advanced exchanges such as drawings etc.. As with all the Cornflake Articles, this project should lay the ground work for greater things to come. When you're done reading this be sure to send me some feedback on it. I'd love to hear how I can improve my writing style or explanations etc. Thanks now let's get going.
First of all we're going to need a strategy as to how we want communications to flow. I've already mentioned the words client and server but let's define those a little better.
The client is the application that the users' will use to connect to the server. The server is the application that will host the chat session and that all users will connect to. The communication will run through the system like so:
A socket is a method of connecting to a physical machine through it's network connection. We need an IP address and a Port number on that IP address. Ports are important in that certain things such as firewalls, routers and switches all can block or allow traffic on certain ports. For instance most webservers are hosted on port 80 of whatever machine they're running on. This doesn't have to be the case though, a webserver could run on any port so long as it's available. And that's the key word here, "available". Similarly, we'll need to chose a port to host our server on and ensure no other application is using it. For that reason I won't chose port 80, I'll pick a random port (1212). How do I know that's available? Answer: I don't until I try to start up the server I'm going to build. There's a virtually infinite number of ports I can pick so if this one is not available, I'll just pick another.
First we need to add the Winsock control to our list of components. Click "Project->Components" and select "Microsoft Winsock Control 6.0" from the list. That should add the following control to your toolbox:
The code for the startup form is really simple. It just sets the IP address and port for the server or client to listen on or connect to. It then shows the client or server depending on the button the users' pressed. It looks like this:
Option Explicit Private Sub SetAddress() gPort = txtPort.Text gIPAddress = txtIPAddress.Text End Sub Private Sub cmdClient_Click() Call SetAddress frmClient.Show Unload Me End Sub Private Sub cmdServer_Click() Call SetAddress frmServer.Show Unload Me End SubLet's note here that I'm using Option Explicit.
The first thing we're going to do is start our server Listening. We'll do this in the Form load event. Our server is going to listen on a specific port for our clients who want to connect. Here's the code for that:
Private Sub Form_Load() ' Only connection(0) listens, all others are connections sktConnection(0).Close sktConnection(0).LocalPort = gPort sktConnection(0).Listen ReDim gHandles(1) As String gHandles(0) = "Cornflake Server" AddToServerLog ("Server is Listening on port " & gPort) End SubOk so let's assume a connection is requested. For this we'll need to code the ConnectionRequest event. The most interesting and difficult thing to get right when coding any kind of network application is the handshake. Just as you'd shake someone's hand when you meet them in the street, our client must shake hands with the server to introduce itself. When our client says hello, we'll say hello back and ask the user to tell us what their name or "handle" is. We'll also do the following:
a) create an additional winsock control in the server
b) assign it a random port (since it can't share the same port as the server) and
c) accept the new connection on that control.
d) We'll also assign a "handle" to that user, add them to the listed connections on the server.
We do this by putting the following code in the connection request event.
Private Sub sktConnection_ConnectionRequest(Index As Integer, ByVal requestID As Long) ' Now accept the new connection 'A connection was requested from the server. Dim i As Integer Dim iConnection As Integer 'Make sure this is control 0 in the array. 'This is the only one that can accept connections. If Index = 0 Then 'Search for available Winsock control. For i = 1 To gNumConnections If sktConnection(i).State = sckClosed Then iConnection = i Exit For End If Next i 'If none was found, create a new one. If iConnection = 0 Then ' Tell the world there's a new connection gNumConnections = gNumConnections + 1 'Load a new Winsock control for this connection. Load sktConnection(gNumConnections) ' This connection needs a handle ReDim Preserve gHandles(gNumConnections) As String ReDim Preserve gSentYN(gNumConnections) As Boolean ReDim Preserve gMessages(gNumConnections) As String ' Catch this user up on the previous conversation ' This way they don't get resent the chat session to date gMessages(gNumConnections) = gTotalincoming ' set their handle gHandles(gNumConnections) = "unknown" ' Add to the servers connections lblActiveConnections.Caption = gNumConnections & " Active Connections" iConnection = gNumConnections End If 'Set port for this control to 0. (Randomly assigns an available port.) sktConnection(iConnection).LocalPort = 0 'Have this control accept the connection. sktConnection(iConnection).Accept requestID ' Send the welcome message sktConnection(iConnection).SendData "Welcome to the CornflakeZone, enter your handle" End If End SubOk so now the users' received a request to input their username/handle. Let's imagine that the client did that. We'd get a DataArrival event triggered on that client's server side winsock control. VB tells us which control this is by filling in the "index" parameter for us. To complete the "handshake", I've added some logic to state that if the user's handle is unknown then the data arriving must be their handle so assign it.
If the handle is assigned then the data arriving must be a part of the conversation so we add it to the global record of the conversation. I've coded some simple functions to do these tasks for me. Here's the DataArrival code.
Private Sub sktConnection_DataArrival(Index As Integer, ByVal bytesTotal As Long) 'Data has arrived at the server from an open connection. Dim newdata As String 'Get the data. sktConnection(Index).GetData newdata If gHandles(Index) = "unknown" Then ' Store it internally gHandles(Index) = newdata ' Announce the new arrival AddToTotalIncoming (newdata & " just joined") Else 'Pass the index of the connection from which the data came. AddToTotalIncoming (gHandles(Index) & ":: " & newdata) End If End SubOk, it's time to complete the server side code. Let's review. We have a way for the users to connect, we have a way for the server to accept data. We don't have a way to transmit the conversation to the users. That's what we'll code next. You saw in the form screen shots that we added a timer to the server. Double click on that control to create it's interval event. The server's responsibility is to transmit the conversation to the clients.
You might have noticed me referring to the "global record of the conversation". What I mean by this is that the server maintains the conversation to date. It would be inefficient of us to send the entire conversation to EVERY client EVERY time there's an update. That's just silly. What makes more sense is that if we just send the differences in the conversation to the client. We maintain a copy of the conversation that's been sent to each client so we can determine what the differences are for that client. I do this with the "Left()" function.
We'll send the conversation updates one at a time to each client. Every time the timer fires, we send one of the clients' the latest conversation updates. Let's maintain an array of "sent YN" flags to help us figure out which clients need an update. The timer function will fire each 100 or so milliseconds so a 10 user session will experience 1 second total lag time. Not bad for a homegrown solution. You can play around with the 100 milliseconds to find the value that works best for you.
Take a look at the code for the timer function.
Private Sub tmServerTimer_Timer() txtTotalIncoming.Text = gTotalincoming 'Send the new data to the group Call BroadcastMessage End Sub Private Sub BroadcastMessage() Dim i Dim myMessage As String Dim conn_index As Integer ' Set the default connection index conn_index = -1 ' Loop through all connections excluding the servers ' Hence we start at 1 For i = 1 To gNumConnections ' if this connection has not had an update then If gSentYN(i) = False And sktConnection(i).State = sckConnected Then ' Get the message we need to deliver myMessage = Left(gTotalincoming, Len(gTotalincoming) - Len(gMessages(i))) ' update the message store for this user gMessages(i) = gTotalincoming ' Get the index of this connection conn_index = i Exit For End If Next i ' This is so we know to ' send the data to everyone If conn_index = -1 Then For i = 1 To gNumConnections gSentYN(i) = False Next End If If conn_index > -1 Then ' check the connection's open If sktConnection(conn_index).State = sckConnected Then ' send the data sktConnection(conn_index).SendData myMessage 'signal that we've sent to this user gSentYN(conn_index) = True End If End If End SubOk, let's turn our attention to the client. This a much simpler little app. It handles the other side of the handshake and also accepts data and displays the chat session to the user. I've added a little button to re-connect if we'd like.
Private Sub sktClient_DataArrival(ByVal bytesTotal As Long) Dim newdata As String ' Get the arriving data and print it out. sktClient.GetData newdata ' add the data to the output txtOutput.Text = newdata & txtOutput.Text End Sub Private Sub sktClient_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean) ' This should handle any winsock errors. Call AddToOutput("Error: " & Description) End SubOk, that's it. If you'd like to test the application just download it and give it a shot. What's that you say? Only have one computer? That's ok, just type in localhost or 127.0.0.1. That'll connect the client to the server with both running on the same machine.
I hope you liked this tut and maybe learned a thing or two. I know I did. I didn't implement very much error checking in this app but you can easily add this and I felt it would get in the way of the code.
We implemented this in VB to keep things simple. Now that you understand how the multi-user thing works you're ready to move onto more challenging applications. Try a multi-user drawing session with a picture control. This could easily be achieved by sending the users' mouse coordinates instead of the text messages. You could then have the clients' draw what the other users are drawing! Now THAT's communicating.
If you'd like a REAL challenge try creating a COM object to wrap up the winsock object in VC++ (see last tutorial). I've already done this in VC++ and will post it in a couple of days. I do most of my coding in C++ and just found it more useful to implement the C++ object. As always, send me feedback on this article.