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:
gethostbyname PROTO name:DWORD
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:
.model flat, stdcall
option casemap:none
include <windows.inc>
include <kernel32.inc>
include <user32.inc>
include <crtlib.inc>
include <ws2_32.inc> ; the winsock 2 include
includelib <kernel32.lib>
includelib <user32.lib>
includelib <crtlib.lib>
includelib <ws2_32.lib> ; the winsock 2 library
main proto stdcall :dword, :dword
.code
; startup code
start:
sub esp, 12
lea eax, [esp+0] ; &env
lea ecx, [esp+4] ; &argc
lea edx, [esp+8] ; &argv
invoke getmainargs, ecx, edx, eax, 0
add esp, 4 ; remove env (not used)
call main
invoke ExitProcess, eax
main proc uses ebx argc:dword, argv:dword
; main program
main endp
end start
The example uses some of microsoft's C runtime library functions to get the program's parameters and output data. These functions reside in crtlib.dll, a default windows installation already includes this DLL so there's no need to distribute it. You do need the library and include, which are included in the zip file with the source code.
The startup code sets up the parameters for the main function, where the main program will be in. The argc parameter contains the number of parameters (note that the program's name also counts as a parameter), argv points to an array of string pointers, each containing one parameter. People familiar with C(++) will probably be familiar with this.
Also note the winsock includes, ws2_32.inc and ws2_32.lib. These are necessary for winsock 2.
5. Constants and global data
The program will need some constants and global data, which we define in the following code snippet:
LF equ 0Ah
g_defaultServerName db "www.google.com",0
SERVER_PORT equ 80
TEMP_BUFFER_SIZE equ 128
REQ_WINSOCK_VER equ 2
g_request_part1 db "HEAD / HTTP/1.1",CR,LF ; Get root index from server
db "Host: " ; Specify host name used
REQUEST_SIZE1 equ $ - g_request_part1 ; size of request part1 in bytes
g_request_part2 db CR,LF ; End hostname header from part1
db "User-agent: HeadReqSample",CR,LF ; Specify user agent
db "Connection: close",CR,LF ; Close connection after response
db CR,LF ; Empty line indicating the end of the request
REQUEST_SIZE2 equ $ - g_request_part2 ; size of request part2 in bytes
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). The REQUEST_SIZE1 and REQUET_SIZE2 constants are calculated by taking the first address after the request data, which is the current address at that point (represented by a dollar sign $), and subtracting the start address of the request data from it. Note that the strings are not null-terminated, since we don't need that. Only the actual request text needs to be send, without the null terminator. Finally there's a set of messages and error strings that are used throughout the program:
LF equ 0Ah
g_msgLookupHost db "Looking up hostname %s... ",0
g_msgFound db "found.",CR,LF,0
g_msgCreateSock db "Creating socket... ",0
g_msgCreated db "created.",CR,LF,0
g_msgConnect db "Attempting to connect to %s:%d... ",0
g_msgConnected db "connected.",CR,LF,0
g_msgSendReq db "Sending request... ",0
g_msgReqSent db "request sent.",CR,LF,0
g_msgDumpData db "Dumping received data...",CR,LF,CR,LF,0
g_msgInitWinsock db "Initializing winsock... ",0
g_msgInitialized db "initialized.",CR,LF,0
g_msgDone db "done.",CR,LF,0
g_msgCleanup db "Cleaning up winsock... ",0
g_errHostName db "could not resolve hostname.",CR,LF,0
g_errCreateSock db "could not create socket.",CR,LF,0
g_errConnect db "could not connect.",CR,LF,0
g_errSend db "failed to send data.",CR,LF,0
g_errRead db "socket error while receiving.",CR,LF,0
g_errStartup db "startup failed!",0
g_errVersion db "required version not supported!",0
g_errCleanup db "cleanup failed!",CR,LF,0
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:
local wsaData:WSADATA
invoke printf, addr g_msgInitWinsock
invoke WSAStartup, REQ_WINSOCK_VER, addr wsaData
mov ecx, offset g_errStartup
test eax, eax
jnz _error
; Check if major version (low byte) is at least REQ_WINSOCK_VER
cmp byte ptr [wsaData.wVersion], REQ_WINSOCK_VER
mov ecx, offset g_errVersion
jb _error_cleanup
invoke printf, addr g_msgInitialized
; Check if hostname is given as the program's parameter,
; otherwise use the default hostname.
mov ecx, offset g_defaultServerName
cmp [argc], 2 ; at least 1 argument?
mov eax, [argv] ; get argument vector
jb @F
mov ecx, [eax][1*4] ; get first argument
@@:
invoke RequestHeaders, ecx
mov ebx, eax
xor ebx, 1
; ebx now is 0 on success, 1 on failure of RequestHeaders
_cleanup:
invoke printf, addr g_msgCleanup
invoke WSACleanup
test eax, eax
jz _done
invoke printf, addr g_errCleanup
_done:
invoke printf, addr g_msgDone
mov eax, ebx ; return code in ebx
ret
_error_cleanup:
mov ebx, _cleanup
jmp _printError
_error:
mov ebx, _done
_printError:
invoke printf, ecx
mov eax, ebx
mov ebx, 1 ; return 1 (error)
jmp eax
main endp
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:
; pServername pointer to a string containing the server
; name to perform a HTTP request on.
; Return value
; 0: failed
; 1: succeeded
RequestHeaders proc uses ebx esi pServername:dword
local tempBuffer[TEMP_BUFFER_SIZE]:byte,
sockAddr:sockaddr_in
; hSocket:dword = esi
; initialize socket value to prevent cleaning up
; on error with socket creation.
mov esi, INVALID_SOCKET
; ---- code goes here -----
_cleanup:
; do cleaning up
mov eax, ebx
ret
_error:
invoke printf, ecx
xor ebx, ebx ; return code (0 = error)
jmp _cleanup
RequestHeaders endp
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. Because the socket handle is often used, it is put in the esi register instead of a local variable. The socket handle is initialized to INVALID_SOCKET, the only value that can't be used as a socket handle. The exit point of the function is the ret after _cleanup. If everything goes fine, the code will fall through to _cleanup and the cleanup code will do its job. This code assumes the return value is in ebx before it is executed, it will copy it to eax after cleaning up. I used ebx instead of eax because eax would have been trashed by the API calls that are going to be used for the cleanup. If an error occurs anywhere in the code, the error message is put in ecx and the _error code gets executed. This code simply prints the error, sets ebx to 0 (return code 'false') and jumps to the cleanup code, since cleanup still might have to be done.
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.
; pServerName pointer to a string containing the server
; name to resolve the IP number for.
; Return value
; IP number in network byte order or NULL if the hostname
; was not found.
FindHostIP proc uses ebx pServerName:dword
invoke gethostbyname, [pServerName]
test eax, eax
jz _return
; eax is a pointer to a HOSTENT structure now,
; get address list pointer in list:
mov eax, [(hostent ptr [eax]).h_list]
test eax, eax
jz _return
; get first address pointer in list:
mov eax, [eax]
test eax, eax
jz _return
; get first address from pointer
mov eax, [eax]
; eax is IP number now, fall through so IP gets returned
_return:
ret
FindHostIP endp
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 member of hostent points to a null-terminated array of other pointers. Each pointer points in that array points to an address. 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:
; pSockAddr pointer to the sockaddr_in structure to fill
; pServerName pointer to a string containing the server
; name to address
; portNumber address port number
; Return value
; 0: host lookup failed
; not 0: function succeeded
FillSockAddr proc pSockAddr:dword, pServerName:dword, portNumber:dword
invoke FindHostIP, [pServerName]
test eax, eax
jz _done
mov edx, [pSockAddr]
mov ecx, [portNumber]
xchg cl, ch ; convert to network byte order
mov [edx][sockaddr_in.sin_family], AF_INET
mov [edx][sockaddr_in.sin_port], cx
mov [edx][sockaddr_in.sin_addr.S_un.S_addr], eax
_done:
ret
FillSockAddr endp
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. If an error occurs in the FindHostIP function (eg. host not found), the function returns 0. Otherwise it returns another value (the IP number actually).
Back to the RequestHeaders function we call FillSockAddr to fill in our local sockaddr_in structure with the right information:
mov ebx, [pServername]
invoke printf, addr g_msgLookupHost, ebx
; Find server and fill sockAddr structure with its
; information
invoke FillSockAddr, addr sockAddr, ebx, SERVER_PORT
mov ecx, offset g_errHostName
test eax, eax
jz _error
invoke printf, addr g_msgFound
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:
invoke printf, addr g_msgCreateSock
invoke socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
mov ecx, offset g_errCreateSock
cmp eax, INVALID_SOCKET
je _error
mov esi, eax
invoke printf, addr g_msgCreated
If socket fails, it returns INVALID_SOCKET. In that case, no further operations are performed and the following cleanup code (at _cleanup) is executed directly:
; close socket if it was created:
mov eax, esi
cmp eax, INVALID_SOCKET
je @F
invoke closesocket, eax
@@:
mov eax, ebx
ret
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.
invoke inet_ntoa, [sockAddr.sin_addr.S_un.S_addr]
invoke printf, addr g_msgConnect, eax, SERVER_PORT
; Attempt to connect:
invoke connect, esi, addr sockAddr, sizeof sockAddr
mov ecx, offset g_errConnect
test eax, eax
jnz _error
invoke printf, addr g_msgConnected
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
invoke send, esi, addr g_request_part1, REQUEST_SIZE1, 0
mov ecx, offset g_errSend
cmp eax, SOCKET_ERROR
je _error
; send hostname
invoke lstrlen, [pServername]
invoke send, esi, [pServername], eax, 0
mov ecx, offset g_errSend
cmp eax, SOCKET_ERROR
je _error
; send request part 2
invoke send, esi, addr g_request_part2, REQUEST_SIZE2, 0
mov ecx, offset g_errSend
cmp eax, SOCKET_ERROR
je _error
; all sends succeeded
invoke printf, addr g_msgReqSent
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:
; the server.
invoke printf, addr g_msgDumpData
_recvLoop:
invoke recv, esi, addr tempBuffer, TEMP_BUFFER_SIZE-1, 0
test eax, eax
mov ecx, offset g_errRead
jz _connectionClosed ; return value 0 means connection closed
cmp eax, SOCKET_ERROR
je _error
; eax is number of bytes received, add a null
; terminator and print the buffer:
mov [tempBuffer][eax], 0
invoke printf, addr tempBuffer
jmp _recvLoop
_connectionClosed:
mov ebx, 1 ; return code (1 = no error)
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_asm.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.