7
\$\begingroup\$

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<Int8>.null(),
    ai_addr: UnsafeMutablePointer<sockaddr>.null(),
    ai_next: UnsafeMutablePointer<addrinfo>.null())


// For the result from the getaddrinfo

var servinfo = UnsafeMutablePointer<addrinfo>.null()


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

status = getaddrinfo(UnsafePointer<Int8>.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<addrinfo>.null() {

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

            var ipAddressString = [CChar](count:Int(INET_ADDRSTRLEN), 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!
            log.atLevelDebug(source: SOURCE, object: nil, message: message)

        } else { // Should be IPv6

            var ipAddressString = [CChar](count:Int(INET6_ADDRSTRLEN), repeatedValue: 0)
            inet_ntop(
                info.memory.ai_family,
                info.memory.ai_addr,
                &ipAddressString,
                socklen_t(INET6_ADDRSTRLEN))
            let addressStr = String.fromCString(&ipAddressString)
            var message = addressStr == nil ? "No IPv6 address found" : "IPv6 address: " + addressStr!
            log.atLevelDebug(source: SOURCE, object: nil, message: message)
        }

        info = info.memory.ai_next
    }
}


// Get the socket descriptor, use the results from the getaddrinfo call earlier

var socketDescriptor = socket(servinfo.memory.ai_family, servinfo.memory.ai_socktype, servinfo.memory.ai_protocol);

log.atLevelDebug(source: SOURCE, object: nil, message: "Socket value: " + socketDescriptor.description)


// Since we will be listening, bind the socket descriptor to our socket

status = bind(socketDescriptor, servinfo.memory.ai_addr, servinfo.memory.ai_addrlen);

log.atLevelDebug(source: SOURCE, object: nil, message: "Status from binding: " + status.description)


// Cop out if there is an error

// PS: Need to handle "in use" errors

if status != 0 {
    let message = "Binding error, status = " + status.description + ", error = " + errno.description
    log.atLevelEmergency(source: SOURCE, object: nil, message: message)
    setApplicationError(ErrorCode.initPortForListeningBindingError, message)
    return
}


// Don't need this anymore

freeaddrinfo(servinfo)
\$\endgroup\$

migrated from stackoverflow.com Dec 6 '14 at 13:37

This question came from our site for professional and enthusiast programmers.

  • 3
    \$\begingroup\$ On CodeReview, we shouldn't edit questions to incorporate the suggestions from answers. If you'd like further review after incorporating these suggestions, you're welcome to post another question. But when you modify your code, you are invalidating answers. \$\endgroup\$ – nhgrif Dec 6 '14 at 18:53
8
\$\begingroup\$

(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<T>, 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<addrinfo>.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<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)

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<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 ?? "?")
}

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.

\$\endgroup\$
  • \$\begingroup\$ Thanks Martin, those are very useful suggestions. I will update accordingly. \$\endgroup\$ – Rien Dec 6 '14 at 16:36
  • \$\begingroup\$ When I ran this new code, it reported to be listening on 0.0.0.0, i.e. on all interfaces. \$\endgroup\$ – Rien Dec 6 '14 at 18:37
  • \$\begingroup\$ @Rien: That is correct because you have specified no host name in getaddrinfo(). Btw. that should behave identical to your original code. Perhaps you did not notice it because your usage of inet_ntop() was wrong and produced a wrong output. \$\endgroup\$ – Martin R Dec 6 '14 at 18:39
  • \$\begingroup\$ re: wrong usage. Yes I think so too, I also believe that this means that it is not necessary to start listening on all interfaces seperately. \$\endgroup\$ – Rien Dec 7 '14 at 11:26
  • \$\begingroup\$ @Rien: Why separately? You have one socket bound to the "any" address. – But this discussion is going to be off-topic for this site. I initiated the migration from Stack Overflow to Code Review because I thought that your question is mainly about reviewing and improving existing code (and I still think that is true). If you have additional questions how to create a listening socket correctly then I would suggest to start a new question on Stack Overflow. \$\endgroup\$ – Martin R Dec 7 '14 at 11:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.