본문 바로가기

TIL

[TIL] Socket 통신 구현

지난번 게시글에서는 Socket에 대해 간단하게 정리해 보았습니다.

이번 글에서는 Socket 통신을 직접 구현한 내용에 관해 살펴보겠습니다.

구현된 화면은 아래와 같습니다.

구현 화면

소켓 구현 설명 

소켓으로 구현시 개발자의 역할은 데이터가 적절한 시점에 서버와의 연결시켜주었다가 적절한 시점에 연결을 해제하는 것이 중요합니다.

 

우선 가장 중요한 Socket을 구성하는 코드부터 살펴보겠습니다.(API는 업비트 API를 사용하였습니다.)

 

WebSocket 연결

아래 코드는 Socket을 연결하는 코드입니다.

아래 코드를 보시면 URLSession를 default session으로 구성하였는데 그 이유는 Delegate를 통해 WebSocket에 대한 연결 상태에 대해 확인하기 위해서입니다.

나머지 코드는 일반적으로 URLSession에서 네트워크 통신을 구성할 때 사용되는 방식과 동일하다는 것을 알 수 있습니다.

 

여기서 아래 코드를 보시면 ping() 메서드가 있는데 이에 대한 것은 추후 아래에서 상세히 설명해보도록 하겠습니다.

{
	//...
    
	// WebSocketManager.swift
    func openWebSocket() {
        if let url = URL(string: "wss://api.upbit.com/websocket/v1") {

            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)

            webSocket = session.webSocketTask(with: url)
            webSocket?.resume()

            ping() // 핑 테스트 구현부
        }
    }
    
    //...
}

 

WebSocket 연결 해제

아래 코드는 WebSocket 연결 해제 관한 코드입니다.

 

webSocket?.cancel(with: .goingStay, reason: nil) 구문은 WebSocket 연결 취소하는 부분입니다. 역기서 .goingAway는 열거형 WebSocket.CloseCode 에 속한 코드로, 서버나 클라이언트가 "사라짐"을 의미하며, 일반적으로 정상적인 연결 종료, 예를 들어 애플리케이션을 종료할 때 주로 사용됩니다.

 

webSocket = nil 문은 webSocket 연결에 대한 인스턴스를 완전히 해제하는 구문입니다.

 

그 외에 timer?.invalidate(), timer = nil 와 같은 구문들은 위에 WebSocket 연결을 구성하는 메서드에서 핑테스트를 위한 타이머를 해제해주는 구문입니다.

 

그리고 마지막으로 isOpen 구문은 WebSocket이 연결되었는지 그 상대값을 나타내주는 플래그(Flag)를 나타냅니다.

{
	// ...
    
    // WebSocketManager.swift
    func closeWebSocket() {
        webSocket?.cancel(with: .goingAway, reason: nil)
        webSocket = nil
        
        timer?.invalidate()
        timer = nil
        
        isOpen = false
    }
    
    // ...    
}

 

Socket 통신 연결/해제 후 Delegate 구성

아래 코드는 WebSocket 통신 연결 및 해제가 완료되었을 알려주는 Delegate를 구성한 부분입니다.

 

urlSession(_:webSocketTask:didOpenWithProtocol:) 메서드 내부를 보시면 두 가지 구문이 있는데요.

isOpen의 경우, Socket 통신 연결 상태에 대한 플래그(Flag)이고,

recevieSocketData() 메서드는 Socket 통신 연결 후, 서버에서 보내온 요청을 받기위해 대기(Listening)하는 메서드입니다. 이 메서드에 대해선 아래 더 자세히 다뤄보겠습니다.ㅇㄹdflsdf.aㄴ

ㅇ리ㅏㅓㄴ이랴ㅓ니댜ㅓㄹ

urlSession(_:webSocketTask:didCloseWith:reason:) 메서드는 Socket 통신 연결이 해제된 직후, 호출되는 Delegate입니다.

이때 isOpen = false를 설정하여 Socket 통신 연결이 해제되었음을 나타내고 있습니다.

// WebSocketManager.swift
extension WebSocketManager: URLSessionWebSocketDelegate {
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        print("Socket Open")
        isOpen = true
        recevieSocketData()
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("Socket Close")
        isOpen = false
    }
}

 

WebSocket을 통한 메세지 전송

아래 구문은 연결된 WebSocket을 통해 메세지를 전송해주는 부분입니다.

extension WebSocketManager {
    
