WWDC2019 - Advances in Networking, Part 1

2024. 1. 24. 18:09iOS/WWDC 파보기

주요 내용 요약

  • Low Data Mode

  • Combine in URLSession

  • WebSocket URLSession and Network framework

  • Network mobility
    Network 프레임워크와 URLSession 2가지 2track으로 진행한다.모티브사이드 프로젝트 채팅 기능에서 WebSocket을 사용하는데, StartScream 대신 직접 구현해보고자 서칭하다가 보게 됨.
    URLSession 기반 WebSocketTask를 중점적으로 봄.

    Low Data Mode

    비용이 많이 드는 expensive mode인 cellular이거나 혼잡한 Wifi 환경일 때 LowData Mode라고 한다.

Always save network data when there is no impact on user experience.

앱을 만들었다를 넘어서서 잘 만든 앱을 만드려면 리소스 최적화도 이루어져야 하나보다.

사용자에게 앱 관점에서 어떻게 적용해볼 수 있을까?

  • Reduce Image quaility
  • Reduce prefeching
  • Synchronize less often
  • Mark tasks discretionary
  • disable Background App Refresh
  • Disable auto-play

이것을 컨트롤할 수 있는 flag로 URLSession에는 다음과 같은 2가지 프로퍼티가 있다.

request.allowsConstrainedNetworkAccess  // lowdata 모드 허용
allowsExpensiveNetworkAccess // expensive network interface 허용 여부

LowData는 뭐고, expensive Network는 뭐지?

lowData 모드는 유저가 Setting App에서 on/off 선택할 수 있는 것
반면에, expensive는 System이 Cellular나 Hotspot Network일 경우 expensive하다고 판단함

Combine in URLSession

설명하기에 앞서 Combine을 사용하는 이유는 뭘까?
바로 선언형으로 비동기 코드를 직관적으로 사용할 수 있기 때문이다. 함수형 람다 메서드를 이용하여 dataflow 작성이 가능하다.
Combine을 이용한 Search API Call을 작성해본다면 다음과 같다. RxSwift와 유사하다.

textField
.publisher()
.debounce(0.2)
.removeDupicates()
.filter { $0.count >= 3 }
.map(toSearchURL)
.sink()

다음은 URLSessionDataTaskPublisher의 interface이다.

public struct DatataskPublisher: Publisher {
    public typealias Output = (data: Data, response: URLResponse)
    public typealias Failure = URLError

tuple로 (data, response)를 방출

Demo: Network 상태에 따른 이미지 로드

Network가 LowMode냐 아니냐에 따라 다른 퀄리티의 이미지를 로드한다. 이 과정을 기존의 closure 방식에서 combine으로 변형하는 것을 보여준다.

위의 코드는 몇가지 단점이 있는데 다음과 같다.

  • task를 실행하는 코드도 중복으로 사용

  • callback처리 때문에 코드를 읽을 때 시선이 위아래로 왔다갔다한다.

  • cell capture하는 걸 까먹는 human error가 발생할 수 있다.

    func adaptiveLoader(session: URLSession = .shared, regularURL: URL, lowDataURL: URL) -> AnyPublisher<Data, Error> {
      var request = URLRequest(url: regularURL)
      request.allowsConstrainedNetworkAccess = false
      return session.dataTaskPublisher(for: request)
        .tryCatch { error -> URLSession.DataTaskPublisher in
          guard error.networkUnavailableReason == .constrained else {
            throw error
          }
          return session.dataTaskPublisher(for: lowDataURL)
        }
        .tryMap { data, response -> Data in
          guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
            throw CustomError.invalidserverResponse
          }
          return data
        }
        .eraseToAnyPublisher()
    }

    기존 closure image binding은 셀을 재활용하면서 이미지가 꼬이는 버그가 있었는데, combine을 활용하여 cell안에 cancellable?을 두고 prepareForReuse()에서 cancel()로 구독을 해제 할 수 있다.

  • 시선이 위에서 아래로 흘러 DataFlow가 직관적임

  • lamda를 이용 DataFlow가 state less하여 안전하고 선언적임

  • 중복코드 없음

  • retry하고 싶을 경우 retry(n)으로 쉽게 가능
    주의해야할 것은 retry의 경우, cost가 많이 들기 때문에 최소한만 해야하고,
    pay같은 경우 기댓값이 달라지기 때문에 주의하여 사용해야한다.(멱등법칙)
    (원문)
    > Support retry

  • Use low retry count

  • Only idempotent(멱등법칙)

    request멱등법칙이란?

    멱등법칙 또는 멱등성은 수학이나 전산학에서 연산의 한 성질을 나타내는 것으로, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다. 멱등법칙의 개념은 추상대수학과 함수형 프로그래밍의 여러 부분에서 사용하고 있다. -wiki-

