관리 메뉴

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

[iOS - swift] Starscream을 이용한 WebSockets (웹 소켓) 사용 방법 본문

iOS framework

[iOS - swift] Starscream을 이용한 WebSockets (웹 소켓) 사용 방법

jake-kim 2022. 1. 8. 00:50

1. Starscream을 이용한 WebSockets (웹 소켓) 사용 방법

2. Starscream을 이용하여 WebSockets (웹 소켓) ping, pong 사용 방법


* URLSessionWebSocketTask를 이용하여 WebSocket 사용 방법은 
이 포스팅 글 참고


<WebSocket 사용하여 실시간 채팅 구현> 좌측(서버) - 사파리 앱, 우측(클라이언트) - 아이폰

WebSockets이란?

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

cf) polling 방식: 클라이언트에서 실시간 업데이트를 위해서 서버에 API 요청을 몇초마다 요청하여 결과값을 얻는 방식
(WebSockets보다 데이터 갱신이 느린 단점 존재)

Starscream 프레임워크

  • iOS내부적으로 제공하는 URLSessionWebSocketTask을 사용하면 웹 소켓을 사용하면 어렵지만, Starscream을 이용하여 웹 소켓을 심플하게 준비하고 사용이 가능
  • Starscream은 내부적으로 GCD를 이용하여 모두 background에서 동작하도록 설계 (NonBlocking)
pod 'Starscream'
pod 'SnapKit' # UI 작업 편리를 위해 사용

WebSockets 테스트를 위해 서버 준비

  • nodejs를 통해 local에 서버 동작 - node가 설치 안되어 있다면, 설치
    • node가 설치되어 있는지 확인
      node --version
    • node 설치 (homebrew를 통해 설치)
      brew install node​
  • npm을 통해 chat서버 구축
    • js파일 준비 - 이곳에서 코드 다운로드 (출처 - raywenderlich)
    • 압축 해제 후, cd/EmojiTransmitter-starter/nodeapp
    • websocket 다운로드
      npm install websocket​


    • 채팅 서버 시작
      # cd nodeapp
      node chat-server.js​
    • safari나 chrome에서 frontend.html 오픈
       
    • 채팅기능 확인
       
  • 위와 같이 준비된 상태라면, 해당 서버와 연결된 유저에게 broadcast로 전달

Starscream 사용방법

예제 프로젝트)

이름 입력 서버에 이모지 전송
  • 이모지 CollectionView가 들어있는 ViewController에서 webSocket 연동

"EmojiViewController.swift" 파일에 아래 부분 모두 작성

 

1. import

import Starscream

2. web secket 초기화

private var socket: WebSocket?

// viewDidLoad에서 호출
private func setupWebSocket() {
  let url = URL(string: "ws://localhost:1337/")!
  var request = URLRequest(url: url)
  request.timeoutInterval = 5
  socket = WebSocket(request: request)
  socket?.delegate = self
  socket?.connect()
}

delegate 준수 (.conected에 client.write로 이름 입력)

extension EmojiViewController: WebSocketDelegate {
  func didReceive(event: WebSocketEvent, client: WebSocket) {
    switch event {
    case .connected(let headers):
      client.write(string: userName)
      print("websocket is connected: \(headers)")
    case .disconnected(let reason, let code):
      print("websocket is disconnected: \(reason) with code: \(code)")
    case .text(let text):
      print("received text: \(text)")
    case .binary(let data):
      print("Received data: \(data.count)")
    case .ping(_):
      break
    case .pong(_):
      break
    case .viabilityChanged(_):
      break
    case .reconnectSuggested(_):
      break
    case .cancelled:
      print("websocket is canclled")
    case .error(let error):
      print("websocket is error = \(error!)")
    }
  }
}

3. message 전송

private func sendMessage(_ message: String) {
  self.title = "메세지 전송"
  socket?.write(string: message)
}

-> 버튼을 탭한 경우 위 메소드호출

