관리 메뉴

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

[iOS - swift] RxSwift로 PingPong 로직 구현 방법 (핑퐁 로직, timer) 본문

iOS 응용 (swift)

[iOS - swift] RxSwift로 PingPong 로직 구현 방법 (핑퐁 로직, timer)

jake-kim 2022. 7. 26. 22:18

핑퐁 로직

  • PingPong을 사용하는 케이스는 일반적으로 웹소켓에서 사용
  • 앱에서 서버에게 ping을 보내고, 서버로부터 pong을 받는 로직
    • 만약 서버로부터 pong이 앱에서 설정한 특정 시간안에 안오면 lost connection으로 판단
  • 주의할점
    • 앱에서 서버에서 ping을 보낼때 throttle 적용 (만약 pong이 올때마다 ping을 날리면 짧은순간에 수많은 통신이 이루어짐)

핑퐁 구현 아이디어

  • ping을 보내는 부분
    • 1) API.requestPing 수행 -> response를 받으면 다시 ping을 보내는 코드를 구현
    • 2) 서버에ping을 보낼땐 항상 timer를 사용하여 시간을 체크하고 타이머가 다 된 경우, completion 처리를 수행
  • 타이머는 Swift에서 제공하는 Timer를 사용하지 않고 Observable<Int>.timer(_:scheduler:)를 사용하여 간편하게 구현
    • Swift에서 제공하는 Timer를 사용하면 deinit할때도 timer를 따로 invalidate()시켜줘야 하는 코드가 들어가므로 번거로움
    • Observable의 timer와 disposeBag을 적절히 사용하면 매우 편리하게 구현이 가능

핑퐁 구현에 사용한 라이브러리

핑퐁 구현

  • 사용하는 쪽
    • PingPong()이라는 인스턴스를 이용하여 runPingPong에 closure에 인터넷 연결이 끊긴 경우, completion 처리를 넣어서 사용
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
  var pingPong: PingPong?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    self.pingPong = PingPong()
    self.pingPong?.runPingPong {
      print("lost connection")
    }
  }
}
  • 예제로 사용할 API 구현
enum API {
  static func requestPing() -> Observable<Int> {
    .create { observer -> Disposable in
      print("send ping")
      DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        observer.onNext(Int.random(in: 0...10))
      }
      return Disposables.create()
    }
  }
}
  • PingPong 클래스 선언
class PingPong {
  
}
  • 상수 선언
    • timeOutSeconds: Observable<Int>.timer에 들어갈 값이며, 5초안에 서버로부터 응답이 안오면 타임아웃할때 사용
    • throttleSeconds: 앱에서 서버로 ping을 보낼때 연속적으로 보내지 않고 스로틀링을 위해 적절한 시간에 보내기 위해 사용
  private enum Const {
    static let timeOutSeconds = 5
    static let throttleSeconds = timeOutSeconds - 1
  }
  • disposeBag 선언
    • disposeBag: 해당 인스턴스가 사라질때 자동으로 서버에 ping을 요청했을때 응답을 처리하는 스트림 해제에 사용
    • timeoutDisposeBag: timer를 계속 초기화시켜줄때 사용
  private var disposeBag = DisposeBag()
  private var timeoutDisposeBag = DisposeBag()
  • 외부에서 호출하는 runPingPong 메소드 구현
  func runPingPong(_ completion: @escaping () -> ()) {
    self.sendPing { completion() } // 아래에서 구현 예정
  }
  • sendPing 구현
    • 1) 핑을 보내는 부분
    • 2) 타이아웃을 시작하는 부분 (핵심)
  // 1)
  private func sendPing(_ completion: @escaping () -> ()) {
    API.requestPing()
      .observe(on: ConcurrentDispatchQueueScheduler.init(qos: .background))
      .throttle(.seconds(Const.throttleSeconds), scheduler: ConcurrentDispatchQueueScheduler.init(qos: .background))
      .subscribe(with: self, onNext: { ss, _ in
        ss.receivePong { completion() } // 아래에서 구현
      })
      .disposed(by: self.disposeBag)
    
    // 2)
    Observable<Int>
      .timer(.seconds(Const.timeOutSeconds), scheduler: ConcurrentDispatchQueueScheduler.init(qos: .background))
      .observe(on: ConcurrentDispatchQueueScheduler.init(qos: .background))
      .subscribe(onNext: { _ in
        completion()
      })
      .disposed(by: self.timeoutDisposeBag)
  }
  • receivePong 구현
    • 응답을 받으면 타임아웃을 초기화
    • 다시 sendPing 수행 (반복)
  private func receivePong(_ completion: @escaping () -> ()) {
    print("receive pong")
    self.timeoutDisposeBag = DisposeBag()
    self.sendPing { completion() }
  }

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

Comments