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)
-
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– nhgrif2014年12月06日 18:53:03 +00:00Commented Dec 6, 2014 at 18:53
1 Answer 1
(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.
-
\$\begingroup\$ Thanks Martin, those are very useful suggestions. I will update accordingly. \$\endgroup\$Rien– Rien2014年12月06日 16:36:01 +00:00Commented Dec 6, 2014 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– Rien2014年12月06日 18:37:23 +00:00Commented Dec 6, 2014 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– Martin R2014年12月06日 18:39:47 +00:00Commented Dec 6, 2014 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– Rien2014年12月07日 11:26:23 +00:00Commented Dec 7, 2014 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– Martin R2014年12月07日 11:37:28 +00:00Commented Dec 7, 2014 at 11:37