WebSocekt in URLSession

드디어, 내가 원하는 part까지 왔다.WebSocket

  • 2 way 양방향 통신 프로토콜로 TLS/TCP Connection 위에서 동작한다.
  • 양방향 통신은 HTTP long polling을 거쳐 WebSocket으로 발전했다.
  • long polling은 message 전달을 위해 같은 response / request header가 필요하여 overhead가 크다.
  • javascript API에서 구현 된 것으로 connection을 열어두고 지속적인 통신을 하는 것이다.TLS - Transport Layer Security 란?

SSL에서 보안 취약성을 업그레이드 된 것으로, 현재 혼용하여 사용하고 있다. 따라서, 우리가 알고 있는 SSL은 모두 TLS라고 할 수 있다. 공통점은 모두 서버, 어플리케이션, 사용자와 시스템간의 통신을 암호화하는 프로토콜이다. 주요 차이점으로는 SSL은 보얀 취약점이 있는 md5 알고리즘사용하지만, TLS는 해쉬기반 보안 알로기즘을 사용한다. 출처 AWS 문서

md5 - Message Digest algorithm 5 란?

md5는 무엇이고 왜 취약한 지 알아보자

단방향 알고리즘으로 128비트의 해쉬값을 출력하는 알고리즘이다. 패스워드 암호화에 주로 사용되었다. 속도가 빠르기 때문에 브루트 포스로 같은 해쉬값을 출력하는 collision을 찾아내어 문자열을 찾아내는 취약점이있다. 현재는 네트워크 전송된 파일의 무결성 검증에 주로 쓰인다.

TLS와 SSL 차이점 요약

다시 WebSocket으로 돌아와서, Apple에서는 HTTP 통신을 URLSession을 권장하고 있는데 이번 iOS13에서 WebSocketTask를 추가했다.

URLSession WebSocket

Client에서 쓸 수 있고, foundation에 있기 때문에 매우 가볍다. App Clip에서 활용하기 매우 용이하다.

Netowrk framework WebSocket

Client/Server 모두 support하다!

채팅 예제

Demo: 실시간 가격 변동 예제가 있었으나 코드 실습에 한계가 있어서 아래 예제로 대신한다.
Client: URLSessionWebSocketTask
Server: Network를 이용

Client

protocol WebSocketConnection {
  func connect()
  func disconnect()
  func readMessage()
  func sendPing(handler: @escaping (TimeInterval?) -> Void)
  func send(_ message: MessageDTO)

  var delegate: WebSocketConnectionDelegate? { get set }
}

protocol WebSocketConnectionDelegate: AnyObject {
  func onConnected(connection: WebSocketConnection)
  func onDisconnected(connection: WebSocketConnection, error: Error?)
  func onError(connection: WebSocketConnection, error: Error?)
  func onMesssage(connection: WebSocketConnection, text: String)
  func onMessage(connection: WebSocketConnection, data: Data)
}
  • 메서드 명세를 위해 프로토콜 작성
  • 실제 Event처리를 위한 delegate 작성
  • init(url: URL) { super.init() webSocketTask = URLSession.shared.webSocketTask(with: url) connect() } func connect() { webSocketTask?.resume() readMessage() } func disconnect() { webSocketTask?.cancel(with: .normalClosure, reason: nil) webSocketTask = nil }
  1. URL을 입력받아서 URLSession기반 WebSocketTask를 인스턴스
  2. connect()에서 task resume(). 내부 API에서 handshake 실행. readMessage() 호춯하여 메세지 계속 수신
  3. disconnect()에서 task cacel하고 releasereadMessage()는 apple demo에서 사용한 메서드명인데 개인적으로 listen()이 더 직관적으로 이해된다.

