Back to Home

TCP server in iOS on POSIX sockets

The article breaks down the implementation of a TCP server in an iOS app using POSIX API. Apple's networking frameworks, socket creation, HTTP parsing and file serving are described. Full working code with error handling is provided.

TCP Server inside iOS: full guide to sockets
Advertisement 728x90

TCP Server in an iOS App Using POSIX Sockets: From Theory to Practice

In iOS development, the networking stack covers everything from high-level HTTP requests to low-level BSD sockets. URLSession is ideal for standard HTTP and WebSocket operations, the Network framework handles TCP/UDP with TLS, and CFNetwork supports legacy protocols like FTP and Bonjour. The POSIX API provides direct access to the transport layer, while SwiftNIO adds server-side capabilities in Swift.

All these tools rely on sockets. For custom protocols (like SMTP or IMAP) or server logic, you need transport-layer access. This article walks you through implementing a TCP server using the POSIX API—minimal dependencies and full control.

POSIX Sockets: Creation and Setup

The TCPServer class encapsulates the socket, port, and lifecycle:

Google AdInline article slot
final class TCPServer {
    let port: UInt16

    init(port: UInt16 = 8081) {
        self.port = port
    }

    deinit {
        stop()
    }

    func start() throws { }
    func stop() { }
}

Errors are strongly typed:

enum SocketError: LocalizedError {
  case socketCreationFailed(errno: Int32)
  case bindFailed(errno: Int32)
  case listenFailed(errno: Int32)
  case writeFailed(errno: Int32)
  case writeTimeout

  var errorDescription: String? { /* ... */ }
}

In start():

  • socket(AF_INET, SOCK_STREAM, 0) creates an IPv4 TCP socket. It returns a descriptor or -1 on error (check errno).
  • setsockopt with SO_REUSEADDR=1 allows rebinding the address without TIME_WAIT delays.
var reuseAddr: Int32 = 1
setsockopt(currentSocket, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, socklen_t(MemoryLayout<Int32>.size))

Binding, Listening, and Handling Connections

The sockaddr_in address structure is populated for INADDR_ANY (0.0.0.0):

Google AdInline article slot
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = in_port_t(port.bigEndian)
addr.sin_addr.s_addr = in_addr_t(0)

bind(currentSocket, sockaddr_cast(&addr), socklen_t(MemoryLayout<sockaddr_in>.size))

listen(currentSocket, 10) queues up to 10 pending connections.

The handling loop runs in a background thread:

DispatchQueue.global().async { [weak self] in
    while let server = self, server.isRunning {
        var clientAddr = sockaddr_in()
        var len = socklen_t(MemoryLayout<sockaddr_in>.size)
        
        let clientSocket = accept(server.currentSocket, sockaddr_cast(&clientAddr), &len)
        // Handle clientSocket
    }
}

HTTP Parsing and File Serving

The server implements basic HTTP/1.1. The parser extracts the method, path, and headers from the raw request.

Google AdInline article slot
struct HTTPRequest {
    let method: String
    let path: String
    let headers: [String: String]
    let body: Data?
}

For GET /files/, it returns a list of files from the Documents directory:

<html><body><h1>App Files</h1><ul>
<li><a href="/file/filename">filename</a></li>
</ul></body></html>

Downloads via /file/: uses sendfile for efficiency.

The response is built manually:

"HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: \(data.count)\r\n"
+ "\r\n" + String(data: data, encoding: .utf8)!

Security and Limitations

  • Local network: The server binds to 0.0.0.0:8081, accessible via the device's IP.
  • Sandbox: Access limited to Documents. No system files.
  • Timeout: 30s per request; close on errors.
  • iOS limitations: Background networking requires background-fetch or VoIP entitlements.

Key takeaways:

  • POSIX API is a universal low-level interface, identical to macOS/Linux.
  • SO_REUSEADDR is essential for quick dev restarts.
  • Run accept() in an async loop without blocking the main thread.
  • HTTP parsing is minimalist: method + path + Content-Length.
  • sendfile() outperforms write() for large files.

Testing and Debugging

  • Launch: try server.start()
  • Local access: http://localhost:8081/files
  • Network access: http://192.168.1.100:8081/files
  • Logs: print(String(cString: strerror(errno)))
  • Instruments: Use Network Link Conditioner for simulations.

The project demonstrates the full cycle: from socket to web interface. The code is extensible for WebSocket or custom protocols.

— Editorial Team

Advertisement 728x90

Read Next