Introduction
In previous articles in this series we went through writing a simple TCP
server that could accept a single connection at a time and also a simple TCP
client that could download a file via HTTP. But it must have been obvious to
most of you that a server program must surely be able to handle more than one
client connection at any time. Otherwise if a client is currently
connected, other clients wouldn't find the server to be of much utility to
them.
In this article we shall write a multithreaded TCP server. In addition we
shall also create our own custom TCP chat protocol, albeit a very simple one. We
will also then write a client that will connect to this server and chat using
this protocol. You might try running multiple versions of the client at the same
time to test whether the server can indeed accept multiple connections. The
server simply sends requested files to the client which then saves it on the
client machine.
The method used here for handling multiple clients is the age-old method of
one thread per client connection. There are several other more efficient
mechanisms for handling multiple connections like IO completion ports. But if
the chat protocol is elementary and each client connection is not very
processor-intensive then this is a pretty reasonable method unless the number of
clients that would connect at any one time is abnormally large.
Writing the multithreaded server
Those of you who have read my article on writing a simple TCP
server would probably know how a TCP server is created. Others might be
better off to go and read that article first. Others can continue. There is not
much difference in our main()
function. We start the
server thread and loop endlessy on _getch()
till
someone presses ESC and then we close the server socket and exit.
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;
cout << "Press ESCAPE to terminate program\r\n";
AfxBeginThread(MTServerThread,0);
while(_getch()!=27);
closesocket(server);
WSACleanup();
return nRetCode;
}
Now lets take a look at the server thread. Up to the part where we call listen()
the code is essentially the same. It's in the accept()
part where we make our small
change.
UINT MTServerThread(LPVOID pParam)
{
if(listen(server,10)!=0)
{
return 0;
}
SOCKET client;
sockaddr_in from;
int fromlen=sizeof(from);
while(true)
{
client=accept(server,
(struct sockaddr*)&from,&fromlen);
AfxBeginThread(ClientThread,(LPVOID)client);
}
return 0;
}
What happens is essentially simple. We accept a connection and the moment we
accept it, we start off a new thread passing to the thread the client SOCKET
. Then we return back to
accept()
. Thus the moment a client connects, a new thread
is started to handle the client and thus the next client can also connect which
will start off yet another thread and so on and so forth. Oh boy! And to think,
some of you guys actually expected it to be a lot harder than that eh?
Our own custom protocol
Let's define the commands allowed in our own TCP chat protocol. We'll
obviously need a command for closing the session. How about "QUIT"? That seems
to be a nice obvious word for that. Now we don't want just about everyone to
download files. So let's add an "AUTH" command that has a parameter which
specifies the password. If the password is correct then we put the user into the
authorized state. Both AUTH and QUIT may be used in the non-authorized state.
But the command "FILE" which is used to retrieve files, is allowed only in the
authorized state. Well, that's three commands, two of them allowed at all times
and one of them allowed only during authorized state.
QUIT
:- This will close the connection
AUTH [password]
:- This will log the user into
authorized mode
FILE [filename with full path]
:- Retrieves a file [works
only in authorized mode]
Fancy that! Our own nice little protocol. Lets use # to denote success
messages and ! to denote error messages. I'll now show you how a simple TCP chat
session to our server will look like before I actually show you how it is
implemented in code.
Trying 192.168.1.44...
Connected to 192.168.1.44.
Escape character is '^]'.
#Server Ready.
file c:\config.sys
!You are not logged in.
auth yellow
!Bad password.
auth passwd
#You are logged in.
file c:\config.sys
DEVICE=C:\WINDOWS\HIMEM.SYS
DEVICE=C:\WINDOWS\EMM386.EXE
#File c:\config.sys sent successfully.
file c:\setup.log
[InstallShield Silent]
Version=v6.00.000
File=Log File
[ResponseResult]
ResultCode=0
[Application]
Name=Intel Ultra ATA Storage Driver
Version=6.03.007
Company=Intel
Lang=0009
#File c:\setup.log sent successfully.
file d:\g5.doc
!File d:\g5.doc could not be opened.
quit
Connection closed by foreign host.
As you can see, once a user is logged in, he can request any number of files.
I've shown only text files here, but he might as well demand binary files too.
In the client program we will write later, we can do this too. Right now let's
look at how the client thread handles this chat protocol.
UINT ClientThread(LPVOID pParam)
{
char buff[512];
CString cmd;
CString params;
int n;
int x;
BOOL auth=false;
SOCKET client=(SOCKET)pParam;
strcpy(buff,"#Server Ready.\r\n");
send(client,buff,strlen(buff),0);
while(true)
{
n=recv(client,buff,512,0);
if(n==SOCKET_ERROR )
break;
buff[n]=0;
if(ParseCmd(buff,cmd,params))
{
if(cmd=="QUIT")
break;
if(cmd=="AUTH")
{
if(params=="passwd")
{
auth=true;
strcpy(buff,"#You are logged in.\r\n");
}
else
{
strcpy(buff,"!Bad password.\r\n");
}
send(client,buff,strlen(buff),0);
}
if(cmd=="FILE")
{
if(auth)
{
if(SendFile(client,params))
sprintf(buff,
"#File %s sent successfully.\r\n",
params);
else
sprintf(buff,
"!File %s could not be opened.\r\n",
params);
x = send(client, buff,
strlen(buff),0);
}
else
{
strcpy(buff,"!You are not logged in.\r\n");
send(client,buff,strlen(buff),0);
}
}
}
else
{
strcpy(buff,"!Invalid command.\r\n");
send(client,buff,strlen(buff),0);
}
}
closesocket(client);
return 0;
}
Hopefully, the code is self-explanatory. I'll just run through it briefly. We
first send a server greeting as is customary among TCP servers. Now we keep
looping and accepting commands. We use our function ParseCmd
to parse the
command string entered into two CString
objects, one containing the command and the
other containing the arguments if any. If ParseCmd
returns true
it
means an unknown command was sent, and we give back an error message.
If the command is QUIT we break out of the while loop, close the client
socket and exit the thread. We also have a boolean flag called auth and
we make this true only after the client has authorized itself. Till then, if we
get a FILE command we send back a message saying that the client is not logged
in. Right now I have hard coded "passwd" as our password, but in a real
situation, the username/password will be taken from some database or
configuration file.
Using the AUTH command the user can log in. We compare with our password and
if they match we send a message telling them they are logged in and set the auth flag to true, else we give them a bad login error message. Once they
are authorized they can request files. We use a function called SendFile
to send the files across the TCP connection. I hope things are clear now.
Now lets look at the ParseCmd
function
BOOL ParseCmd(char *str, CString& cmd, CString& params)
{
int n;
CString tmp=str;
tmp.TrimLeft();
tmp.TrimRight();
if((n=tmp.Find(' '))==-1)
{
tmp.MakeUpper();
if(tmp!="QUIT")
return false;
cmd=tmp;
return true;
}
cmd=tmp.Left(n);
params=tmp.Mid(n+1);
cmd.MakeUpper();
if((cmd!="AUTH") && (cmd!="FILE"))
return false;
return true;
}
Well as you can see, the function is pretty much straightforward. It splits
the string and returns true
if it encounters a valid command. Otherwise it
returns false
which indicates error.
Now let's take a look at the SendFile
function.
BOOL SendFile(SOCKET s, CString fname)
{
CFile f;
BOOL p=f.Open(fname,CFile::modeRead);
char buff[1024];
int y;
int x;
if(!p)
return false;
while(true)
{
y=f.Read(buff,1024);
x=send(s,buff,y,0);
if(y<1024)
{
f.Close();
break;
}
}
return true;
}
This is also a simple function. It returns true
if
the file was successfully sent and false
if the file
was not found. Bit confusing isn't it. I used true
to
represent an error in ParseCmd
and here I am using false
to represent an error.
I guess I have a long way to
go as far as coding standards are concerned. I hope you nice ladies and
gentlemen will pardon this grave failing in my personality.
Alright now we have written our multithreaded TCP server with our own custom
chat protocol. That's all very well I guess. But I bet some of you are now
thinking that perhaps it would be nice to actually write a nice little client
program that connects to the server, and chats with it using our protocol, and
retrieves a few files as well. Hmmm. By an amazing coincidence I have had the
very same idea too. Therefore we shall proceed to write a client program.
The custom client
I won't go through the source code this time. Those of you who have gone
through my simple TCP
client would have absolutely no problem in downloading and understanding the
source code. Just a few points though. Do not use the string manipulation
functions like strcpy()
and strchr()
on the buffers returned by
recv()
. These byte buffers may not be null terminated and
calling functions like strcpy()
will simply wreck
your program. You may open the source and look into two functions I have used to
search the buffer for a certain set of characters. I have not used the string
manipulation functions at all.
Here is how to use the custom client. By the way I have hard coded 127.0.0.01 into the client source code as the server IP.
You might want to change that if you are running the server and client on two
different machines. Before running please make sure that the server is up and
running.
E:\work\MTSClient\Debug>mtsclient
Usage :- mtsclient [file1] [file2] [file3] ....
E:\work\MTSClient\Debug>mtsclient c:\cu.gif c:\cp.gif c:\g.gif c:\ddd
File c:\cu.gif not found on server
cp.gif has been saved.
g.gif has been saved.
File c:\ddd not found on server
E:\work\MTSClient\Debug>
Conclusion
This is probably the last in my 3 part series of introductory articles on TCP
programming using Winsock. By now you must be able to write basic TCP clients
and TCP servers [multithreaded]. I suggest that you try writing a simple program
that connects to a POP server, logs in and checks whether there is any mail in
there. Or you might try writing a small program that uses SMTP chat to send a
mail. To test your server coding skills, you might want to write a simple HTTP
server. Or think up something and write your own custom server with your own
custom protocol. Anyway, have fun...