코드 설명하기에 앞서 send와 receive API Interface를 살펴보면 다음과 같다. closure와 aysnc 모두 사용 가능하다.

public enum Message : Sendable { case data(Data) case string(String) }

Sendable이란?

race condition을 해결하기 위해 통신할 때 스이는 data model에게 value type임을 보장하는 protocol이다. 왜 value type이냐하면 ref타입을 넘겨주고 값 변경을 하게 되면 참조된 값들에 영향(= race condition)을 미치기 때문이다. 이를 위해 thread 내에서 통신할 때 seriallize를 하게 되고, 값 타입 확인이 필요하기 때문에 탄생하게 되었다.

-   task.receive closure를 통해 callback을 받아서 data 타입인 것만을 가져온다.(text type을 가져와도 됨) 성공할 경우, delegate 호출하고, readMesage()를 호출한다. recursive하게 만들어서 계속 메세지를 수신한다.. 실패할 경우 disconnect() 호출
-   단순 text전송할 경우 utf8로 인코딩하여 보내고 모델일 경우 encoding하여 data로 전송한다.Server
-   

func readMessage() { 
    guard let task = self.webSocketTask else { return } 
    task.receive { result in 
    switch result { 
    case .success(.data(let data)): 
        self.delegate?.onMessage(connection: self, data: data) 
        self.readMessage() default: self.disconnect() 
        }
    } 
} 
func send(_ message: MessageDTO) { 
    if let data = try? JSONEncoder().encode(message) { 
        webSocketTask?.send(.data(data), completionHandler: { error in 
            if let error = error { 
                self.delegate?.onError(connection: self, error: error) 
                } 
          })
      }
 }
protocol WSNetworkProtocol {  
func connect() throws  
func disconnet()  
func stateDidChanges(to newState: NWListener.State)  
func didAccept(nwConnection: NWConnection)  
}
  • 메서드 명세를 위해 프로토콜 작성
    import Network
    

class SwiftVanilaSocketServer: WSNetworkProtocol {
let port: NWEndpoint.Port
let listener: NWListener
let parameters: NWParameters

private var connectionByID: [Int: ServerConnection] = [:]

init(port: UInt16) {
self.port = NWEndpoint.Port(rawValue: port)!
parameters = NWParameters(tls: nil)
parameters.allowLocalEndpointReuse = true
parameters.includePeerToPeer = true // 피어 통신 flag

let wsoptions = NWProtocolWebSocket.Options()
wsoptions.autoReplyPing = true
parameters.defaultProtocolStack.applicationProtocols.insert(wsoptions, at: 0)
listener = try! NWListener(using: parameters, on: self.port)

}

````

핵심은 NWProtocolWebSocket를 만들어 config하고 파라미터의 프로토콜 스택에 넣는다. 이 파라미터를 조합해서 NWListenr 인스턴스를 생성한다.

func connect() throws {
    print("Server Starting..")
    listener.stateUpdateHandler = self.startDidChanges(to:) // handler 작성, state printing
    listener.newConnectionHandler = self.didAccept(nwConnection:) // "
    listener.start(queue: .main) // release되는 것을 방지하기 위해 메인스레드에서 돌아가게 해야 한다.
  }

  func disconnet() {
    self.listener.stateUpdateHandler = nil
    self.listener.newConnectionHandler = nil
    self.listener.cancel()

    for connection in self.connectionByID.values { // release
      connection.didStopCallback = nil
      connection.stop()
    }
  }
  func didAccept(nwConnection: NWConnection) { // 커넥션 발생할 경우 커넥션 행동 정의 핸들러
    let connection = ServerConnection(nwConnection: nwConnection)
    connectionByID[connection.id] = connection

    connection.start()

    connection.didStopCallback = { [weak self] error in
      if let error = error {
        print(error.localizedDescription)
      }
      self?.connectionDidStop(connection)
    }

    connection.didReceive = { [weak self] data in
      self?.connectionByID.values.forEach { connection in
        print("sent \(String(data: data, encoding: .utf8) ?? "Nothing") to open connection")
        connection.send(data: data)
      }
    }

    connection.send(data: "Welcome you are connection: \(connection.id)".data(using: .utf8)!)
    print("server did open connection\(connection.id)")
  }
  1. 커넥션이 새로 들어오면 커넥션을 메모리에 유지하기 위해 array에 저장한다.

  2. didReceive에서 연결된 모든 커넥션에 메세지를 보낸다. 커넥션 id와 id가 같은 경우 제외할지 말지는 구현 방식에 따라 다른것 같다. 현재는 client에서 화자와 같을 경우, 제외시킨다.

  3. connection도 server와 같이 stateUpdateHandler가 있고, 여기에서 receive 시 문제 없을 경우 recursive호출을 한다.

    // ServerConnect.swift 파일
    // ServerConnect는 connection에 id를 관리하기 위해, NWConnection Wrapper class
    func send(data: Data) {
     let metaData = NWProtocolWebSocket.Metadata(opcode: .binary)
     let context = NWConnection.ContentContext(identifier: "context", metadata: [metaData])
     connection.send(content: data, contentContext: context, isComplete: true, completion: .contentProcessed { [weak self] error in
       guard let self = self else { return }
       if let error = error {
         self.connectionDidFail(error: error)
         return
       }
       print("connection \(self.id) did send, data: \(data as NSData)")
     })
    }
  4. opcode를 바이너리로 준다.
    context를 opcode를 binary로 주고 커넥션에 send에 파라미터로 보냄.

opcode는 WebSocket 통신에 쓰이는 header에 사용되며, 0은 continue로 계속 입력을 받는 것이도, 1은 text와 2는 binary가 있다. 이외의 값은 미래를 위해 예약해둔 값으로 아직 의미는 없다.
Opcode
The opcode denotes the frame type of the WebSocket frame, as
defined in Section 5.2. The opcode is an integer number between 0
and 15, inclusive.

Network Mobility in iOS13

우리는 집 밖에 나갈 때 와이파이를 끄고 나갑니다. 와이파이 신호가 약할 경우, 앱이 느려지는 안 좋은 경험을 했기 때문입니다.

(개인적으로 Mobility대한 세션을 휠체어를 탄 개발자가 발표해서 Apple이 참 대단하다고 느꼈다. 위트 있기도 하고, 역설적인 관계 때문에 세션에 더 집중이 잘 되는 것 같았다.)
Apple은 Wifi를 끄지 않아야 한다고 믿습니다. 즉, Wifi 신호가 약하더라도 어플리케이션 정상적으로 작동해야 한다고 믿습니다.

What is Mobility

네트워크 신호에 따라서 앱이 반응하여 WIfi를 연결하거나 Cellular에 연결해야 합니다. iOS 이전에 여러 시도들이 있었지만, iOS 13에서는 Network framework에 크게 다음의 2가지가 추가되었다.

Wifi Assist

System에서 Network Recovery 과정을 assist와 통신하고, 또 상위 레이어인 Network framework 나 URLSession과 통신합니다.
따라서, 우리의 앱은 더 이상 약한 Wifi 신호 때문에 stuck 상태에 있을 필요가 없습니다.

그래서 우리가 뭘 어떻게 사용할 수 있는데?

  • high-level api like URLSession and Network framework
  • Rethink SCNetworkReachability
  • can control accesss by allowsExpensiveNetworkAccess

    Multipath Transport

    이부분은 Client에서 할 수 있는게 없어 보여 생략함.

실습해본 깃 레포 링크

  • Swift Server 프로젝트
  • Client 프로젝트
  • LowData Mode 프로젝트

'iOS > WWDC 파보기' 카테고리의 다른 글

Apple Design Challenge Part 1 - 컨설팅 후기  (0) 2023.03.15
Apple Design Challenge Part 1  (0) 2023.03.15