Files
ConnectionKit-mirror/ConnectionKit/Classes/SocketConnection.swift
2018-12-19 17:44:13 +02:00

208 lines
6.9 KiB
Swift

//
// SocketConnection.swift
// ConnectionKit
//
// Created by Georges Boumis on 16/06/16.
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
import Foundation
#if canImport(CocoaAsyncSocket) && canImport(RepresentationKit)
import CocoaAsyncSocket
import RepresentationKit
/// A socket connection based on TCP. Uses `GCDAsyncSocket` for the heavy
/// lifting.
///
/// A `SocketConnection` always transfer `Data` through the wire. So the owner
/// of the connection should provide the `DataRepresentation` of the objects
/// to send throught the `SocketConnection`.
public final class SocketConnection: NSObject, Connection {
// MARK: - Interface
/// The host that the receiver connects to.
final public let host: Host
/// The port of the `host` the receiver connects to.
final public let port: Port
/// The time out to use for send/reception of data through the connection.
final public var timeOut: TimeInterval = 5.0
// MARK: - Private
final fileprivate var socket: GCDAsyncSocket!
final private let out: DataRepresentation
// MARK: - Conformance to Connection
/// The delegate of the connection.
weak final public var delegate: ConnectionDelegate?
/// The error delegate of the connection.
weak final public var errorDelegate: ConnectionErrorDelegate?
/// Creates a `SocketConnection`.
/// - parameter host: The host to connect to.
/// - parameter port: The port of the `host` to connect to.
/// - parameter delegate: The delegate of the connection.
/// - parameter errorDelegate: The error delegate to connect to.
/// - parameter outboundRepresentation: The data representation to use
/// on objects that are to be sent throught the receiver.
public init(host: Host,
port: Port,
delegate: ConnectionDelegate?,
errorDelegate: ConnectionErrorDelegate?,
outboundRepresentation: DataRepresentation) {
self.host = host
self.port = port
self.delegate = delegate
self.errorDelegate = errorDelegate
self.out = outboundRepresentation
super.init()
self.socket = GCDAsyncSocket(delegate: self,
delegateQueue: DispatchQueue.main)
self.socket.isIPv4PreferredOverIPv6 = false
}
convenience public init(endpoint: (host: Host, port: Port),
delegates: (delegate: ConnectionDelegate?, errorDelegate: ConnectionErrorDelegate?),
outboundRepresentation: DataRepresentation) {
self.init(host: endpoint.host,
port: endpoint.port,
delegate: delegates.delegate,
errorDelegate: delegates.errorDelegate,
outboundRepresentation: outboundRepresentation)
}
deinit {
self.disconnect()
self.socket = nil
self.delegate = nil
self.errorDelegate = nil
}
// MARK: - Public methods
final public func connect() {
do {
try self.socket.connect(toHost: self.host,
onPort:self.port)
}
catch {
let nserror: NSError = error as NSError
if nserror.code == GCDAsyncSocketError.alreadyConnected.rawValue {
self.errorDelegate?.connection(self, didFailWith: ConnectionError.alreadyConnected)
}
else {
self.errorDelegate?.connection(self, didFailWith: ConnectionError.connectionFailed)
}
}
}
final public func disconnect() {
self.socket.disconnect()
}
final public func close() {
self.delegate = nil
self.socket.disconnectAfterWriting()
}
// never throws
final public func send(_ representable: Representable) {
let representation = representable.represent(using: self.out) as! DataRepresentation
self.socket.write(representation.data,
withTimeout: self.timeOut,
tag: Tag.outMessage.rawValue)
}
}
fileprivate extension SocketConnection {
fileprivate enum Tag: Int {
case inMessage = 42
case outMessage = 84
}
}
extension SocketConnection: GCDAsyncSocketDelegate {
// MARK: - GCDAsyncSocketDelegate
@objc
final public func socket(_ sock: GCDAsyncSocket,
didRead data: Data,
withTag tag: Int) {
if let received = data as? Representable {
self.delegate?.connection(self, didReceive: received)
}
else {
self.errorDelegate?.connection(self,
didFailWith: ConnectionError.receptionFailed)
}
self.socket.readData(to: GCDAsyncSocket.lfData(),
withTimeout: self.timeOut,
tag: Tag.inMessage.rawValue)
}
@objc
final public func socket(_ sock: GCDAsyncSocket,
didConnectToHost host: String,
port: UInt16) {
self.delegate?.connectionDidConnect(self)
// start reading
self.socket.readData(to: GCDAsyncSocket.lfData(),
withTimeout: self.timeOut,
tag: Tag.inMessage.rawValue)
}
@objc
final public func socketDidDisconnect(_ sock: GCDAsyncSocket,
withError err: Error?) {
if let error = err {
self.delegate?.connection(self,
didDisconnectWithReason: error)
}
else {
self.delegate?.connection(self,
didDisconnectWithReason: ConnectionError.disconnection)
}
}
}
#if TARGET_OS_IPHONE
extension SocketConnection: StreamConnection {
public func accessStreams(_ block: @escaping (InputStream, OutputStream) -> Void) {
self.socket.perform { [unowned self] in
guard let input = self.socket.readStream()?.takeUnretainedValue(),
let output = self.socket.writeStream()?.takeUnretainedValue() else { return }
block(input, output)
}
}
}
#endif
#endif