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:

.586
.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:

CR  equ 0Dh
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:

CR  equ 0Dh
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:

main proc uses ebx argc:dword, argv:dword
    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:

; Parameters
;   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.

; Parameters
;   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:

; Parameters
;   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:

; Lookup hostname:
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:

; Create socket:
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:

_cleanup:
    ; 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.

; Convert IP to ascii string and print connect message:
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.

invoke  printf, addr g_msgSendReq

; 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:

    ; Receive data in a loop until the connection is closed by
    ; 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.