WebSocketManager
싱글톤 패턴URLSessionWebSocketTask
, URLSessionWebSocketDelegate
// 응답 데이터 모델
struct OrderBookWS: Decodable {
let timestamp: Int
let totalAskSize, totalBidSize: Double
let orderbookUnits: [OrderbookUnit]
enum CodingKeys: String, CodingKey { /*...*/ }
}
struct OrderbookUnit: Codable {
let askPrice, bidPrice, askSize, bidSize: Double
enum CodingKeys: String, CodingKey { /*...*/ }
}
// 뷰 데이터 모델
struct OrderBookItem: Hashable, Identifiable {
let id = UUID()
let price: Double
let size: Double
}
struct Market: Codable, Hashable {
let id = UUID()
let market: String
let koreanName: String
let englishName: String
enum CodingKeys: String, CodingKey { /*...*/ }
}
class WebSocketManager: NSObject
static let shared = WebSocketManager()
private override init() {
super.init()
}
private var timer: Timer? // 5초에 한 번씩 Ping 보내기 위한 타이머
private var webSocket: URLSessionWebSocketTask?
private var isOpen = false // 소켓 연결 상태
var orderBookSbj = PassthroughSubject<OrderBookWS, Never>()
// RxSwift PublishSubject -> Combine PassthroughSubject
// Rx는 데이터 타입만 설정 -> Combine은 에러 타입도 설정
1. open
didOpen
으로 연결 확인func openWebSocket() {
if let url = URL(string: "wss://api.upbit.com/websocket/v1") {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
// -> URLSessionWebSocketDelegate 프로토콜 채택
webSocket = session.webSocketTask(with: url)
webSocket?.resume()
ping()
}
}
2. close
소켓 해제 -> delegate의 didClose
로 해제 확인
다양한 CloseCode가 존재하는데 주로 .goingAway
와 messageTooBig
사용
VM, VC와 따로 동작하는 timer 관리 중요 (Keyword : run loop)
func closeWebSocket() {
webSocket?.cancel(with: .goingAway, reason: nil)
webSocket = nil
timer?.invalidate()
timer = nil
isOpen = false
}
3. send
소켓 통신을 통해 받고 싶은 데이터를 요청 포맷에 맞춰서 요청
func send(_ codes: String) {
let requestStr = """
[{"ticket":"test"},{"type":"orderbook","codes":["\(codes)"]}]
"""
webSocket?.send(.string(requestStr), completionHandler: { error in
if let error { print("send Error : \(error.localizedDescription)") }
})
}
4. receive
필요한 순간에 서버에서 데이터를 받는다
func receive() {
if isOpen { // 소켓이 열렸을 때만 데이터 수신이 가능하도록 한다
webSocket?.receive(completionHandler: { [weak self] result in
switch result {
case .success(let success):
print("receive Success : \(success)")
switch success {
case .data(let data):
print("success - data : \(data)")
do {
let decodedData = try JSONDecoder().decode(OrderBookWS.self, from: data)
// RxSwift .onNext -> Combine .send
self?.orderBookSbj.send(decodedData)
} catch {
print("decodingError : \(error.localizedDescription)")
}
case .string(let string):
print("success - string : \(string)")
@unknown default:
fatalError()
}
case .failure(let failure):
print("receive Fail : \(failure.localizedDescription)")
self?.closeWebSocket() // 소켓 데이터가 제대로 오지 않기 때문에, 닫아준다
}
// recursive
self?.receive()
})
}
}
5. ping
서버에 의해 연결이 끊어지지 않도록 주기적으로 ping을 보낸다
(120초 Idle Timeout)
선언해둔 timer 활용
private func ping() {
self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { [weak self] _ in
self?.webSocket?.sendPing(pongReceiveHandler: { error in
if let error {
print("Ping Error : \(error.localizedDescription)")
} else {
print("Ping Success")
}
})
})
}
extension WebSocketManager: URLSessionWebSocketDelegate
URLSessionWebSocketDelegate
를 타고타고 올라가보면,NSObjectProtocol
을 채택하고 있다.WebSocketManager
에서 NSObjectProtocol
의 필수 프로퍼티와 메서드를 구현해야 한다.NSObject
를 WebSocketManger
가 상속받도록 한다소켓이 연결되었을 때와 해제되었을 때 실행되는 메서드
extension WebSocketManager: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
print(#function)
print("WebSocket OPEN")
isOpen = true
receive() // 데이터 수신 시작
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
print(#function)
print("WebSocket CLOSE")
isOpen = false
}
}
class SocketTestViewModel: ObservableObject
var marketData: Market // 어떤 코인인지
@Published var askOrderBook: [OrderBookItem] = [] // 매도 정보
@Published var bidOrderBook: [OrderBookItem] = [] // 매수 정보
// RxSwift disposeBag -> Combine cancellable
private var cancellable = Set<AnyCancellable>()
init
소켓 연결. 즉, VM 인스턴스를 만들면 바로 소켓 통신이 시작된다
init(market: Market) {
self.marketData = market
WebSocketManager.shared.openWebSocket()
WebSocketManager.shared.send(marketData.market)
// Rx subscribe -> Combine sink
// Rx Scheduler(.main) -> Combine receive
// Rx Dispose -> Combine AnyCancellable
WebSocketManager.shared.orderBookSbj
.receive(on: DispatchQueue.main)
.sink { [weak self] order in
guard let self else { return }
self.askOrderBook = order.orderbookUnits
.map { .init(price: $0.askPrice, size: $0.askSize)}
.sorted { $0.price > $1.price }
self.bidOrderBook = order.orderbookUnits
.map { .init(price: $0.bidPrice, size: $0.bidSize)}
.sorted { $0.price > $1.price }
}
.store(in: &cancellable)
}
deinit
deinit
함수가 실행되지 않으면 소켓은 영원히 해제되지 않는다. 주의하기deinit {
WebSocketManager.shared.closeWebSocket()
}
잘 보고 갑니당 :)