Skip to content

Instantly share code, notes, and snippets.

@omochi
Created June 17, 2019 11:58
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omochi/3328f80f27325fb27c30dfe3d3a3c645 to your computer and use it in GitHub Desktop.
Save omochi/3328f80f27325fb27c30dfe3d3a3c645 to your computer and use it in GitHub Desktop.

slidenumber: true autoscale: true

iOS13とmacOS CatalinaのWebSocketサポート

参加してなくてもついていけるもん!

WWDCゴリゴリキャッチアップ会 2019

@omochimetaru


WebSocket

  • ウェブブラウザのJavaScriptから使える汎用トランスポートプロトコル

  • リアルタイム双方向通信

    • 利用例: Slackとか

iOS/macOSも対応

iOS13とmacOS CatalinaでWebSocketサポートが追加された

  • Foundation / URLSession

    • Clientのみ
  • Network.framework

    • Server, Client両対応

プロトコルの特徴

  • TCP / TLS 上で動く
  • HTTP / HTTPS と共存できる
  • メッセージベース
  • 信頼性
  • 順序保証
  • プロキシサポート
  • Text / Binary
  • Ping / Pong
  • フレーミング
  • サブプロトコル

URLSession

Client API


接続

open class URLSession {
    open func webSocketTask(with url: URL) 
    	-> URLSessionWebSocketTask

    open func webSocketTask(with url: URL, protocols: [String]) 
    	-> URLSessionWebSocketTask

    open func webSocketTask(with request: URLRequest) 
    	-> URLSessionWebSocketTask
}

open class URLSessionTask {
    open func resume()
}

接続成功/失敗通知

public protocol URLSessionWebSocketDelegate : 
	URLSessionTaskDelegate 
{   
    optional func urlSession(_ session: URLSession, 
    	webSocketTask: URLSessionWebSocketTask, 
    	didOpenWithProtocol protocol: String?)
}

public protocol URLSessionTaskDelegate : URLSessionDelegate {
    optional func urlSession(_ session: URLSession, 
    	task: URLSessionTask, 
    	didCompleteWithError error: Error?)
}

メッセージ送信

extension URLSessionWebSocketTask {
    public enum Message {
        case data(Data)
        case string(String)
    }

    public func send(_ message: URLSessionWebSocketTask.Message,
    	completionHandler: @escaping (Error?) -> Void)
}

メッセージ受信

extension URLSessionWebSocketTask {
    public func receive(
    	completionHandler: 
    	@escaping (Result<URLSessionWebSocketTask.Message, 
                          Error>) -> Void)
}

丁寧な切断

open class URLSessionWebSocketTask {
    open func cancel(with 
    	closeCode: URLSessionWebSocketTask.CloseCode,
    	reason: Data?)
}

丁寧な被切断通知

public protocol URLSessionWebSocketDelegate { 
    optional func urlSession(_ session: URLSession, 
    	webSocketTask: URLSessionWebSocketTask, 
    	didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, 
    	reason: Data?)
}

雑な切断

open class URLSessionTask {
    open func cancel()
}

雑な被切断通知

public protocol URLSessionTaskDelegate {
    optional func urlSession(_ session: URLSession, 
    	task: URLSessionTask, 
    	didCompleteWithError error: Error?)
}

Ping送信

open class URLSessionWebSocketTask {    
    open func sendPing(
    	pongReceiveHandler: @escaping (Error?) -> Void)
}

サブプロトコル

var request: URLRequest = ...
request.addValue("chat", 
	forHTTPHeaderField: "Sec-WebSocket-Protocol")

Network.framework

Client API


接続

final public class NWConnection {
    public init(to: NWEndpoint, using: NWParameters)

	public func start(queue: DispatchQueue)
}

let options = NWProtocolWebSocket.Options()
let parameters = NWParameters.tcp
parameters.defaultProtocolStack.applicationProtocols = [
    options
]
let connection = NWConnection(to: endpoint, using: parameters)

接続成功/失敗通知

final public class NWConnection {
    public enum State {
        case setup
        case waiting(NWError)
        case preparing
        case ready
        case failed(NWError)
        case cancelled
    }

    public var stateUpdateHandler: ((NWConnection.State) -> Void)?
}

送信

final public class NWConnection {
    public enum SendCompletion {
        case contentProcessed((NWError?) -> Void)
        case idempotent
    }

