The good news about TCP, and other low-level network protocols, is that even when we’re writing low-level application code, we generally don’t need to implement them ourselves. Instead, our OS provides an implementation, and exposes it to us in the form of system calls and their accompanying C library functions. Although we’ve used syscalls a few times already, this is a good time to discuss some of the technical details.
When our code invokes a syscall, it hands over execution to the operating system. While the OS is working, it executes its own code, in its own private memory address space, until it has a result to return. Some system calls return almost instantly, although there’s still an overhead to switching from our program’s memory, to the OS, and then back again. But many other system calls can take long or indeterminate amounts of time to return, and our program can be totally frozen until they do; when this happens, it’s referred to as a blocking system call, and we’ll learn how to work with them effectively over the next several chapters.
In a straightforward, simple-threaded program, however, blocking system calls can be surprisingly intuitive; in fact, you’ve already used a few in earlier chapters. Although the fgets() and printf() functions I introduced back in Chapter 1, The Basics: Input and Output, are not themselves system calls, these C library functions utilize two of the most basic system calls of all: read() and write(). When used with the standard console inputs and outputs, printf() almost always returns nearly instantly, unless your program is producing output too quickly for your terminal to render, but you may have observed that fgets() is different—when we invoke fgets(), our program waits for a whole line of text to arrive before it returns and allows our program to proceed.
For now, since we’re focused on building an HTTP client for single-threaded applications, we won’t have to think too much about it; in many cases, blocking can even provide performance benefits to the system as a whole, since the OS can schedule other programs for execution while we wait.
In that spirit, let’s look at the system calls we will need to set up a TCP connection.
Before we establish a connection, we need to create a socket with the socket() function. Its signature and arguments look like this:
| def socket(domain:Int, socketType:Int, protocol:Int):Int |
You’ll note that socket() doesn’t return any sort of Socket struct or object—the Int that is returned is, in this case, a file descriptor. The other three arguments are all enums. Although they are integers from the point of view of the compiler, you’ll want to pick one of a few predefined standard values to plug into them. In Scala Native, these are all available if you import the socket bindings. You could write socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) for a TCP socket to an IPv4 address, or socket(AF_INET6, SOCK_DATAGRAM, IPPROTO_UDP) for a UDP socket to an IPv4 address. domain also allows AF_UNIX, which allows you to create objects on the filesystem for local communication over sockets. Likewise, socketType allows you to specify SOCK_RAW for direct access to IP packets, in case you need to implement an alternative transport protocol.
It’s important to remember, however, that no communication occurs when we create the socket—all we’ve done is ask the OS for an object that we can configure for communication. With TCP sockets, the next step is to establish a connection.
For TCP sockets, connect() is responsible for actually establishing a connection to a remote host. By convention, the host that establishes the connection is typically called a client, and the host that receives the connection is called a server. Connect has this signature:
| def connect(socket:Int, serveraddr:Sockaddr, addr_len:socklen_t ):Int |
The first argument to connect() is the uninitialized socket object that we just created with socket(). The second is a pointer to a Sockaddr data structure that contains the numeric IP address to connect to, and the third argument is the size of the sockaddr struct. This raises a few questions.
First, you might wonder why we need the addr_len at all. After all, you learned in Chapter 2, Arrays, Structs, and the Heap, that all CStruct objects have a fixed length and a predetermined memory layout. Why would we need to pass in the length of the address?
We know that connect can take a variety of different kinds of IP addresses. And in the C API’s, each address type actually has its own struct type.
They look like this:
| type in_port_t = uint16_t |
| type in_addr_t = uint32_t |
| |
| // IPv6 addresses are a fixed array of 16 bytes |
| type in6_addr = CStruct1[CArray[uint8_t, _16]] |
| |
| type sockaddr_in = CStruct3[ |
| CUnsignedShort, // sin_family |
| in_port_t, // sin_port |
| in_addr_t, // sin_addr |
| ] |
| |
| type sockaddr_in6 = CStruct5[ |
| in6_addr_t, // sin6_addr |
| CUnsignedShort, // sin6_family |
| in_port_t, // sin6_port |
| uint32_t, // sin6_flowinfo |
| uint32_t] // sin6_scope_id |
| |
| type sockaddr = CStruct2[ |
| sa_family_t, // sa_family |
| CArray[CChar, _14] // sa_data, size = 14 in OS X and Linux |
| ] |
To make it easier to work with these, Scala Native provides helper methods to address the fields by name. Over the next few major releases, addressing struct fields by name is likely to become the standard practice in Scala Native, but it’s limited to these socket-related structs for now.
If you look closely at these structs, though, you’ll notice that they’re shaped differently, and have totally different sizes. In particular, the in6_addr type is much larger than the abstract sockaddr that connect() is expecting. And there are some internal details that make this even worse. OSs like Mac OS and Linux may have different sizes and layouts for these structs as well. So a reasonable programmer could easily wonder how it’s even possible to write safe, portable code with an API like this.
In essence, the way the OS handles this is by taking the addr_len argument that always accompanies a sockaddr, in connect or any other syscall. In other words, the OS function underlying the user-space syscall node knows that you can and will pass data that has a different size than what it’s nominally expecting from the function definition, which allows it to treat them appropriately. Scala Native can also use this to do some transformation and layout shuffling to smooth out the OS differences. However, one side effect of this design is that Scala Native introduces some additional C code between our invocation and the OS.
So, for us to make use of this facility, we’d just have to do a quick cast of a sockaddr_in struct to sockaddr, while passing the correct sizeof[sockaddr_in as the length, like this:
| def connect_ip4(socket:Int, addr:Sockaddr_in):Int = { |
| val sa = addr.cast[Sockaddr] |
| connect(socket, sa, sizeof[Sockaddr_in]) |
| } |
This technique, sometimes called a type pun, is common in C APIs, since C has no notion of inheritance or interface types, but it feels ugly and unsafe. Although we’ll use it a few more times in this book, it’s worth avoiding wherever possible. But the good news is that we can avoid performing this cast manually because the standard C library includes address lookup functions that can do most of the hard work for us—and a lot of other chores, too.
So far, we’ve been working with numeric IP addresses, as represented by the previous structures. But what if we only know the name of the host we want to connect to, like www.pragprog.com?
You may be familiar with the Domain Name Service (DNS), which provides a global, distributed, hierarchical registry of host names. You use it every day when you navigate to pages in a web browser. UNIX systems include a few other lookup mechanisms as well.
For example, if you look at the file /etc/hosts in your Docker container, it has a list of hostnames and IP addresses, like this:
| 127.0.0.1 localhost |
| ::1 localhost ip6-localhost ip6-loopback |
| fe00::0 ip6-localnet |
| ff00::0 ip6-mcastprefix |
| ff02::1 ip6-allnodes |
| ff02::2 ip6-allrouters |
| 172.17.0.2 a970285f20c3 |
So, if we want to look up a hostname, we have a few possible sources. We could have multiple entries in a local file, as well as multiple entries with several remote DNS servers. Rather than implement this logic ourselves, we can rely on our operating system to do the work for us, and combine the results, with the helpful getaddrinfo() function:
| def getaddrinfo(hostname: CString, |
| service: CString, |
| hints: Ptr[Addrinfo], |
| res: Ptr[Ptr[Addrinfo]] ):Int |
In the argument list, hostname is a string containing the hostname to look up; service is a string that usually contains the port number, although it can also look up named services like “echo” in some cases; hints can be null, or else it can contain a partially populated Addrinfo object. Finally, we pass it a pointer to a pointer to an addrinfo object that will hold the result. This is the first time we’ve seen this pattern, so it’s worth looking at closely. Why would we need a pointer to a pointer, and how would we create that pointer in our code?
This is a surprisingly common pattern in C APIs. C only allows a function to return a single value, and has no concept of “exceptions” or other language-level error-handling constructs. Instead, many C functions simply return an integer to signal whether an error has occurred or not; by convention, a return of 0 means “no error,” and various nonzero values signifies different kinds of errors. But if the function just returns an error/no-error code, how do we get the actual address result that we want?
The answer is in the function signature—the Ptr[Ptr[Addrinfo]] argument is called res for result. The outer Ptr is effectively a mutable cell that will contain the result if the syscall completes without error; in this case, since the value we receive is itself a Ptr[Addrinfo], we end up with the somewhat dense Ptr[Ptr[Addrinfo]] as the result type. As a side benefit, this also allows the OS to take responsibility for allocating the appropriate amount of memory for an IPv4 or IPv6 address, which simplifies our implementation a bit.
We’ll see one more trick at play here—when we look at the actual Addrinfo struct:
| type addrinfo = CStruct8[ |
| CInt, // ai_flags |
| CInt, // ai_family |
| CInt, // ai_socktype |
| CInt, // ai_protocol |
| socklen_t, // ai_addrlen |
| Ptr[sockaddr], // ai_addr |
| Ptr[CChar], // ai_canonname |
| Ptr[addrinfo] // ai_next |
| ] |
Fortunately, like with the address types, Scala Native provides field accessors, so we don’t have to remember the offset of each field. You may also notice that not only does the struct contain a string form of the address and a bunch of metadata, it also contains a pointer to another addrinfo struct! The key here is that the value of ai_next is permitted to be null, which allows the structure to function as a linked list of variable length. This is because in many situations, a name can resolve to multiple addresses. When it does, getaddrinfo() will allocate and return multiple Addrinfo objects. It returns a pointer to the first one, and if we need to find more, we can check if the ai_next pointer is null or not. (We’ll always take the first one in this chapter, though.)
Finally, we’ll need to free these addrinfo structures, too. Any function you call that allocates memory for you, whether in a library or a system call, should instruct you as to your responsibilities for freeing that memory when you’re finished with it. In some cases, you may be responsible for calling free() on the returned pointer, and in other cases, there may be another library call to take care of it for you. In this case, the system also provides a helper function for us:
| def freeaddrinfo(ai:Ptr[addrinfo]):Unit |
freeaddrinfo() will also walk through the chain of ai_next values until everything is freed up, so we don’t have to deal with it ourselves.