@objc private func didTapButton() {
  self.sendMessage(self.informationLabelText)
}

4. 메세지 수신

private func receivedMessage(_ message: String, senderName: String) {
  self.title = "메세지 from (\(senderName))"
  self.informationLabelText = message
}

-> WebSocketDelegate구현 부의 .text 케이스에서 receive 처리

case .text(let text):
  // 4-2
  guard let data = text.data(using: .utf16),
    let jsonData = try? JSONSerialization.jsonObject(with: data, options: []),
    let jsonDict = jsonData as? NSDictionary,
    let messageType = jsonDict["type"] as? String else {
      return
  }
  
  if messageType == "message",
    let messageData = jsonDict["data"] as? NSDictionary,
    let messageAuthor = messageData["author"] as? String,
    let messageText = messageData["text"] as? String {
    self.receivedMessage(messageText, senderName: messageAuthor)
  }

5. connection 해제

deinit {
  socket?.disconnect()
  socket?.delegate = nil
}

완성

WebSocket 구현 완성

* WebSocket이 연동된 EmojiViewController 전체 코드

//
//  EmojiViewController.swift
//  ExWebSockets
//
//  Created by Jake.K on 2022/01/07.
//

import UIKit
// 1
import Starscream

final class EmojiViewController: UIViewController {
  // MARK: Constants
  private enum Metric {
    static let collectionViewItemSize = CGSize(width: 40, height: 40)
    static let collectionViewSpacing = 16.0
    static let collectionViewContentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
  }
  private enum Color {
    static let white = UIColor.white
    static let clear = UIColor.clear
  }

  // MARK: UI
  private let informationLabel: UILabel = {
    let label = UILabel()
    label.textColor = .black
    return label
  }()
  private let sendButton: UIButton = {
    let button = UIButton()
    button.setTitle("이모지 전송", for: .normal)
    button.setTitleColor(.systemBlue, for: .normal)
    button.setTitleColor(.blue, for: .highlighted)
    button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
    return button
  }()
  private let separatorView: UIView = {
    let view = UIView()
    view.backgroundColor = .lightGray
    return view
  }()
  
