Blocking sockets: client
The first I/O model I'm going to explain to you is the simplest one, the blocking sockets. Winsock functions operating on blocking sockets will not return until the requested operation has completed or an error has occurred. This behavior allows a pretty linear program flow so it's easy to use them. In chapter 4, you've seen the basic winsock functions. These are pretty much all functions you need to program blocking sockets, although I will show you some additional functions that may be useful in this chapter.
You might not be very interested in blocking sockets if you plan to use an I/O model that uses non-blocking socket. Nonetheless, I strongly recommend you to read the chapters about blocking sockets too since they cover the socket programming basics and other useful winsock features I will assume you remember for the next chapters.
1. A simple client
The first example is a simple client program that connects to a website and makes a request. It will be a console application as they work well with blocking sockets. I won't assume you have deep knowledge of the HTTP (the protocol used for the web), this is what happens in short:
- The client connects to the server (on port 80 by default)
- The server accepts the connection and just waits
- The clients sends its HTTP request as an HTTP request message
- The server responds to the HTTP request with an HTTP response message
- The server closes the connection*
*) This depends on the value of the connection HTTP header, but to keep things simple, we assume the connection will always be closed.
HTTP follows the typical client-server model, the client and server talk to each other in turns. The client initiates the requests; the server reacts with a response.
An HTTP request includes a request method of which the three most used are GET and POST and HEAD. GET is used to get a resource from the web (webpage, image, etc.). POST sends data to the server first (like form data filled by the user), then receives the server's response. Finally, HEAD is the same as GET, except for that the actual data is not send by the server, only the HTTP response message. HEAD is used as a fast way to see if a page has been modified without having to download the full page data. In the example program I will use HEAD since GET can return quite some data while HEAD will only return a response code and set of headers so the program's output easier to read.
A typical HTTP request with the HEAD request method looks like this:
HEAD / HTTP/1.1 <crlf>
Host: www.google.com <crlf>
User-agent: HeadReqSample <crlf>
Connection: close <crlf>
<crlf>
The first / in the fist line is the requested page, in this case the server's root (default page). HTTP/1.1 indicates version 1.1 of the HTTP protocol is used. After this first special line that contains the command follows a set of header in the form "header-name: value", terminated by a blank line. As line terminators, a combination of carriage return (CR, 0x0D) and line feed (LF, 0x0A) is used. That last blank line indicates the end of the client's request. As soon as the server detects this, it will send back a response in this form:
HTTP/1.1 Response-code Response-message <crlf>
header-name: value <crlf>
header-name: value <crlf>
header-name: value <crlf>
<crlf>
As you can see the response format is much like that of a request. Response-code is a 3-digit code that indicates the success or failure of the request. Typical response codes are 200 (everything OK), 404 (page not found, you probably knew this one :) and 302 (found but located elsewhere, redirect). Response-message is a human-readable version of the response code and can be anything the server likes. The set of headers include information about the requested resource. A HEAD request will result in the above response. If the request method would have been GET, the actual page data will be sent back by the server after this response message.
So far for the crash course HTTP, it's not really necessary to understand it all to read the examples about blocking sockets, but now you have some background information too. If you want to read more about HTTP, find the RFC for it (www.rfc-editor.org) or google for HTTP. Another great introduction to HTTP is HTTP made really easy.
2. Program example
A possible output of the example program called HeadReq is shown here:
X:\>headreq www.microsoft.com
Initializing winsock... initialized.
Looking up hostname www.microsoft.com... found.
Creating socket... created.
Attempting to connect to 207.46.134.190:80... connected.
Sending request... request sent.
Dumping received data...
HTTP/1.1 200 OK
Connection: close
Date: Mon, 17 Mar 2003 20:14:03 GMT
Server: Microsoft-IIS/6.0
P3P: CP='ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo C
NT COM INT NAV ONL PHY PRE PUR UNI'
Content-Length: 31102
Content-Type: text/html
Expires: Mon, 17 Mar 2003 20:14:03 GMT
Cache-control: private
Cleaning up winsock... done.
If the program's parameter (www.microsoft.com) is omitted, www.google.com is used.
3. Hostnames
So what do we need for the client? I'm assuming you have the address of the webpage (www.google.com for example) and you want to get the default webpage for it, like the page you get when entering www.google.com in your web browser (in order to keep things simple we will only receive the server's response headers, not the actual page).
As you know from chapter 4, you can connect a socket to a server with the connect function, but this function requires a sockaddr structure (or sockaddr_in in the case of TCP/IP). How do we build up this structure? Sockaddr_in needs an address family, an IP number and a port number. The address family is simply AF_INET. The port number is also easy; the default for the HTTP protocol is port 80. What about the IP, we only got a hostname? If you remember chapter 2 there's a DNS server that knows which IPs correspond to which hostnames. To find this out, winsock has a function called gethostbyname:
hostent * gethostbyname(const char *name);
You simply provide this function a hostname as a string (eg. "www.google.com") and it will return a pointer to a hostent structure. This hostent structure contains a list of addresses (IPs) that are valid for the given hostname. One of these IPs can then be put into the sockaddr_in structure and we're done.
4. Framework
The program we're going to write will connect to a web server, send a HEAD HTTP request and dump all output. An optional parameter specifies the server name to connect to, if no name is given it defaults to www.google.com.
First of all, we define the framework for the application:
#define WIN32_MEAN_AND_LEAN
#include <winsock2.h>
#include <windows.h>
using namespace std;
class HRException
{
public:
HRException() :
m_pMessage("") {}
virtual ~HRException() {}
HRException(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
}
The winsock headers are already included by windows.h, but because we use some winsock 2 specific things we also need to include winsock2.h. Include this file before windows.h to prevent it from including an older winsock version first. We will also need the STL's iostream classes, so we included those too. Don't forget to link to ws2_32.lib, or you'll get a bunch of unresolved symbol errors.
The HRException class is a simple exception class used to throw errors that occur. One of its constructors takes a const char * with an error message that can be retrieved with the what() method.
5. Constants and global data
The program will need some constants and global data, which we define in the following code snippet:
const char DEF_SERVER_NAME[] = "www.google.com";
const int SERVER_PORT = 80;
const int TEMP_BUFFER_SIZE = 128;
const char HEAD_REQUEST_PART1[] =
{
"HEAD / HTTP/1.1\r\n" // Get root index from server
"Host: " // Specify host name used
};
const char HEAD_REQUEST_PART2[] =
{
"\r\n" // End hostname header from part1
"User-agent: HeadReqSample\r\n" // Specify user agent
"Connection: close\r\n" // Close connection after response
"\r\n" // Empty line indicating end of request
};
// IP number typedef for IPv4
typedef unsigned long IPNumber;
These constants and data define the default hostname (www.google.com), server port (80 for HTTP), receive buffer size, and the minimum (major) winsock version required (2 or higher in our case). Furthermore, the full HTTP request is put in two variables. The request is split up because the hostname of the server needs to be inserted as the host header (see the HTTP message examples above). While all strings in C automatically get a 0 byte at the end to terminate it, we don't actually treat it as a null-terminated string. Only the text itself will be send, without the null terminator. Finally, unsigned long is typedef'ed to IPNumber to make the code a bit clearer.
6. The main function
The first thing to do is initializing winsock. We will do this in the main function and write the actual code for the HTTP request in a different function named RequestHeaders. The main function is:
{
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";
// Set default hostname:
const char *pHostname = DEF_SERVER_NAME;
// Set custom hostname if given on the commandline:
if (argc > 1)
pHostname = argv[1];
iRet = !RequestHeaders(pHostname);
}
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;
}
The value the main function returns will be given back as exit code to the OS. Since the convention for command line program is that an exit code of 0 indicates success while other values indicate some kind of error, we will follow this and return the correct value depending on the success of the winsock initialization and the RequestHeaders function.
First of all, WSAStartup is called. It wants the highest winsock version your program supports (REQ_WINSOCK_VER) and fills in a WSADATA structure. After we check if this function succeeded, we still need to check which winsock version has been loaded, since this might be less than REQ_WINSOCK_VER (see chapter 4). If the major version number is at least REQ_WINSOCK_VER, we got the right version.
Then, argc is checked to see if a parameter was given to the program. If there was, it should be a hostname and instead of the default hostname, the parameter comes from argv and is passed on to RequestHeaders.
If WSAStartup succeeded, a matching call to WSACleanup is needed. This is done at the end of the code.
7. RequestHeaders
RequestHeaders is the function where all the magic happens. The basic structure of it is:
{
SOCKET hSocket = INVALID_SOCKET;
char tempBuffer[TEMP_BUFFER_SIZE];
sockaddr_in sockAddr = {0};
bool bSuccess = true;
try
{
// code goes here
}
catch(HRException e)
{
cerr << "\nError: " << e.what() << endl;
bSuccess = false;
}
return bSuccess;
}
As a parameter, RequestHeaders gets the name of the server to connect to. There are some variables we will use, the socket handle, a temporary buffer used to store received data and a sockaddr_in structure for the server's address. The socket handle is initialized to INVALID_SOCKET, the only value that can't be used as a socket handle. bSuccess is a bool that is set to false if the function fails. The main code is surrounded by a try-catch block, any error that occurs is thrown as a standard STL exception and caught by this function. The cleanup code will be after the try-catch block, so cleaning up happens both when everything succeeds and on failure.
The RequestHeaders function has the following tasks:
- Resolve the hostname to its IP.
- Create a socket.
- Connect the socket to the remote host.
- Send the HTTP request data.
- Receive data and print it until the other side closes the connection.
- Cleanup
I will show you how to implement each step in the next sections.
8. Resolving the hostname to its IP
To connect to the server, we need to fill a sockaddr_in structure with its address. As I said earlier, this structure consists of an address family (always AF_INET), an IP number and a port number. Although the port number is not always 80 for web servers, we will assume it is. I also explained gethostbyname can be used to lookup a hostname at the DNS server and retrieve its IP number. The next function of our program, FindHostIP, uses this winsock function.
Note that looking up a host involves a request to a DNS server so it might take some time (typically only 10 milliseconds or so but that's slow compared to normal code). If the hostname isn't found, it might even take seconds. Because we are using blocking sockets, the program will simply hang on gethostbyname until it either succeeds or fails. While gethostbyname is running, we have no control over our program. But as the program is a console program, this doesn't matter.
{
HOSTENT *pHostent;
// Get hostent structure for hostname:
if (!(pHostent = gethostbyname(pServerName)))
throw HRException("could not resolve hostname.");
// Extract primary IP address from hostent structure:
if (pHostent->h_addr_list && pHostent->h_addr_list[0])
return *reinterpret_cast<IPNumber*>(pHostent->h_addr_list[0]);
return 0;
}
Gethostbyname takes a hostname as its single parameter and returns a pointer to a hostent structure. Note that it cannot handle hostnames that are IPs in string form (like "101.102.103.104"). Therefore our program does not accept an IP number as server name in the first parameter. If you would want to allow this, the string can be converted into a number with the inet_addr function.
If the function fails it returns NULL, which is the first thing we check. It means the server name could not be resolved. If it did succeed, we now have a hostent structure pointer. This allocated memory doesn't need to be freed; winsock has a piece of memory for each thread specifically for storing this data in. However this does imply that on the next call to gethostbyname, you cannot use the hostent structure returned by a previous call to it, since it would have been overwritten.
The hostent structure can contain a list of addresses, which do not necessarily have to be IP numbers. Since we use TCP/IP, they will be IP numbers but the structure still has to support other forms of addresses. The h_addr_list member of hostent points to a null-terminated array of other pointers. Each pointer points in that array points to an address. Since the hostent structure does not know the type of addresses used, you need to cast the pointers to the right type, in this case IPNumber*. The FindHostIP code extracts the first available IP address from this structure and returns it. Some additional pointer checks ensure that the program doesn't crash if the pointers are not set or arrays are empty.
The return value of this function, the IP number in network byte order, is used by FillSockAddr:
{
// Set family, port and find IP
pSockAddr->sin_family = AF_INET;
pSockAddr->sin_port = htons(portNumber);
pSockAddr->sin_addr.S_un.S_addr = FindHostIP(pServerName);
}
All it does is calling FindHostIP and storing the IP in the sockaddr_in structure pointed to by the pSockAddr parameter. It also converts the port number from the portNumber parameter to network byte order and stores it as well.
Back to the RequestHeaders function we call FillSockAddr to fill in our local sockaddr_in structure with the right information:
cout << "Looking up hostname " << pServername << "... ";
FillSockAddr(&sockAddr, pServername, SERVER_PORT);
cout << "found.\n";
9. Creating a socket
The next step is to create a socket to connect with. This is quite simple, just call socket with the right parameters:
cout << "Creating socket... ";
if ((hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
throw HRException("could not create socket.");
cout << "created.\n";
If socket fails, it returns INVALID_SOCKET. In that case, no further operations are performed and the following cleanup code (after the catch() handler) is executed directly:
closesocket(hSocket);
The cleanup code is always executed, whether an error occurred or not. It first checks if the socket handle wasn't INVALID_SOCKET (no socket was created). If it isn't, the socket handle is valid and needs to be closed.
10. Connecting the socket
Now that we have the socket, we can connect it to a remote host with connect. Connect uses the sockaddr_in structure we've setup earlier with FillSockAddr and attempts to connect the given socket with the addressed host. Here too, connect will block until a connection has been established or something went wrong. The return value of connect is zero if the socket is connected, otherwise it's SOCKET_ERROR. Before actually connecting, a message is print with the IP and port number of the remote host. The inet_ntoa function is used to convert the numeric IP into a string with the IP in dotted format.
cout << "Attempting to connect to " << inet_ntoa(sockAddr.sin_addr)
<< ":" << SERVER_PORT << "... ";
if (connect(hSocket, reinterpret_cast<sockaddr*>(&sockAddr), sizeof(sockAddr))!=0)
throw HRException("could not connect.");
cout << "connected.\n";
11. Sending the request
When the socket is connected the HTTP request can be send. It is sent in three parts, to easily insert the hostname inside the request:
HEAD / HTTP/1.1 <crlf>
Host: www.google.com <crlf>
User-agent: HeadReqSample <crlf>
Connection: close <crlf>
<crlf>
The send calls are pretty straightforward, each call takes a buffer and sends the specified amount of bytes from it to the remote host. Send will block until all the data has been sent, or fail and return SOCKET_ERROR.
// send request part 1
if (send(hSocket, HEAD_REQUEST_PART1, sizeof(HEAD_REQUEST_PART1)-1, 0)==SOCKET_ERROR)
throw HRException("failed to send data.");
// send hostname
if (send(hSocket, pServername, lstrlen(pServername), 0)==SOCKET_ERROR)
throw HRException("failed to send data.");
// send request part 2
if (send(hSocket, HEAD_REQUEST_PART2, sizeof(HEAD_REQUEST_PART2)-1, 0)==SOCKET_ERROR)
throw HRException("failed to send data.");
cout << "request sent.\n";
Note that the buffer sizes specified are one less than sizeof(buffer), because we don't want to send the null-terminator at the end of the string.
12. Receiving the response
The final step of the program before cleaning up is to receive data and print it until the other side closes the connection. The HTTP header "Connection: close" in our request tells the server that it should close the connection after it has sent its response. Receiving data is done with the recv function that receives the currently available data and puts it in a buffer. I kept the example simple by choosing to just dump this output instead of actually doing something with it, so all we have to do is keep calling recv until the connection is closed. Recv too will block if no data is available immediately and return if some has arrived. The return value of recv is either 0, SOCKET_ERROR or the number of bytes read. SOCKET_ERROR of course indicates a socket error, 0 indicates closure of the connection. So basically we will loop until recv returns 0 (connection closed, done) or SOCKET_ERROR (something went wrong). This leads to the following code:
// Loop to print all data
while(true)
{
int retval;
retval = recv(hSocket, tempBuffer, sizeof(tempBuffer)-1, 0);
if (retval==0)
{
break; // Connection has been closed
}
else if (retval==SOCKET_ERROR)
{
throw HRException("socket error while receiving.");
}
else
{
// retval is number of bytes read
// Terminate buffer with zero and print as string
tempBuffer[retval] = 0;
cout << tempBuffer;
}
}
Take a look at the call to recv. tempBuffer is the buffer that will receive the data. As the size of the buffer, we specify its actual size minus one. This is because we will put a 0 byte after the last byte received to transform the raw data into a null terminated string we can easily print. Note that in general, it might be perfectly possible to have a 0 byte in the received data since TCP/IP data is not restricted to text. You'll have to treat it as binary data. However, the HTTP protocol does not allow 0 bytes in a HTTP response message (only text) so this won't happen. Even if it would happen, the string would be printed wrong (the 0 byte would be wrongly seen as the terminator) but it isn't likely to happen unless the HTTP server is bad (or the server is not a HTTP server). What this comes down to is that this is just a quick and dirty way to print all the received data that works find for correct HTTP HEAD responses. If you would actually do something with the data more care needs to be taken (for example, a 0 byte in the received data may not be seen as a terminator but indicates a bad HTTP server).
13. Cleaning up
Finally, the socket is closed (if it was created) as shown earlier and the RequestHeaders function will return true or false depending on the success of the function. Back in the main function, winsock will be cleaned up (WSACleanup) and the program quits after printing a last message.
14. Finished!
That's all, the program is finished.
Download the source zip file here: headreq_cpp.zip
The zip file contains the source files and the binary executable.
15. Conclusion
As you can see, blocking sockets are quite easy to use. Their blocking property makes it possible to code a very linear program, winsock operations just happen right where you ask for them. This is different with non-blocking sockets, where you need synchronization because operations do not always complete at once and you can't block. In the next chapter I will explain how to write a simple server with blocking sockets, make sure you understand the client example well as it uses the very basics of winsock.