홈으로 돌아가기

iOS에서 POSIX 소켓을 사용한 TCP 서버

이 기사는 POSIX API를 사용한 iOS 앱 내 TCP 서버 구현을 분해하여 설명합니다. Apple의 네트워킹 프레임워크, 소켓 생성, HTTP 파싱 및 파일 서빙이 설명됩니다. 오류 처리와 함께 완전 작동 코드가 제공됩니다.

iOS 내부 TCP 서버: 소켓 완전 가이드
Advertisement 728x90

# iOS 앱에서 POSIX 소켓을 사용한 TCP 서버: 이론에서 실전까지

iOS 개발에서 네트워킹 스택은 고수준 HTTP 요청부터 저수준 BSD 소켓까지 모든 것을 다룹니다. URLSession은 표준 HTTP 및 WebSocket 작업에 이상적이며, Network 프레임워크는 TLS가 적용된 TCP/UDP를 처리하고, CFNetwork은 FTP나 Bonjour 같은 레거시 프로토콜을 지원합니다. POSIX API는 전송 계층에 직접 접근할 수 있게 해주며, SwiftNIO는 Swift에서 서버 측 기능을 추가합니다.

이 모든 도구들은 소켓에 의존합니다. 사용자 정의 프로토콜(SMTP나 IMAP처럼)이나 서버 로직의 경우 전송 계층 접근이 필요합니다. 이 글에서는 POSIX API를 사용한 TCP 서버 구현 과정을 단계별로 안내합니다—최소한의 의존성과 완전한 제어.

POSIX 소켓: 생성과 설정

TCPServer 클래스는 소켓, 포트, 생명 주기를 캡슐화합니다:

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

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

    deinit {
        stop()
    }

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

오류는 강타입으로 정의됩니다:

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

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

start()에서:

  • socket(AF_INET, SOCK_STREAM, 0)은 IPv4 TCP 소켓을 생성합니다. 오류 시 -1을 반환하며 (errno 확인).
  • setsockopt으로 SO_REUSEADDR=1을 설정하면 TIME_WAIT 지연 없이 주소를 재바인딩할 수 있습니다.
var reuseAddr: Int32 = 1
setsockopt(currentSocket, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, socklen_t(MemoryLayout<Int32>.size))

바인딩, 리스닝, 연결 처리

sockaddr_in 주소 구조체를 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)은 최대 10개의 대기 연결을 큐잉합니다.

처리 루프는 백그라운드 스레드에서 실행됩니다:

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 파싱과 파일 서빙

서버는 기본 HTTP/1.1을 구현합니다. 파서는 원시 요청에서 메서드, 경로, 헤더를 추출합니다.

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

GET /files/의 경우 Documents 디렉터리의 파일 목록을 반환합니다:

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

/file/를 통한 다운로드는 효율성을 위해 sendfile을 사용합니다.

응답은 수동으로 구성됩니다:

"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)!

보안과 제한 사항

  • 로컬 네트워크: 서버는 0.0.0.0:8081에 바인딩되며, 기기 IP를 통해 접근 가능합니다.
  • 샌드박스: Documents에만 접근 가능. 시스템 파일은 불가.
  • 타임아웃: 요청당 30초; 오류 시 연결 종료.
  • iOS 제한: 백그라운드 네트워킹은 background-fetch 또는 VoIP 권한이 필요합니다.

핵심 요약:

  • POSIX API는 macOS/Linux와 동일한 범용 저수준 인터페이스입니다.
  • SO_REUSEADDR은 빠른 개발 재시작에 필수입니다.
  • accept()을 메인 스레드를 블로킹하지 않고 비동기 루프에서 실행하세요.
  • HTTP 파싱은 최소화: 메서드 + 경로 + Content-Length.
  • sendfile()은 큰 파일에서 write()보다 우수합니다.

테스트와 디버깅

  • 실행: try server.start()
  • 로컬 접근: http://localhost:8081/files
  • 네트워크 접근: http://192.168.1.100:8081/files
  • 로그: print(String(cString: strerror(errno)))
  • Instruments: 시뮬레이션을 위해 Network Link Conditioner 사용.

이 프로젝트는 소켓부터 웹 인터페이스까지 전체 사이클을 보여줍니다. 코드는 WebSocket이나 사용자 정의 프로토콜로 확장 가능합니다.

— Editorial Team

Advertisement 728x90

다음 읽기