Background
A generic, light-weighted client/server socket class that is written in C++ is presented here, together with a mini-messenger that is constructed by using this class. There is indeed a vast amount of information about socket programming over the Internet, and it is not time-consuming for one to download relevant code directly, therefore, it is certainly necessary to discuss the advantage of this class first.
First of all, this class is light-weighted. One can find socket APIs in MFC, in C#, in .NET framework, but apparently, to take advantage of these APIs, one has to bind the application with MFC, with .NET framework. In some cases, MFC, C# or .NET may not be the optimal solution to the application needs. Also, if the application is to be developed on a Unix/Linux platform, then these APIs are not even available. On the other hand, the class that is presented here can be easily used in a console application; there is no need to provide any window handle for this class to be used. Also, this class can be incorporated directly into applications that are developed on Unix/Linux platform by simply adding #define UNIX
to the header file.
Secondly, this class provides high performance client/server structure by using the most generic and low-level building blocks. This gives the great flexibility back to the developer, for example, it is then up to the developer to add an appropriate custom-designed security system. Among other advantages, a server that is built by using this class can run on a Windows machine, the client can be on the same machine (the same machine with inter-processes communication), or more often on a different machine that runs either Windows system or Unix system. The other communication party can be a station on a local network, or it can be a machine that communicates across the Internet.
Introduction
The rest of this article is organized by following the intuitive flow of making a communication happen:
- before the communication;
- during the communication;
- after the communication;
Before the communication, we need to know the basic information about the communication parties: what is the local machine�s name, what is the local machine�s IP address? If I know the remote host�s name is www.yuchen.net, then what is the corresponding IP address? What about vise versa? This basic information can be easily retrieved by using our myHostInfo
class - although not the �star� class in this article, still many developers may find it convenient to use.
To make the communication happen, mySocket
class and its sub-class, myTcpSocket
, are the star classes. In order to ensure the performance, one needs to be able to control the behavior of a socket. The base class, mySocket
, provides methods to control the socket behaviors such as blocking or non-blocking, linger on/off, receive buffer size, send buffer size etc. myTcpSocket
further defines the socket to be a TCP socket, its methods make sure the communication happen in a reliable manner.
After the communication, the house-keeping job is done by the above star class. It is also worthy of mentioning the other two classes that we developed to help the communication. Because of the complexity of communication, it is important to protect against possible failures. A very simple exception handling class, myException
, is provided, the response()
method is defined to be virtual
. The default implementation of this method is to simply output the error message to both the log file (see below) and the standard output. However, one may decide whatever specific handing one may desire by overriding this method.
Another simple class is the system log class called myLog
, which is very handy when recording the actions that are taken by the system. Very often, the log file that is generated by the system is the first place that one can locate errors.
The last section of this article presents a small application that is developed by using the above classes. This application is called mini-messenger, two people can use it to talk to each other. It is essentially a Yahoo! Messenger, but when using it, you don�t have the Yahoo! logo showing on your screen, so you don�t have to worry that your boss who might be watching over your shoulders. The main purpose of this small application is certainly to show a possible way to use the classes we developed.
Class myHostInfo
The first step to build a communication is to understand the other party: its domain name, its corresponding IP address. myHostInfo
class is provided to query the host networking information.
Constructor/Destructor Summary:
myHostInfo()
The default constructor. In the case where neither the domain name nor the IP address is given/known, this constructor could be used, and in which case, the standard name for the current processor will be used as the local host name.
myHostInfo(string& sHostName, hostType type)
sHostName
can be a host name, such as www.yuchen.net, in which case type
has to use the value NAME
. sHostName
can also be an IP address, such as �128.34.67.23�, in which case type
has to use the value ADDRESS
.
~myHostInfo()
Methods Summary:
The following example shows the usage of this class.
string serverName = "www.yuchen.net";
myHostInfo myServer(serverName,NAME);
string serverName = myServer.getHostName();
string serverAddr = myServer.getHostIPAddress();
cout << "Server name: " << serverName << endl;
cout << "Server IP address: " << serverAddr << endl;
The output of the above code is as follows (I cannot get the screenshots, let me just use the simple cut-and-paste):
Server name: www.yuchen.net
Server IP address: 66.218.85.169
Press any key to continue
Class mySocket
Socket is the most fundamental building block for all Internet communications. mySocket
class is provided as the base class for this building block. TCP and UDP sockets will be built from inheriting this base class (in this article, only TCP socket is developed).
Constructor/Destructor Summary:
mySocket(int portNumber)
A port number is used to construct a socket.
virtual ~mySocket()
Once a socket is created, it has a variety of settings that can be queried or reset by the application. The methods in mySocket
class are mainly mutators for these settings.
Methods Summary
We only present the set methods, the get methods are omitted, refer to the code for detail:
void setDebug(int)
Turns on the recording of the debugging information. This option allows the kernel to maintain a history of messages sent and received. 1 is ON, 0 is OFF.
void setReuseAddr(int)
Under normal conditions, two or more applications cannot bid to the same port and address using bind()
. By setting this option on, this rule is ignored. 1 is ON, 0 is OFF.
void setKeepAlive(int)
Connected sockets are kept alive by enabling periodic transmission of messages. If the sending and receiving of messages fail, the connected socket is considered broken. 1 is ON, 0 is OFF.
void setLingerOnOff(bool)
Setting this option ON causes the socket to linger when closing the socket. Normally, when the application tries to close the socket, the system will attempt to deliver unsent data. If this option is ON, the process is blocked until all the data is transmitted or until the linger time expires. true
is ON and false
is OFF.
void setLingerSeconds(int)
Set how long the system will use to send all the unsent data. int
is in the unit of second. If 0 is set, data remaining to be sent is discarded when the socket is closed. See setLingerOnOff(bool)
.
void setSocketBlocking(int)
Setting blocking on and off for a given socket controls the process suspension when the following actions are performed:
- writing data to a connected TCP socket.
- receiving data from a connected TCP/UDP socket.
- accepting connections from peer applications using TCP sockets.
By default, these actions block the executing process. Use 1 as ON to enforce the blocking and 0 as OFF to turn off the blocking.
void setSendBufSize(int)
Sets the buffer size for sending data. This is useful only when the default send buffer size is not large enough. See also setReceiveBufSize(int)
.
void setReceiveBufSize(int)
Sets the buffer size for receiving data. This is useful only when the default receive buffer size if not large enough. See also setSendBufSize(int)
.
friend ostream& operator<<(ostream&,mySocket&)
Used for logging the socket to the standard output or any other IO stream.
The following is an example showing the setting of a socket (notice that we used myHostInfo
to get the information of my local machine):
cout << "Retrieve the local host name and address:" << endl;
myHostInfo myLocalMachine;
string localHostName = myLocalHost.getHostName();
string localHostAddr = myLocalHost.getHostIPAddress();
cout << "Name: " << localHostName << endl;
cout << "Address: " << localHostAddr << endl;
mySocket localSocket(1000);
cout << localSocket;
localSocket.setLingerOnOff(true);
localSocket.setLingerSeconds(10);
cout << endl << "After changing the socket settings ... " << endl;
cout << localSocket;
Here is the output from the above code:
Name: liyang
Address: 209.206.17.121
--------------- Summary of socket settings -------------------
Socket Id: 1960
port #: 1000
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: false
linger seconds: 0
----------- End of Summary of socket settings ----------------
After changing the socket settings ...
--------------- Summary of socket settings -------------------
Socket Id: 1960
port #: 1000
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: true
linger seconds: 10
----------- End of Summary of socket settings ----------------
Press any key to continue
Class myTcpSocket
myTcpSocket
class is used to build communications based on TCP sockets. We will first describe the important methods in this class, then discuss the flow of building a communication between server and client.
Constructor/Destructor Summary:
Once a TCP socket is created, the following methods can be used to build the communication between two sockets.
Methods Summary:
int sendMessage(string&)
Send message string&
to the other party. The number of bytes that are sent is returned. This can be either a server call or a client call.
void bindSocket()
This call is used to associate the socket file descriptor with the socket port number, along with the address that the future client will use. It is a necessary call before any communication can happen. This is called only for the server end, not for the client.
myTcpSocket* acceptClient(string&)
This function is called to accept a connecting client. This again is a server call and not for the client end. This will also block the thread until an incoming client call is detected. Once an incoming client call is detected, a new socket is returned. For the server, this new socket is the sole representation of the incoming client: any forthcoming communication with this connecting client is done via this socket.
The incoming client�s host name, such as www.yuchen.net, is saved in the string&
for easy access.
void listenToClient(int numPorts = 5)
A server end call to set the server in the �listening� status. The numPorts
specifies how many incoming calls altogether the server can handle.
virtual void connectToServer(string&,hostType)
A client end call for the client to be connected with the server. The string&
is either the name or the IP address of the server which the client tries to connect to. The definition of the hostType
is as follows:
enum hostType {NAME, ADDRESS};
A typical call normally takes one of the following two forms:
int recieveMessage(string&)
Receive message from the other party. This can be either a server call or a client call. The string&
holds the buffer, which will have the message after a successful return, and the return int
value shows how many bytes have been received.
Building a communication
Now, it is the time to describe the flow of building a communication between a server and a client (we are not going into the details about myException
class and myLog
class, one can read the document/code to understand them). Since this article is not intended to be a tutorial, so this description will only capture the important part of the story.
The server has to be started first:
myTcpSocket myServer(PORTNUM);
cout << myServer;
This will create a server called myServer
. Inside this class, a socket has been built using the port number given by PORTNUM
. This is quite like applying for a phone line for your house: you called, let us say, BellSouth, already, so you have got a phone number for your house.
The next thing to do is to bind your phone with the phone number you got, you accomplish this by connecting the phone to the phone jack on the wall. The phone jack on the wall is the socket that is just created, and the phone itself is like the PORTNUM
:
myServer.bindSocket();
After this, you are ready to receive phone calls from your friends (and sadly, your billing companies). This is done by waiting for the incoming calls. In our model, this is accomplished by the following:
myServer.listenToClient();
When the phone rings, you will accept the incoming call by picking it up:
cout << "server is waiting for client connection ... " << endl;
myTcpSocket* client;
string clientHost;
client = myServer.acceptClient(clientHost);
Notice that the server accepts the incoming client call by creating a new socket, it will in fact continue to wait for other incoming calls on its own socket � this can be understood like the �call waiting� function that is offered by phone companies.
Once we reach this point, the only thing the server will do is to pass information back and forth with the client by calling the sendMessage()
and receiveMessage()
functions. The mini-messenger in the next section shows the details.
The client side, on the other hand, is quite simple. However, before we start the client, we do need to know which server we are going to call. Assuming we know the IP address of the server (if not, we should know the name of the server, then we can use myHostInfo
class to query the IP address of the server), we need to write this IP address into the serverConfig.txt file, and we need to read this file to get the IP address of the server so we can call it.
First, let us build the client:
myTcpSocket myClient(PORTNUM);
myClient.setLingerOnOff(true);
myClient.setLingerSeconds(10);
cout << myClient;
Assuming the server�s IP address is saved in serverAddr
, we can now connect to the server:
myClient.connectToServer(serverAddr,ADDRESS);
The next is to communicate with the server by using the sendMessage()
and receiveMessage()
methods.
Mini-Messenger
To demo the usage of the classes we discussed so far, a small application is developed. This application lets you to talk with your party over the Internet without using things like Yahoo! Messenger. With the discussion above, it should be fairly easy to understand both the server and client side now.
To use this messenger, assuming your party is the server, he has to start the server on his side, and then he will let you know the IP address of his server, say, by giving you a call (we still need the low-tech). After writing the IP address in your serverConfig.txt file, you can start your client, and you are ready to talk.
The server side:
#include "..\mySocket\mySocket.h"
#include "..\myLog\myLog.h"
#include "..\myException\myException.h"
#include "..\myHostInfo\myHostInfo.h"
myLog winLog;
int main()
{
#ifdef WINDOWS_XP
WSADATA wsaData;
winLog << "system started ..." << endl;
winLog << endl << "initialize the winsock library ... ";
try
{
if (WSAStartup(0x101, &wsaData))
{
myException* initializationException = new
myException(0,"Error: calling WSAStartup()");
throw initializationException;
}
}
catch(myException* excp)
{
excp->response();
delete excp;
exit(1);
}
winLog << "successful" << endl;
#endif
winLog << endl;
winLog << "Retrieve the local host name and address:" << endl;
myHostInfo uHostAddress;
string localHostName = uHostAddress.getHostName();
string localHostAddr = uHostAddress.getHostIPAddress();
cout << "----------------------------------------" << endl;
cout << " My local host information:" << endl;
cout << " Name: " << localHostName << endl;
cout << " Address: " << localHostAddr << endl;
cout << "----------------------------------------" << endl;
winLog << " ==> Name: " << localHostName << endl;
winLog << " ==> Address: " << localHostAddr << endl;
myTcpSocket myServer(PORTNUM);
cout << myServer;
winLog << "server configuation: " << endl;
winLog << myServer;
myServer.bindSocket();
cout << endl << "server finishes binding process... " << endl;
winLog << endl << "server finishes binding process... " << endl;
myServer.listenToClient();
cout << "server is listening to the port ... " << endl;
winLog << "server is listening to the port ... " << endl;
cout << "server is waiting for client connecction ... " << endl;
winLog << "server is waiting for client connnection ... " << endl;
myTcpSocket* client;
string clientHost;
client = myServer.acceptClient(clientHost);
cout << endl << "==> A client from [" << clientHost
<< "] is connected!" << endl << endl;
winLog << endl << "==> A client from [" << clientHost
<< "] is connected!" << endl << endl;
while(1)
{
string clientMessageIn = "";
int numBytes = client->recieveMessage(clientMessageIn);
if ( numBytes == -99 ) break;
cout << "[RECV:" << clientHost << "]: "
<< clientMessageIn << endl;
winLog << "[RECV:" << clientHost << "]: "
<< clientMessageIn << endl;
char sendmsg[MAX_MSG_LEN+1];
memset(sendmsg,0,sizeof(sendmsg));
cout << "[" << localHostName << ":SEND] ";
cin.getline(sendmsg,MAX_MSG_LEN);
if ( numBytes == -99 ) break;
string sendMsg(sendmsg);
if ( sendMsg.compare("Bye") == 0 || sendMsg.compare("bye") == 0 )
break;
winLog << "[" << localHostName << ": SEND] " << sendMsg << endl;
client->sendMessage(sendMsg);
}
#ifdef WINDOWS_XP
winLog << endl << "system shut down ...";
try
{
if (WSACleanup())
{
myException* cleanupException
= new myException(0,"Error: calling WSACleanup()");
throw cleanupException;
}
}
catch(myException* excp)
{
excp->response();
delete excp;
exit(1);
}
winLog << "successful" << endl;
#endif
return 1;
}
The client side:
#include "..\mySocket\mySocket.h"
#include "..\myLog\myLog.h"
#include "..\myException\myException.h"
#include "..\myHostInfo\myHostInfo.h"
myLog winLog;
string serverIPAddress = "";
void readServerConfig();
void checkFileExistence(const string&);
int main()
{
#ifdef WINDOWS_XP
WSADATA wsaData;
winLog << "system started ..." << endl;
winLog << endl << "initialize the winsock library ... ";
try
{
if (WSAStartup(0x101, &wsaData))
{
myException* initializationException =
new myException(0,"Error: calling WSAStartup()");
throw initializationException;
}
}
catch(myException* excp)
{
excp->response();
delete excp;
exit(1);
}
winLog << "successful" << endl;
#endif
winLog << endl;
winLog << "Retrieve the localHost [CLIENT] name and address:" << endl;
myHostInfo uHostAddress;
string localHostName = uHostAddress.getHostName();
string localHostAddr = uHostAddress.getHostIPAddress();
cout << "Name: " << localHostName << endl;
cout << "Address: " << localHostAddr << endl;
winLog << " ==> Name: " << localHostName << endl;
winLog << " ==> Address: " << localHostAddr << endl;
readServerConfig();
winLog << endl;
winLog << "Retrieve the remoteHost [SERVER] name and address:" << endl;
winLog << " ==> the given address is " << serverIPAddress << endl;
myHostInfo serverInfo(serverIPAddress,ADDRESS);
string serverName = serverInfo.getHostName();
string serverAddr = serverInfo.getHostIPAddress();
cout << "Name: " << serverName << endl;
cout << "Address: " << serverAddr << endl;
winLog << " ==> Name: " << serverName << endl;
winLog << " ==> Address: " << serverAddr << endl;
myTcpSocket myClient(PORTNUM);
myClient.setLingerOnOff(true);
myClient.setLingerSeconds(10);
cout << myClient;
winLog << "client configuation: " << endl;
winLog << myClient;
cout << "connecting to the server [" << serverName
<< "] ... " << endl;
winLog << "connecting to the server [" << serverName
<< "] ... " << endl;
myClient.connectToServer(serverAddr,ADDRESS);
int recvBytes = 0;
while (1)
{
char sendmsg[MAX_MSG_LEN+1];
memset(sendmsg,0,sizeof(sendmsg));
cout << "[" << localHostName << ":SEND] ";
cin.getline(sendmsg,MAX_MSG_LEN);
string sendMsg(sendmsg);
if ( sendMsg.compare("Bye") == 0 || sendMsg.compare("bye") == 0 )
break;
winLog << "[" << localHostName << ": SEND] " << sendMsg << endl;
myClient.sendMessage(sendMsg);
string clientMessageIn = "";
recvBytes = myClient.recieveMessage(clientMessageIn);
if ( recvBytes == -99 ) break;
cout << "[RECV:" << serverName << "]: "
<< clientMessageIn << endl;
winLog << "[RECV:" << serverName << "]: "
<< clientMessageIn << endl;
}
#ifdef WINDOWS_XP
winLog << endl << "system shut down ...";
try
{
if (WSACleanup())
{
myException* cleanupException =
new myException(0,"Error: calling WSACleanup()");
throw cleanupException;
}
}
catch(myException* excp)
{
excp->response();
delete excp;
exit(1);
}
winLog << "successful" << endl;
#endif
return 1;
}
void readServerConfig()
{
string serverConfigFile = "serverConfig.txt";
checkFileExistence(serverConfigFile);
ifstream serverConfig(serverConfigFile.c_str());
getline(serverConfig,serverIPAddress);
serverConfig.close();
}
void checkFileExistence(const string& fileName)
{
ifstream file(fileName.c_str());
if (!file)
{
cout << "Cannot continue:" << fileName
<< " does NOT exist!" << endl;
exit(1);
}
file.close();
}
Here is a sample output screen (notice that I am using a single PC to talk, still via Internet though):
The server:
------------------------------------------------------
My local host information:
Name: liyang
Address: 209.206.17.121
------------------------------------------------------
--------------- Summary of socket settings -------------------
Socket Id: 1960
port #: 1200
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: false
linger seconds: 0
----------- End of Summary of socket settings ----------------
server finishes binding process...
server is listening to the port ...
server is waiting for client connecction ...
==> A client from [liyang] is connected!
[RECV:liyang]: hello?
[liyang:SEND] yes?
[RECV:liyang]: so we can talk...
[liyang:SEND] looks like so.
!! your party has shut down the connection...
Press any key to continue
The client:
Name: liyang
Address: 209.206.17.121
Name: liyang
Address: 209.206.17.121
--------------- Summary of socket settings -------------------
Socket Id: 1944
port #: 1200
debug: false
reuse addr: false
keep alive: false
send buf size: 8192
recv bug size: 8192
blocking: true
linger on: true
linger seconds: 10
----------- End of Summary of socket settings ----------------
connecting to the server [liyang] ...
[liyang:SEND] hello?
[RECV:liyang]: yes?
[liyang:SEND] so we can talk...
[RECV:liyang]: looks like so.
[liyang:SEND] bye
Press any key to continue
Conclusion
We presented a light-weighted server/client class in C++ in this article, I hope this will be of some use in your development work, and certainly there are more things to consider to make this work well. I welcome any comments/suggestions.