💊 소켓이란?
소켓은 네트워크 상의 두 장치 간에 데이터 전송 통로를 마련하는 인터페이스이다. 이렇게 말하면 ?? 가 떠오를 수 있는데 소켓을 단순하게 말하면 문자열 데이터(패킷)을 주고받는 것이다. 소켓은 통신에 필요한 데이터를 보관하는 곳(파일)이라고도 볼 수 있다.
하지만 마구잡이로 보낼수 없으니 어떠한 구조로 보낼지 약속을 한다. 이것을 프로토콜이라고 하는데 소켓은 네트워크 통신에서 TCP와 UDP라는 두 가지 주요 프로토콜을 사용한다. TCP, UDP에 대해서 간단히 요약을 하자면... 연결, 비연결이라고 볼 수 있을 것 같다.
TCP (Transmission Control Protocol)
데이터 전송을 보장하는 연결 기반 프로토콜이다. 데이터의 순서를 보장하고 패킷 손실이 없기 때문에 데이터가 손실없이 가지만 이를 위해 확인 응답, 재전송 정렬 등의 추가적인 작업이 들어가기 때문에 UDP에 비해 속도가 느리다. 클라이언트와 서버가 통신할 준비가 되었는지 확인 후 (보통 3-way handshake) 데이터 전송을 시작한다.
UDP (User Diagram Protocol)
빠른 데이터 전송을 위한 비연결형 프로토콜이다. 데이터가 순서대로 도착하지 않을 수 있고 패킷 손실이 발생할 수 있지만, 속도가 중요한 경우에 적합하다. 연결 설정 과정이 없기 때문에 비연결형 프로토콜이라고 한다.
TCP | UDP | |
연결 방식 | 연결 기반(3-way handshake) | 비연결형 |
신뢰성 | 데이터 전송 보장 (재전송, 손실 방지) | 데이터 전송 보장 없음 (손실 가능) |
순서 보장 | 데이터 송신 순서 보장 | 순서 보장 없음 |
속도 | 상대적으로 느림 | 빠름 |
보통 UDP를 영상 스트리밍, 온라인 게임에 많이 쓴다고 하는데, 영상 전송 프로토콜인 RTMP의 경우에는 TCP를 사용하기 때문에 케이스별로 정책별로 많이 달라지는 것 같다. 하지만 파일 다운로드나 이메일 등 데이터의 무결성이 보장되어야 하는 경우에는 TCP를 사용하는게 필수인 것 같다. "안전하고 순서 있는" 데이터 전송을 위해 TCP, "빠르고 간단한" 데이터 전송을 위해 UDP를 사용하면 될 것 같다.
🦜 Swift로 TCP 소켓 구현해보기
간단하게 setup, send, receive, close메서드를 활용하여 구현해 보았다. 여기서 소켓의 핵심?은 recieve를 재귀 호출하여 계속 연결을 유지하는 것이다.
import Foundation
import Network
final class TCPSocket {
private var connection: NWConnection?
/// 지정된 host(서버 주소)와 port(서버 포트)로 연결을 설정
func setup(host: String, port: UInt16) {
let nwEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!)
connection = NWConnection(to: nwEndpoint, using: .tcp)
/// 연결 상태 감지
connection?.stateUpdateHandler = { state in
switch state {
case .ready: print("연결: host: \(host) port: \(port)")
case .failed(let error): print("연결 실패: \(error)")
default: break
}
}
/// 데이터를 수신할 준비
receive()
connection?.start(queue: .main) // 실제 연결 시작
}
/// 서버로 문자열 메시지를 전송
func send(message: String) {
let data = message.data(using: .utf8) ?? Data()
connection?.send(content: data, completion: .contentProcessed({ error in
if let error = error {
print("전송 실패: \(error)")
} else {
print("전송: \(message)")
}
}))
}
/// 서버로부터 메시지를 수신
private func receive() {
connection?.receive(minimumIncompleteLength: 1, maximumLength: 1024) { data, _, isComplete, error in
if let data = data, !data.isEmpty {
let message = String(data: data, encoding: .utf8) ?? ""
print("수신: \(message)")
}
if isComplete || error != nil {
self.connection?.cancel()
print("연결 닫힘")
} else {
/// 연결이 유지되는 동안 계속해서 receive()를 재귀 호출
self.receive()
}
}
}
/// 연결 종료
func close() {
connection?.cancel()
}
}
소켓 테스트
위의 작성한 코드가 잘 작동 할까? 라는 의문이 생겨 어떻게 테스트 해볼 수 있을까? 생각해봤다. 아래와 같이 아주 간단한 뷰를 만들어 테스트를 진행해 보았다. nc -l 8080 명령어를 통해 간단하게 로컬서버를 실행해 볼 수 있었다.
버튼을 누르면 Hello, Server!가 전송되도록 했더니 서버에서 Hello, Server!를 받아볼 수 있었다.
Control + C 를 통해 서버를 닫으니 바로 연결 닫힘을 확인해 볼 수 있었다.
어떻게 TCP로 연결이 되었냐 하면... NWConnection을 사용하여 TCP 연결을 설정할 때 3-way handshake는 자동으로 수행된다고 한다. 다음은 3-way handshake에 대해서 알아보려고 한다.
🦜 Swift로 UDP 소켓 구현해보기
UDP는 아주 간단하다. TCP와 같이 연결 필요없이 보내기만 하면 된다.
final class UDPSocket {
private var connection: NWConnection?
func send(host: String, port: UInt16, message: String) {
let nwEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!)
let data = message.data(using: .utf8) ?? Data()
connection = NWConnection(to: nwEndpoint, using: .udp)
connection?.send(content: data, completion: .contentProcessed({ error in
if let error = error {
print("전송 실패: \(error)")
} else {
print("전송: \(message)")
}
}))
connection?.start(queue: .main)
}
}
nc -u -l 8080 명령어를 통해 서버를 열 수 있는데, 하나의 데이터그램을 수신하고 나면 프로그램이 종료된다고 한다.
'→ Computer Science' 카테고리의 다른 글
[RTMP] RTMP 프로토콜 개요 (7) | 2024.11.08 |
---|---|
[CS] TCP/UDP 전송계층 (0) | 2024.11.07 |
[CS] 정지문제 (Halting problem) (0) | 2024.04.08 |