  // MARK: Priperties
  private let emojis = ["😀", "😬", "😁", "😂", "😃", "😄", "😅", "😆", "😇", "😉", "😊", "🙂", "🙃", "☺️", "😋", "😌", "😍", "😘", "😗", "😙", "😚", "😜", "😝", "😛", "🤑", "🤓", "😎", "🤗", "😏", "😶", "😐", "😑", "😒", "🙄", "🤔", "😳", "😞", "😟", "😠", "😡", "😔", "😕", "🙁", "☹️", "😣", "😖", "😫", "😩", "😤", "😮", "😱", "😨", "😰", "😯", "😦", "😧", "😢", "😥", "😪", "😓", "😭", "😵", "😲", "🤐", "😷", "🤒", "🤕", "😴", "💩"]
  private let collectionView: UICollectionView = {
    let flowLayout = UICollectionViewFlowLayout()
    flowLayout.itemSize = Metric.collectionViewItemSize
    flowLayout.minimumInteritemSpacing = Metric.collectionViewSpacing
    flowLayout.minimumLineSpacing = Metric.collectionViewSpacing
    let view = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
    view.register(EmojiCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    view.contentInset = Metric.collectionViewContentInset
    view.showsHorizontalScrollIndicator = false
    view.backgroundColor = Color.clear
    return view
  }()
  private var informationLabelText: String = "" {
    didSet {
      self.informationLabel.attributedText = NSMutableAttributedString()
        .resize(string: self.informationLabelText, fontSize: 120)
    }
  }
  private let userName: String
  private var socket: WebSocket?
  
  init() {
    self.userName = UserDefaults.standard.string(forKey: "name") ?? ""
    super.init(nibName: nil, bundle: nil)
  }
  
  // 5
  deinit {
    socket?.disconnect()
    socket?.delegate = nil
  }
  
  @available(*, unavailable)
  required init?(coder: NSCoder) {
    fatalError()
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    self.title = "이모지 선택"
    
    self.view.backgroundColor = Color.white
    self.view.addSubview(self.collectionView)
    self.view.addSubview(self.informationLabel)
    self.view.addSubview(self.sendButton)
    self.view.addSubview(self.separatorView)
    
    self.collectionView.snp.makeConstraints {
      $0.left.right.equalToSuperview()
      $0.top.equalTo(self.view.safeAreaLayoutGuide).inset(250)
      $0.bottom.equalTo(self.view.safeAreaLayoutGuide)
    }
    self.informationLabel.snp.makeConstraints {
      $0.top.equalTo(self.view.safeAreaLayoutGuide).inset(32)
      $0.centerX.equalToSuperview()
    }
    self.sendButton.snp.makeConstraints {
      $0.top.equalTo(self.collectionView.snp.top).offset(-50)
      $0.centerX.equalToSuperview()
    }
    self.separatorView.snp.makeConstraints {
      $0.height.equalTo(1)
      $0.bottom.equalTo(self.collectionView.snp.top).offset(1)
      $0.left.right.equalToSuperview()
    }
    
    self.collectionView.dataSource = self
    self.collectionView.delegate = self
    
    // 2 connect to web socket server
    self.setupWebSocket()
  }
  
  override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)
    self.view.endEditing(true)
  }
  
  private func setupWebSocket() {
    let url = URL(string: "ws://localhost:1337/")!
    var request = URLRequest(url: url)
    request.timeoutInterval = 5
    socket = WebSocket(request: request)
    socket?.delegate = self
    socket?.connect()
  }
  
  @objc private func didTapButton() {
    // 3-2
    self.sendMessage(self.informationLabelText)
  }
  
  // 3-1
  private func sendMessage(_ message: String) {
    self.title = "메세지 전송"
    socket?.write(string: message)
  }
  
  // 4-1
  private func receivedMessage(_ message: String, senderName: String) {
    self.title = "메세지 from (\(senderName))"
    self.informationLabelText = message
  }
}

extension EmojiViewController: UICollectionViewDelegate, UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    self.emojis.count
  }
  
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! EmojiCollectionViewCell
    cell.prepare(emoji: emojis[indexPath.item])
    return cell
  }
  
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let message = emojis[indexPath.item]
    self.informationLabelText = message
  }
}

extension EmojiViewController: WebSocketDelegate {
  func didReceive(event: WebSocketEvent, client: WebSocket) {
    switch event {
    case .connected(let headers):
      client.write(string: userName)
      print("websocket is connected: \(headers)")
    case .disconnected(let reason, let code):
      print("websocket is disconnected: \(reason) with code: \(code)")
    case .text(let text):
      // 4-2
      guard let data = text.data(using: .utf16),
        let jsonData = try? JSONSerialization.jsonObject(with: data, options: []),
        let jsonDict = jsonData as? NSDictionary,
        let messageType = jsonDict["type"] as? String else {
          return
      }
      
      if messageType == "message",
        let messageData = jsonDict["data"] as? NSDictionary,
        let messageAuthor = messageData["author"] as? String,
        let messageText = messageData["text"] as? String {
        self.receivedMessage(messageText, senderName: messageAuthor)
      }
    case .binary(let data):
      print("Received data: \(data.count)")
    case .ping(_):
      break
    case .pong(_):
      break
    case .viabilityChanged(_):
      break
    case .reconnectSuggested(_):
      break
    case .cancelled:
      print("websocket is canclled")
    case .error(let error):
      print("websocket is error = \(error!)")
    }
  }
}

 

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

 

* 참고

https://www.raywenderlich.com/861-websockets-on-ios-with-starscream

https://github.com/daltoniam/Starscream

https://www.raywenderlich.com/861-websockets-on-ios-with-starscream

Comments