관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] WebSocket 사용 방법 (웹 소켓, URLSessionWebSocketTask, URLSessionWebSocketDelegate) 본문

iOS 응용 (swift)

[iOS - swift] WebSocket 사용 방법 (웹 소켓, URLSessionWebSocketTask, URLSessionWebSocketDelegate)

jake-kim 2022. 9. 10. 23:11

* StarScream을 통해 WebSocket 간단하게 사용 방법은 이전 포스팅 글 참고

WebSockets이란?

  • 클라이언트와 서버 사이의 동적인 양방향 연결 채널(Socket Connection)을 구성
  • WebSockets API를 통해 서버로 메세지를 보내면, 별다른 API 요청 없이 응답을 수신
  • HTTP 통신 방법 vs WebSocket 통신 방법
    • WebSockets 프로토콜: 접속에만 HTTP를 사용하고 그 후 통신은 WebSockets 독자적인 프로토콜을 사용
    • WebSockets은 header가 작기 때문에 overhead가 적은 장점이 존재
  • ex) slack의 실시간 채팅, 금융앱에서 실시간 주가 현황

WebSocket 구현

  • 싱글톤으로 구현하기 위해 shared를 선언하고 url을 외부에서 잘못입력하여 사용할 경우 throw를 던져주기 위해 WebSocketError라는 타입을 정의
    • WebSocket의 delegate는 NSObject타입이어야 하므로, NSObject를 서브클래싱
import Foundation

enum WebSocketError: Error {
  case invalidURL
}

final class WebSocket: NSObject {
  static let shared = WebSocket()
  
  private override init() {}
}
  • 외부에서 접근하는 프로퍼티 정의
    • onReceiveClosure는 웹소켓으로부터 값을 받은 경우 처리를 위해 선언
    • delegate는 외부에서 접근하여 open, close되었을때 이벤트를 처리하기 위함 
  var url: URL?
  var onReceiveClosure: ((String?, Data?) -> ())?
  weak var delegate: URLSessionWebSocketDelegate?
  
  ...
//  delegate 설정은 URLSession을 만들때 설정
      let urlSession = URLSession(
      configuration: .default,
      delegate: self,
      delegateQueue: OperationQueue()
    )
  ...
  
  extension WebSocket: URLSessionWebSocketDelegate {
  func urlSession(
    _ session: URLSession,
    webSocketTask: URLSessionWebSocketTask,
    didOpenWithProtocol protocol: String?
  ) {
    self.delegate?.urlSession?(
      session,
      webSocketTask: webSocketTask,
      didOpenWithProtocol: `protocol`
    )
  }
  
  func urlSession(
    _ session: URLSession,
    webSocketTask: URLSessionWebSocketTask,
    didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
    reason: Data?
  ) {
    self.delegate?.urlSession?(
      session,
      webSocketTask: webSocketTask,
      didCloseWith: closeCode,
      reason: reason
    )
  }
}
  • 내부적으로 사용하는 프로퍼티 선언
  private var webSocketTask: URLSessionWebSocketTask? {
    didSet { oldValue?.cancel(with: .goingAway, reason: nil) }
  }
  private var timer: Timer?
  • 웹소켓을 여는 메소드 정의
    • url을 잘못 입력한 경우에는 throw
    • 서버에 의해 연결이 끊어지지 않도록 주기적으로 ping을 서버에 보내주는 작업도 추가
      (webSocketTask.sendPing메소드가 내부적으로 존재하는것 사용)
  func openWebSocket() throws {
    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()
  }
  
  private func startPing() {
    self.timer?.invalidate()
    self.timer = Timer.scheduledTimer(
      withTimeInterval: 10,
      repeats: true,
      block: { [weak self] _ in self?.ping() }
    )
  }
  private func ping() {
    self.webSocketTask?.sendPing(pongReceiveHandler: { [weak self] error in
      guard let error = error else { return }
      print("Ping failed \(error)")
      self?.startPing()
    })
  }
  • send 메소드 정의
    • URLSessionWebSocketTask의 데이터 전송, 수신 타입은 2가지가 존재
// 내부적으로 정의된 2가지 타입

public enum URLSessionWebSocketTask.Message {
  case data(Data)
  case string(String)
}
  • 2가지 타입으로 메시지를 주고받기때문에 send 메소드는 message, data 두 가지로 분류
    • 메시지를 만들어서, webSocketTask.send(_:completionHandler:)로 전송
  func send(message: String) {
    self.send(message: message, data: nil)
  }
  
  func send(data: Data) {
    self.send(message: nil, data: data)
  }
  
  private func send(message: String?, data: Data?) {
    let taskMessage: URLSessionWebSocketTask.Message
    if let string = message {
      taskMessage = URLSessionWebSocketTask.Message.string(string)
    } else if let data = data {
      taskMessage = URLSessionWebSocketTask.Message.data(data)
    } else {
      return
    }
    
    print("Send message \(taskMessage)")
    self.webSocketTask?.send(taskMessage, completionHandler: { error in
      guard let error = error else { return }
      print("WebSOcket sending error: \(error)")
    })
  }
  • 연결을 종료시키는 closeWebSocket() 메소드 구현
  func closeWebSocket() {
    self.webSocketTask = nil
    self.onReceiveClosure = nil
    self.timer?.invalidate()
    self.delegate = nil
  }

// 위에서 didSet에 cancel()되게끔 해놓았으므로, webSocketTask = nil 하면 cancel도 실행
  private var webSocketTask: URLSessionWebSocketTask? {
    didSet { oldValue?.cancel(with: .goingAway, reason: nil) }
  }

사용하는쪽

  • WebSocket.shared로 접근하여 사용
import UIKit
import Foundation

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    WebSocket.shared.url = URL(string: "ws://localhost:1337/")
    try? WebSocket.shared.openWebSocket()
    WebSocket.shared.delegate = self
    WebSocket.shared.onReceiveClosure = { (string, data) in
      print(string, data)
    }
    
    WebSocket.shared.send(message: "hello world")
  }
}

extension ViewController: URLSessionWebSocketDelegate {
  func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
    print("open")
  }
  func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
    print("close")
  }
}

테스트를 위한 웹소켓 서버 생성

  • 이 포스팅 글에서 사용한 node 웹 서버 오픈 (5분정도 소요)
  • node chat-server.js까지 실행하여 준비

  • 위에서 구현한 코드를 실행하면, hello world가 웹소켓에 찍히는 것을 확인
WebSocket.shared.send(message: "hello world")

* 전체 코드: https://github.com/JK0369/ExURLSessionWebSocketTask

* 참고

https://appspector.com/blog/websockets-in-ios-using-urlsessionwebsockettask

 

Comments