👋 들어가기 전
위 글에서 나는 채팅을 위한 스프링 서버를 만들었다.
이번에는 Client에서 사용할 웹 소켓을 만들어보자.
이번 프로젝트에서 생각보다 재밌게 진행한 작업이다.
왜냐하면 보통 채팅기능을 위한 웹소캣 통신은 보통 서버와 클라이언트
모두 오픈소스를 활용해서 구현했었는데 부스트 캠프에서는 서드파티를 지양하고 있기 때문에
시간이 걸리더라도 직접 구현하는 것이 부스트 캠프의 도전 정신이라고 생각한다.
🚗 웹 소켓 등장 배경?
웹 소켓이 왜 등장했는 지 살펴보자.
🏋️♀️ 무거운 헤더
웹 소켓 이전에는 실시간 통신을 하기위해 일정한 주기를 통해 서버로 요청을 받고 응답을 받는 "폴링" 이라는
기술을 사용했다. 하지만 일정한 주기를 갖고 확인하는 것이 실시간이라 부를 수 있을까 ??
더 나아가 폴링의 단점을 개선하기위해 서버에서 자체적으로 요청을 기다리는 "Long 폴링"이란 기술도 있지만
결국 두 기술 모두 HTTP 기반으로 동작하기때문에 Request와 Response로 데이터를 주고 받는다.
단순 데이터를 주고 받는데 이 때 불필한 무거운 헤더가 계속 전송된다.
🧱 일반적인 수신
또한 스트리밍이라는 기술 역시 실시간에 사용되었지만 연결에 필요한 요청을 한 이후에는
단방향으로 이벤트를 받기만한다.
우리는 실시간 양방향 채팅을 위한 기술이 필요하기 때문에 연결 이후 단방향은 목적가 부합하지 않는 구조다.
흠 이게 실시간 통신이 맞나 ??? 🤨 그래서 웹소켓 등장! 이번에는 웹소켓이 어떻게 동작하는지와
특징을 간단하게 살펴보자
💡 웹 소켓
✨ 정의
웹 소켓은 HTML5에 등장 실시간 웹 애플리케이션을 위해 설계된 통신 프로토콜이며, TCP를 기반으로 동작한다.
TCP를 기반으로 한 웹 소켓은 신뢰성 있는 데이터 전송을 보장하며, 메시지 경계를 존중하고, 순서가 보장된 양방향 통신을 제공할 수 있다.
이때 데이터는 패킷(packet) 형태로 전달되며, 전송은 연결 중단과 추가 HTTP 요청 없이 양방향으로 이뤄집니다.
⚡️ 특징
- 최초 접속시에만 http프로토콜 위에서 handshaking을 하기 때문에 http header를 사용한다.
- 연결이 된 후 ws 프로토콜에서 동작한다.
- 웹소켓을 위한 별도의 포트는 없고, 기존 포트를 사용한다
- 메시지에 포함될 수 있는 교환 가능한 메시지는 텍스트와 바이너리 뿐이다.
🛜 핸드 쉐이크 reuqst
GET /chat
Host: example
Origin: https://example
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
1. GET
- 반드시 GET 메소드를 사용해야한다
2. Connection: Upgrade
- 클라이언트에서 프로토콜 교체를 요청하는 신호, 앞에서 말했던 것처럼 연결 이후에서는 ws 프로토콜을 사용함
- http(s) -> wws
3. Upgrade: websocket
- 바꾸고 싶은 프로토콜이 websocket이라는 것을 알림
4. Sec-WebSocket-Key
- 보안을 위해 브라우저에서 생성한 키로, 서버가 웹소켓 프로토콜을 지원하는지를 확인하는 데 사용된다.
5. Sec-WebSocket-Version
- 웹 소켓 프로토콜 버전을 명시한다.
🛜 핸드 쉐이크 Response
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBYongNyong24s99EO10UlZ22C2g=
1. 101 Switching Protocols
- 웹 소켓 연결이 성공적으로 연결된 응답 코드
2. Sec-WebSocket-Accept: hs ... ~
- 요청에서의 Key값을 계산한 값으로 신원 인증에 필요한 헤더
✈️ Swift에서 구현하기
먼저 apple에서 기본적으로 제공하는게 있는 지 먼저 찾아보자.
역시 갓 애플 웹소켓을 쉽게 사용할 수 있게 미리 만들어둔 기술들이 있다.
URLSessionWebSocketTask
이름이 너무 이쁘다.. WebSocket 프로토콜과 소통할 수 있는 URL Session이다.
코드와 함께 중요한 부분만 알아보자.
✅ 소켓 열기
public func openWebSocket() throws {
url = URL(string: "ws:/\(host):\(port)/ws/chat" )
guard let url = url else { throw WebSocketError.invalidURL }
let urlSession = URLSession(
configuration: .default,
delegate: self,
delegateQueue: OperationQueue()
)
let webSocketTask = urlSession.webSocketTask(with: url)
webSocketTask.resume()
self.webSocketTask = webSocketTask
self.startPing()
}
먼저 살펴보면 URLSession을 만든 후 webSocketTask를 통해 websocket용 Session을 할당 받는다.
이후 resume() 함수로 handshake를 시작한다.
여기서 startPing함수는 밑에서 설명하겠다.
🚚 데이터 전송하기
public func send(data: ChatMessage) {
guard let data = try? encoder.encode(data) else { return }
let taskMessage = URLSessionWebSocketTask.Message.data(data)
print("Send message \(taskMessage)")
self.webSocketTask?.send(taskMessage) { error in
guard let error = error else { return }
print("WebSOcket sending error: \(error)")
}
}
위에 웹 소켓에 대해 설명할 교환 가능한 데이터(메시지)는 오직 텍스트와 바이너리 뿐이라고 설명했다.
애플 역시 그렇게 설계 되어있다. URLSessionWebSocketTask.Message를 보면
텍스트와 바이너리(데이터)로 되어있는 것을 볼 수 있다.
📧 데이터 받기
public func receive(onReceive: @escaping ((ChatMessage?) -> Void)) {
self.onReceiveClosure = onReceive
self.webSocketTask?.receive { [weak self] result in
print("Receive \(result)")
guard let self else { return }
switch result {
case let .success(message):
switch message {
case let .string(string):
guard let data = string.data(using: .utf8) else {
onReceive(nil)
return
}
let message = try? decoder.decode(ChatMessage.self, from: data)
onReceive(message)
case let .data(data):
let message = try? decoder.decode(ChatMessage.self, from: data)
onReceive(message)
@unknown default:
onReceive(nil)
}
case .failure:
self.closeWebSocket()
return
}
receive(onReceive: onReceive)
}
}
마찬가지로 webScoketTaks.receive 함수를 통해 서버로 부터 데이터를 받는데 그 형태가
string과 바이너리 데이터로 들어온다. 여기서 중요하게 봐야할 부분은 switch 문 가장 아래에
receive 함수가 재귀로 호출되고 있는 점이다.
애플은 receive 콜백을 통하여 한 개의 메시지를 받고나면 등록을 해제하는 특징이다.
그렇기 때문에 다음 메시지를 받기위해 재귀적으로 다시 한번 더 등록을 해줘야 한다.
❌ 소켓 닫기
public func closeWebSocket() {
self.timer?.invalidate()
self.onReceiveClosure = nil
self.delegate = nil
self.webSocketTask?.cancel(with: .goingAway, reason: nil)
self.webSocketTask = nil
}
webSocketTask.cancle 함수를 통해 연결을 취소하고 위호 할당을 해제해준다.
URLSessionWebSocketDelegate
나는 웹소켓이 열리고 닫힐 때에 특정 동작을 하고 싶다. 그러면 그 시점은 어떻게 알 수 있을까??
역시 갓 애플 쉽게 사용할 수 있게 제공해준다.
didOpen.., didClose .. 쪽에 알맞은 핸들링을 해준면된다.
🚧 문제 발생
웹 소켓을 만들던 도중 신기한 버그를 마주쳤다.
위 delegate에 open 이벤트가 들어와 소켓 연결이 재대로 되었는데..
별도의 동작 없이 대기하고 있었는데 갑자기 close가 호출되며 연결이 종료 되었다.
생각 해보자.. 채팅을 장시간 안쳤다고 연결이 끊겨서 메시지를 보내지 못하거나 응답을 못받는게 말이되는 걸까??
여기서 찾아보니 keep-alive 즉, 연결을 유지하기 위해서는 주기적으로 신호를 보내
연결을 유지해야한다. 우리는 이 신호를 ping이라고 한다.
그러면 애플은 ping 관련된 함수가 있을까 ?? 😁 당연히 있다.
private func startPing() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(
withTimeInterval: 10,
repeats: true) { [weak self] _ in
self?.ping()
}
}
private func ping() {
self.webSocketTask?.sendPing { [weak self] error in
guard let error = error else { return }
print("Ping failed \(error)")
self?.startPing()
}
}
webSocketTask.sendPing 함수를 통해 10초에 한번씩 연결이 끊기지 않기 위해 신호를 보낸준다.
위에서 소켓을 열 때 호출한 startPing이 바로 연결을 유지하기 위한 함수이다.
전체적인 코드를 아래 링크를 살펴보자.
✅ 결과
참고
'iOS' 카테고리의 다른 글
[부스트 캠프] 우리만의 네트워크 만들기 (0) | 2024.11.27 |
---|