Blocking sockets: server
Now that you've seen how a blocking client works, it's time for the blocking server example. This chapter will explain how to build a simple server that ROT13 encodes the received data and then sends it back. ROT13 (rot stands for rotate) is a very simple encryption method used by Caesar. Each character in the alphabet is replaced by the character 13 positions farther (the characters rotate 13 places). The encryption is symmetric, that is encryption works exactly the same as decrypting. You can use rot13.com if you want to play with it.
1. Program flow
The program flow is as follows:
- The server creates a server socket
- The server socket is bound to an address
- The server socket is put into the listening state
- On connection attempt, the connection is accepted and a client socket is available
- The client socket is read, every byte is ROT13'd and sent back.
- When the client closes the connection, the program ends
2. Framework
The framework is almost the same as that of the blocking client example from chapter 6, only the HRException has been renamed to ROTException and some include files were added:
#include <string>
#include <sstream>
#define WIN32_MEAN_AND_LEAN
#include <winsock2.h>
#include <windows.h>
using namespace std;
class ROTException
{
public:
ROTException() :
m_pMessage("") {}
virtual ~ROTException() {}
ROTException(const char *pMessage) :
m_pMessage(pMessage) {}
const char * what() { return m_pMessage; }
private:
const char *m_pMessage;
};
int main(int argc, char* argv[])
{
// main program
}
3. Constants and global data
The program uses a few constants for the default server port number (4444), the required winsock version and receive buffer size.
const int DEFAULT_PORT = 4444;
const int TEMP_BUFFER_SIZE = 128;
4. The main function
The main function too has a lot of common with the blocking client from the previous chapter:
{
int iRet = 1;
WSADATA wsaData;
cout << "Initializing winsock... ";
if (WSAStartup(MAKEWORD(REQ_WINSOCK_VER,0), &wsaData)==0)
{
// Check if major version is at least REQ_WINSOCK_VER
if (LOBYTE(wsaData.wVersion) >= REQ_WINSOCK_VER)
{
cout << "initialized.\n";
int port = DEFAULT_PORT;
if (argc > 1)
port = atoi(argv[1]);
iRet = !RunServer(port);
}
else
{
cerr << "required version not supported!";
}
cout << "Cleaning up winsock... ";
// Cleanup winsock
if (WSACleanup()!=0)
{
cerr << "cleanup failed!\n";
iRet = 1;
}
cout << "done.\n";
}
else
{
cerr << "startup failed!\n";
}
return iRet;
}
Winsock is initialized, and cleaned up again when the program is finished. In between is the server startup code. The program allows an optional parameter that specifies the port the server should run on. If it is not set, the default port number is used (4444). Finally, RunServer is called with the final port number as its parameter. The RunServer function contains the actual server code.
5. RunServer
RunServer is the function where the server is setup and connections are accepted. The basic framework of this function is:
{
SOCKET hSocket = INVALID_SOCKET,
hClientSocket = INVALID_SOCKET;
bool bSuccess = true;
sockaddr_in sockAddr = {0};
try
{
// Create socket
cout << "Creating socket... ";
if ((hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
throw ROTException("could not create socket.");
cout << "created.\n";
// code goes here
}
catch(ROTException e)
{
cerr << "\nError: " << e.what() << endl;
bSuccess = false;
}
if (hSocket!=INVALID_SOCKET)
closesocket(hSocket);
if (hClientSocket!=INVALID_SOCKET)
closesocket(hClientSocket);
return bSuccess;
}
A server socket is created in the usual way, and a variable to hold the client socket is reserved too. The client socket doesn't have to be created, since winsock will do that for us later. You do have to close both socket handles when you won't use them anymore though, this is done in the cleanup part at the end of the code.
6. Binding the socket
After the socket is created, we will bind it to an address. As I've explained in the first chapters, a server listens on a specific port number and possibly on a specific IP number as well. Before you can let the server socket listen, it must be bound. The winsock API bind will do that for you. In this example, the socket will be bound to the port specified by the portNumber parameter of RunServer, the IP number is set to INADDR_ANY, indicating that the server will listen on all available IP numbers. To bind a socket with bind, you need to fill in a sockaddr_in structure with the address you want the socket be bound to. Setting up this structure is done in a separate function called SetServerSockAddr:
{
// Set family, port and find IP
pSockAddr->sin_family = AF_INET;
pSockAddr->sin_port = htons(portNumber);
pSockAddr->sin_addr.S_un.S_addr = INADDR_ANY;
}
This function is called in RunServer in the following way:
cout << "Binding socket... ";
SetServerSockAddr(&sockAddr, portNumber);
if (bind(hSocket, reinterpret_cast<sockaddr*>(&sockAddr), sizeof(sockAddr))!=0)
throw ROTException("could not bind socket.");
cout << "bound.\n";
7. Letting the socket listen
If the binding succeeds, the socket is put into listening mode. As soon as it's in this state, any client can make a connection attempt to the server on the port the socket is bound to. Setting the listening mode is simply done by calling the listen winsock function:
cout << "Putting socket in listening mode... ";
if (listen(hSocket, SOMAXCONN)!=0)
throw ROTException("could not put socket in listening mode.");
cout << "done.\n";
Listen has two parameters. The first is the socket you want to listen, the second is the length of the queue of pending connections. Usually the default value of SOMAXCONN is okay for the latter parameter. This value is the maximum number of connections that winsock will hold pending until your program accepts them. You probably don't need to worry about this value most of the time.
8. Accepting connections
When the server socket is in the listening state, you need to accept the incoming connections using the accept function. The accept function blocks until a connection request comes in, establishes the connection and then returns a client socket handle. It is important to know that the server socket's only purpose is now to listen for connections and accept them. As soon as you accept a connection, a new socket is created by winsock. This socket is usually called the client socket and that's the socket you will be receiving and sending data on. This often confuses winsock beginners, some try to receive or send data on the listening socket, while they should use the client socket.
Besides accepting a connection and returning a client socket handle, accept also fills in a sockaddr_in structure with information about the client. Our example will use this information to print a short description of the client that connected (in the form IP:port).
int clientSockSize = sizeof(clientSockAddr);
// Accept connection:
hClientSocket = accept(hSocket,
reinterpret_cast<sockaddr*>(&clientSockAddr),
&clientSockSize);
// Check if accept succeeded
if (hClientSocket==INVALID_SOCKET)
throw ROTException("accept function failed.");
cout << "accepted.\n";
// Wait for and accept a connection:
HandleConnection(hClientSocket, clientSockAddr);
The above code calls accept, and then handles the both the client socket handle and the sockaddr_in structure to a new function, HandleConnection, which will deal with the connection. After this code has executed, RunServer returns and closes the sockets, as shown earlier.
9. HandleConnection
The HandleConnection function handles the connection. The first thing it does is showing a short description of the client. A separate function (GetHostDescription) is used to create this description.
{
// Print description (IP:port) of connected client
cout << "Connected with " << GetHostDescription(sockAddr) << ".\n";
char tempBuffer[TEMP_BUFFER_SIZE];
// todo
cout << "Connection closed.\n";
}
The GetHostDescription function looks like this:
{
ostringstream stream;
stream << inet_ntoa(sockAddr.sin_addr) << ":" << ntohs(sockAddr.sin_port);
return stream.str();
}
We will now write the part marked as 'todo' in the above HandleConnection framework. The function should loop on recv until the connection is closed (recv returns 0). Every time data is received, it is ROT13 encoded and sent back to the client. First of all, a simple function is written to deal with the ROT13 encryption:
{
for(int i=0;i<size;i++)
{
char c = pBuffer[i];
if ((c >= 'a' && c < 'n') || (c >= 'A' && c < 'N') )
c += 13;
else if ((c>='n' && c <= 'z') || (c>='N' && c <= 'Z'))
c -= 13;
else
continue;
pBuffer[i] = c;
}
}
Then the main loop is simple. First a recv call, that will receive data from the client. The rot13 function is called to encrypt the received data and finally send is used to send the encrypted data back to the client:
while(true)
{
int retval;
retval = recv(hClientSocket, tempBuffer, sizeof(tempBuffer), 0);
if (retval==0)
{
break; // Connection has been closed
}
else if (retval==SOCKET_ERROR)
{
throw ROTException("socket error while receiving.");
}
else
{
/* retval is the number of bytes received.
rot13 the data and send it back to the client */
rot13(tempBuffer, retval);
if (send(hClientSocket, tempBuffer, retval, 0)==SOCKET_ERROR)
throw ROTException("socket error while sending.");
}
}
10. Testing
That's all, it should work now. To test the program, you could use the telnet client supplied by windows but you have to get the settings right. If you switch off the local echo you don't see what you type but you do see what data you receive. This means you see the encrypted text directly. However, I prefer a better client called PuTTY, you can find it here. I recommend you to download it as well. Compile the program, run it and you will (hopefully) see a message that the program is waiting for a connection:
X:\asm\rot13server>rot13server
Initializing winsock... initialized.
Creating socket... created.
Binding socket... bound.
Putting socket in listening mode... done.
Fire up putty, and in the configuration screen, type in localhost as the hostname, 4444 as the port number (or a different one if you choose to run the program with some other port). Set the protocol to Raw. Finally press Open to connect.
You will now see putty's console window. Here you can type text that will be send to the server. Any data received will be printed in the same window. Note: putty by default has local line editing enabled. This means that you can type and even edit the text you type as long as you stay on the same line, since it's not send until you press enter. If you use a client that immediately sends every character, you also get a response immediately. If you have such a client you should disable local echo (ie. showing the text you type), otherwise you get your text and the received text interleaved, which is pretty hard to read. This is not the case with putty. Here's a screenshot of the connection in action:
11. Source code
Finally, the source code:
Download the source zip file here: rot13server_cpp.zip
12. Conclusion
Now you've seen both a blocking client and a blocking server. Blocking sockets are relatively easy to use because they fit in nicely in the program flow. Still, you've only seen pretty simple examples, since both the client and the server we showed did practically nothing with the data other than print it or in this case, encrypt and then send it back. It gets harder when we have to extract meaningful information from the received data like when dealing with a protocol like POP3.