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:
In our application our server will have 1 connection for itself. This connection will be the socket for all other clients to "plug-into" or connect to. Woah, what's a socket?
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:
We'll use three forms in this project. The client, the server and a simple loading form. We'll create all forms and controls right now so that we can then concentrate on the code instead of the GUI.
My forms look like this but you can arrange them anyway you want. Notice the placement of the Winsock controls on the Server and the Clients. Make sure you create a control array for the Winsock control onthe server. Do this by typing "0" in the "index" field of the Server Winsock Control properties.
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:
Private Sub SetAddress()
gPort = txtPort.Text
gIPAddress = txtIPAddress.Text
Private Sub cmdClient_Click()
Private Sub cmdServer_Click()
End Sub[/code][/indent] Let'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:
[indent][code]Private Sub Form_Load()
' Only connection(0) listens, all others are connections
sktConnection(0).LocalPort = gPort
ReDim gHandles(1) As String
gHandles(0) = "Cornflake Server"
AddToServerLog ("Server is Listening on port " & gPort)
End Sub[/code][/indent] Ok 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.
[indent][code]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
'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.
' 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
'Set port for this control to 0. (Randomly assigns an available port.)
sktConnection(iConnection).LocalPort = 0
'Have this control accept the connection.
' Send the welcome message
sktConnection(iConnection).SendData "Welcome to the CornflakeZone, enter your handle"
End Sub[/code][/indent] Ok 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.
[indent][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.
If gHandles(Index) = "unknown" Then
' Store it internally
gHandles(Index) = newdata
' Announce the new arrival
AddToTotalIncoming (newdata & " just joined")
'Pass the index of the connection from which the data came.
AddToTotalIncoming (gHandles(Index) & ":: " & newdata)
End Sub[/code][/indent] Ok, 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.
[indent][code]Private Sub tmServerTimer_Timer()
txtTotalIncoming.Text = gTotalincoming
'Send the new data to the group
Private Sub BroadcastMessage()
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
' This is so we know to
' send the data to everyone
If conn_index = -1 Then
For i = 1 To gNumConnections
gSentYN(i) = False
If conn_index > -1 Then
' check the connection's open
If sktConnection(conn_index).State = sckConnected Then
' send the data
'signal that we've sent to this user
gSentYN(conn_index) = True
[/indent] Ok, 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.
[indent][code]Private Sub sktClient_DataArrival(ByVal bytesTotal As Long)
Dim newdata As String
' Get the arriving data and print it out.
' add the data to the output
txtOutput.Text = newdata & txtOutput.Text
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 Sub[/code][/indent] Ok, 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.