    public func send(content: Data?, 
    	contentContext: NWConnection.ContentContext, 
    	isComplete: Bool, 
    	completion: NWConnection.SendCompletion)
}

let metadata = NWProtocolWebSocket.Metadata(
	opcode: NWProtocolWebSocket.Opcode.binary)
let context = NWConnection.ContentContext(identifier: "context",
                                          metadata: [metadata])

送信

public class NWProtocolWebSocket {
    public enum Opcode : UInt8 {
    	case cont
		case text
		case binary
		case close
		case ping
		case pong
    }
}

Opcodeを使っていろいろと手動実装できるようになっている。


受信

final public class NWConnection {
    public func receiveMessage(
    	completion: @escaping (
    		Data?, 
    		/* context: */ NWConnection.ContentContext?, 
    		/* isCompleted: */ Bool, 
    		NWError?) -> Void)
}

if let metadata = (context?.protocolMetadata
	.compactMap { $0 as? NWProtocolWebSocket.Metadata }.first)
{
    switch metadata.opcode {
    case .text: ...
    case .binary: ...
    ...
    }
}

丁寧な切断

public class NWProtocolWebSocket {
    public class Metadata {
	    public let opcode: NWProtocolWebSocket.Opcode
	    public var closeCode: NWProtocolWebSocket.CloseCode
	}
}

Metadata, Opcode, send, receiveで実装する


雑な切断

final public class NWConnection {
    public func cancel()
}

TCPとしては丁寧な切断


超雑な切断

final public class NWConnection {
    public func forceCancel()
}

Ping

public class NWProtocolWebSocket {
    public class Metadata : NWProtocolMetadata {
	    public let opcode: NWProtocolWebSocket.Opcode
        public func setPongHandler(
        	_ queue: DispatchQueue, 
        	handler: @escaping ((NWError?) -> Void))
    }
}

Metadata, Opcode, send, receiveで実装する pongハンドラはある


Pong

自動実装がある

public class NWProtocolWebSocket {
    public class Options {
	    public var autoReplyPing: Bool
	}
}

サブプロトコル

public class NWProtocolWebSocket {
    public class Options {
    	// クライアントリクエスト
    	public func setSubprotocols(_ subprotocols: [String])
    }
    public class Metadata {
        // サーバ採用
        public var selectedSubprotocol: String? { get }
    }
}

final public class NWConnection {
	// 多分これ
    public func metadata(definition: NWProtocolDefinition) -> NWProtocolMetadata?
}

Network.framework

Server API


サーバ立ち上げ

final public class NWListener {
	public init(using: NWParameters, on: NWEndpoint.Port) throws
    public func start(queue: DispatchQueue)
}

let options = NWProtocolWebSocket.Options()
let parameters = NWParameters.tcp
parameters.defaultProtocolStack.applicationProtocols = [
    options
]
let listener = try NWListener(using: parameters, on: port)

サーバエラー通知

final public class NWListener {
    public enum State {
        case setup
        case waiting(NWError)
        case ready
        case failed(NWError)
        case cancelled
    }

    public var stateUpdateHandler: ((NWListener.State) -> Void)?
}

接続受付

final public class NWListener {
	public var newConnectionHandler: ((NWConnection) -> Void)?
}

NWConnectionが受け取れるので後はClientと同じ。 startの呼び出しが必要なので注意。


サブプロトコル

public class NWProtocolWebSocket {
    public class Options {
        public func setClientRequestHandler(
        	_ queue: DispatchQueue, 
        	handler: @escaping (
        		(
        			/* subprotocols: */ [String], 
        			/* headers: */ [(name: String, value: String)]
        		) -> NWProtocolWebSocket.Response
        	)
        )
    }
}

ネゴシエーションを自作できる


サブプロトコル

public class NWProtocolWebSocket {
    public struct Response {
        public enum Status {
            case accept
            case reject
        }
        public init(status: NWProtocolWebSocket.Response.Status, 
        	subprotocol: String?, 
        	additionalHeaders: [(name: String, value: String)]?)
    }
}

Demo

https://github.com/omochi/WebSocketMacIOSDemo


資料

  • Foundation / URL Loading System https://developer.apple.com/documentation/foundation/url_loading_system

  • Network https://developer.apple.com/documentation/network

  • Advances in Networking, Part 1 https://developer.apple.com/videos/play/wwdc2019/712/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment