Now that we have a little background on HTTP and formats for its request and response payloads, let's try to build an HTTP server from scratch using Swift and some C libraries that we can access via Swift. The point of this exercise is to learn how to build a very basic HTTP web server so we have a better understanding of how all of these web servers are built using sockets. Getting a full stack view is helpful in case you need to dive into low-level code to debug an issue or fix a bug in a package or library you might be using to build our web server. You might also be curious on how to code your own simple web servers, it's actually fairly easy. To create a web server, let's try the following steps:
- Import the C libraries in Swift:
import Darwin.C
- Create a socket using the socket system call. Sockets are a way for other hosts on the network to connect to this process:
let sock = socket(AF_INET, SOCK_STREAM, 0)
- Create a socket address structure and initialize it with host and port information. Then call the bind system call with the socket address structure and bind the server to the localhost on the port specified:
bind(sock, sockaddrPtr, socklen_t(socklen))
- Listen for incoming requests by calling listen and specifying the max number of requests to be added to the queue to be served by our process:
listen(sock, 5)
- Now, we can accept incoming connections by calling accept with the socket file descriptor for our socket that clients connect to. It will remove requests from the listen queue and return a new client socket connection:
let client = accept(sock, nil, nil)
- We can read from the new client socket connection and send data to it using HTTP Protocol:
let html = "<!DOCTYPE html><html><body><h1>Hello from Swift Web Server.</h1></body></html>"
let httpResponse: String = """
HTTP/1.1 200 OK
server: simple-swift-server
content-length: \(html.count)
\(html)
"""
httpResponse.withCString { bytes in
send(client, bytes, Int(strlen(bytes)), 0)
}
- Close the connection using the close system call:
close(client)
That was a quick overview of how network-based programs work and how our web server will work as well. Now, let's look at the code as a whole:
import Darwin.C
let zero = Int8(0)
let transportLayerType = SOCK_STREAM // TCP
let internetLayerProtocol = AF_INET // IPv4
let sock = socket(internetLayerProtocol, Int32(transportLayerType), 0)
let portNumber = UInt16(4000)
let socklen = UInt8(socklen_t(MemoryLayout<sockaddr_in>.size))
var serveraddr = sockaddr_in()
serveraddr.sin_family = sa_family_t(AF_INET)
serveraddr.sin_port = in_port_t((portNumber << 8) + (portNumber >> 8))
serveraddr.sin_addr = in_addr(s_addr: in_addr_t(0))
serveraddr.sin_zero = (zero, zero, zero, zero, zero, zero, zero, zero)
withUnsafePointer(to: &serveraddr) { sockaddrInPtr in
let sockaddrPtr = UnsafeRawPointer(sockaddrInPtr).assumingMemoryBound(to: sockaddr.self)
bind(sock, sockaddrPtr, socklen_t(socklen))
}
listen(sock, 5)
print("Server listening on port \(portNumber)")
repeat {
let client = accept(sock, nil, nil)
let html = "<!DOCTYPE html><html><body style='text-align:center;'><h1>Hello from <a href='https://swift.org'>Swift</a> Web Server.</h1></body></html>"
let httpResponse: String = """
HTTP/1.1 200 OK
server: simple-swift-server
content-length: \(html.count)
\(html)
"""
httpResponse.withCString { bytes in
send(client, bytes, Int(strlen(bytes)), 0)
close(client)
}
} while sock > -1
To run our server, let's take look at the following steps:
- Create a Swift file and call it simple-server.swift.
- Copy the preceding code into the file.
- Run the code using the Swift command and pass the file name as the first argument, as follows:
$ swift simple-server.swift
- The Swift compiler will try to compile the contents of simple-server.swift and run it in one command. You should see the following printed in the Terminal when the server has started:
Server listening on port 4000
- Open the browser and go to http://localhost:4000. You will see the response from our Swift simple HTTP server replying back with HTML content:
The example code works only on macOS but you can easily make it work in Linux by using import Glibc instead of import Darwin.C and changing some of the datatypes that are passed in creation of a socket. Swift supports some of the C directives, such as #if, #elsif, #else, and #endif, to help include or skip code blocks before compiling on certain platforms, such as Linux or macOS, where a certain feature or API usage may be different. In our case, since we depend on C-based libraries, we'd need to import Glibc when OS is Linux or import Darwin.C and also set different types for two variables we use in our code, as follows:
#if os(Linux)
import Glibc
let zero = UInt8(0)
let transportLayerType = SOCK_STREAM.rawValue // TCP
#else
import Darwin.C
let zero = Int8(0)
let transportLayerType = SOCK_STREAM // TCP
#endif