HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Pure Swift solution for socket programming

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
swiftpureprogrammingforsolutionsocket

Problem

I have spend quite some time now to find out how exactly to do socket programming in Swift (Xcode 6.1). There are a few examples out there, but most of them no longer work for the latest release of Swift.

Finally I came up with the following (see below). That seems to work, but I am not 100% certain that it is fully correct. Maybe some of you can have a look at it and see if I made any errors that might cause future (i.e. currently under the hood) problems.

The calls to "log...." refer to a self made logging facility, but that should be clear enough I think.

Any suggestions on how to improve this are welcome.

```
var status: Int32 = 0

// Protocol configuration

var hints = addrinfo(
ai_flags: AI_PASSIVE, // Assign the address of my local host to the socket structures
ai_family: AF_UNSPEC, // Either IPv4 or IPv6
ai_socktype: SOCK_STREAM, // TCP
ai_protocol: 0,
ai_addrlen: 0,
ai_canonname: UnsafeMutablePointer.null(),
ai_addr: UnsafeMutablePointer.null(),
ai_next: UnsafeMutablePointer.null())

// For the result from the getaddrinfo

var servinfo = UnsafeMutablePointer.null()

// Get the info we need to create our socket descriptor

status = getaddrinfo(UnsafePointer.null(), ap_servicePortNumber, &hints, &servinfo)

// Cop out if there is an error

if status != 0 { return }

// Print a list of the found IP addresses

if ap_applicationInDebugMode {

var info = servinfo

while info != UnsafeMutablePointer.null() {

// Check for IPv4
if info.memory.ai_family == AF_INET {

var ipAddressString = CChar, repeatedValue: 0)
inet_ntop(
info.memory.ai_family,
info.memory.ai_addr,
&ipAddressString,
socklen_t(INET_ADDRSTRLEN))
let addressStr = String.fromCString(&ipAddressString)
var message = addressStr == nil ? "No IPv4 address found" : "IPv4 address: " + addressStr!

Solution

(Remark: I am using println() for diagnostic output in this review instead of your
log.atLevelDebug(). The reason is that I wanted to test the code before posting
an answer.)

That seems to be working code (with one exception, see below), but there are some things that can be simplified
or improved.

nil is a possible value for any UnsafeMutablePointer, so you can simplify your
initialization, for example:

var hints = addrinfo(
    ai_flags: AI_PASSIVE,       // Assign the address of my local host to the socket structures
    ai_family: AF_UNSPEC,       // Either IPv4 or IPv6
    ai_socktype: SOCK_STREAM,   // TCP
    ai_protocol: 0,
    ai_addrlen: 0,
    ai_canonname: nil,
    ai_addr: nil,
    ai_next: nil)


You traverse the address list from getaddrinfo with

var info = servinfo
while info != UnsafeMutablePointer.null() {
    // ...
    info = info.memory.ai_next
}


Writing this as a for-loop might be easier to read:

for (var info = servinfo; info != nil; info = info.memory.ai_next) {
    // ...
}


Your printing of the numerical IP addresses with inet_ntop() does not work correctly,
the second argument must be the address of a struct in_addr/in6_addr
and not the address of struct sockaddr, for example:

var sin4addr = UnsafePointer(info.memory.ai_addr).memory.sin_addr
var ipAddressString = [CChar](count:Int(INET_ADDRSTRLEN), repeatedValue: 0)
inet_ntop(
    info.memory.ai_family,
    &sin4addr,
    &ipAddressString,
    socklen_t(INET_ADDRSTRLEN))
let addressStr = String.fromCString(&ipAddressString)


But the more modern interface is getnameinfo() (which works for both IPv4 and IPv6
addresses). Written as a separate utility function:

func sockaddrDescription(addr: UnsafePointer) -> String
{
    var host : String?
    var port : String?

    var hostBuffer = [CChar](count: Int(NI_MAXHOST), repeatedValue: 0)
    var servBuffer = [CChar](count: Int(NI_MAXSERV), repeatedValue: 0)

    if (getnameinfo(addr, socklen_t(addr.memory.sa_len),
                    &hostBuffer, socklen_t(hostBuffer.count),
                    &servBuffer, socklen_t(servBuffer.count),
                    NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
            host = String.fromCString(hostBuffer)
            port = String.fromCString(servBuffer)
    }
    return "host: " + (host ?? "?") + " port: " + (port ?? "?")
}


which you could use as

for (var info = servinfo; info != nil; info = info.memory.ai_next) {
    let addrInfo = info.memory 

    let desc = sockaddrDescription(addrInfo.ai_addr)
    println(desc)
}


Instead of errno you could log strerror(errno) if any of the socket functions fails.
The error message string would be more instructive than a error number. For example:

status = bind(...)
if status == -1 {
    println("bind failed: " + String(UTF8String: strerror(errno))!)
}


Your function prints all found IP addresses in debug mode, but binds the socket
only to the first one. The recommended practice (if I remember correctly, and I will
try to find a reference) is to

  • try all IP addresses until socket/bind/listen is successful, or



  • try to listen on all IP addresses.



is successful. In the first case, your main loop would look like

var socketDescriptor : Int32 = -1 // Invalid socket

for (var info = servinfo; info != nil; info = info.memory.ai_next) {
    let addrInfo = info.memory 

    let sd = socket(addrInfo.ai_family, addrInfo.ai_socktype, addrInfo.ai_protocol)
    if sd == -1 {
        println("socket failed: " + String(UTF8String: strerror(errno))!)
    } else {
        status = bind(sd, addrInfo.ai_addr, addrInfo.ai_addrlen)
        if status == -1 {
            println("bind failed: " + String(UTF8String: strerror(errno))!)
        } else {
            status = listen(sd, 1)
            if status == -1 {
                println("listen failed: " + String(UTF8String: strerror(errno))!)
            } else {
                // SUCCESS !!
                socketDescriptor = sd
                break
            }
        }
        close(sd)
    }
}


and then socketDescriptor is -1 or a valid socket descriptor which is bound and
listening.

The more Swifty way however would be to define an optional

var socketDescriptor : Int32?


which is set to a valid socket descriptor or stays nil in the error case.

Code Snippets

var hints = addrinfo(
    ai_flags: AI_PASSIVE,       // Assign the address of my local host to the socket structures
    ai_family: AF_UNSPEC,       // Either IPv4 or IPv6
    ai_socktype: SOCK_STREAM,   // TCP
    ai_protocol: 0,
    ai_addrlen: 0,
    ai_canonname: nil,
    ai_addr: nil,
    ai_next: nil)
var info = servinfo
while info != UnsafeMutablePointer<addrinfo>.null() {
    // ...
    info = info.memory.ai_next
}
for (var info = servinfo; info != nil; info = info.memory.ai_next) {
    // ...
}
var sin4addr = UnsafePointer<sockaddr_in>(info.memory.ai_addr).memory.sin_addr
var ipAddressString = [CChar](count:Int(INET_ADDRSTRLEN), repeatedValue: 0)
inet_ntop(
    info.memory.ai_family,
    &sin4addr,
    &ipAddressString,
    socklen_t(INET_ADDRSTRLEN))
let addressStr = String.fromCString(&ipAddressString)
func sockaddrDescription(addr: UnsafePointer<sockaddr>) -> String
{
    var host : String?
    var port : String?

    var hostBuffer = [CChar](count: Int(NI_MAXHOST), repeatedValue: 0)
    var servBuffer = [CChar](count: Int(NI_MAXSERV), repeatedValue: 0)

    if (getnameinfo(addr, socklen_t(addr.memory.sa_len),
                    &hostBuffer, socklen_t(hostBuffer.count),
                    &servBuffer, socklen_t(servBuffer.count),
                    NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
            host = String.fromCString(hostBuffer)
            port = String.fromCString(servBuffer)
    }
    return "host: " + (host ?? "?") + " port: " + (port ?? "?")
}

Context

StackExchange Code Review Q#71861, answer score: 8

Revisions (0)

No revisions yet.