    func send(_ string: String) {
        webSocket?.send(.string(string), completionHandler: { error in
            print("Send Error")
        })
    }

	//...
}

 

WebSocket을 통해 받는 서버로부터의 요청

아래 코드는 Socket 통신을 통해 서버로부터 전달받은 요청을 듣고(Listening)하고 있는 부분입니다.

아래 코드를 보시면 completionHandler 클로저를 통해 결과값을 받아와 디코딩(Decoding)후, 해당 데이터를 orderbookSbj라는 Publisher에 보내고 있습니다.

 // WebSocketManager.swift
 {
    var orderbookSbj = PassthroughSubject<OrderBook, Never>()
    // ...
    
    func recevieSocketData() {
        if isOpen {
            webSocket?.receive(completionHandler: { result in
                switch result {
                case .success(let success):
                    switch success {
                    case .data(let data):
                        if let decodedData = try? JSONDecoder().decode(OrderBook.self, from: data) {
                            dump(decodedData)
                            
                            self.orderbookSbj.send(decodedData)
                        }
                    case .string(let string):
                        print(string)
                    @unknown default: print("Unknown Default")
                    }
                case .failure(let failure):
                    print("failure", failure)
                }
                // 재귀로 구성해하야 socket 연결 유지
                self.recevieSocketData()
            })
        }
    }
    
    // ...
}

 

핑 테스트 구성

아래 코드는 핑 테스트를 구현한 부분입니다.

핑 테스트 구성이 중요한 이유는 Socket 통신 연결 후, 서버와의 연결이 살아있는지 아닌지 확인해보기 위해서입니다.

// WebSocketManager.swift
{
	private var timer: Timer?
    
    func openWebSocket() {
        if let url = URL(string: "wss://api.upbit.com/websocket/v1") {
            
            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
            
            webSocket = session.webSocketTask(with: url)
            webSocket?.resume()
            
            ping() // Socket 연결시 서버에 핑(ping) 전송
        }
    }
    
    // ...
    
    // 핑 테스트 구현부
    func ping() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { [weak self] _ in
            self?.webSocket?.sendPing(pongReceiveHandler: { error in
                if let error = error {
                    print("ping pong error", error.localizedDescription)
                } else {
                    print("ping ping ping")
                }
            })
        })
    }
}

 

전체 코드

전체 코드는 다음과 같습니다.

import Foundation
import Combine

final class WebSocketManager: NSObject {
    static let shared = WebSocketManager()
    
    private var webSocket: URLSessionWebSocketTask?
    private var isOpen = false
    
    private var timer: Timer?
    
    var orderbookSbj = PassthroughSubject<OrderBook, Never>()
    
    private override init() {}
    
    func openWebSocket() {
        if let url = URL(string: "wss://api.upbit.com/websocket/v1") {
            
            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
            
            webSocket = session.webSocketTask(with: url)
            webSocket?.resume()
            
            ping()
        }
    }
    
    func closeWebSocket() {
        webSocket?.cancel(with: .goingAway, reason: nil)
        webSocket = nil
        
        timer?.invalidate()
        timer = nil
        
        isOpen = false
    }
}

extension WebSocketManager: URLSessionWebSocketDelegate {
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        print("Socket Open")
        isOpen = true
        recevieSocketData()
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("Socket Close")
        isOpen = false
    }
}

extension WebSocketManager {
    
    func send(_ string: String) {
        webSocket?.send(.string(string), completionHandler: { error in
            print("Send Error")
        })
    }
    
    func recevieSocketData() {
        if isOpen {
            webSocket?.receive(completionHandler: { result in
                switch result {
                case .success(let success):
                    switch success {
                    case .data(let data):
                        if let decodedData = try? JSONDecoder().decode(OrderBook.self, from: data) {
                            dump(decodedData)
                            
                            self.orderbookSbj.send(decodedData)
                        }
                    case .string(let string):
                        print(string)
                    @unknown default: print("Unknown Default")
                    }
                case .failure(let failure):
                    print("failure", failure)
                }
                // 재귀로 구성해하야 socket 연결 유지
                self.recevieSocketData()
            })
        }
    }
    
    func ping() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { [weak self] _ in
            self?.webSocket?.sendPing(pongReceiveHandler: { error in
                if let error = error {
                    print("ping pong error", error.localizedDescription)
                } else {
                    print("ping ping ping")
                }
            })
        })
    }
}

 

긴 글 읽어주셔서 감사합니다.