254 lines
7.7 KiB
Swift
Executable File
254 lines
7.7 KiB
Swift
Executable File
import Foundation
|
|
|
|
/// The TCP socket build upon `GCDAsyncSocket`.
|
|
///
|
|
/// - warning: This class is not thread-safe.
|
|
open class GCDTCPSocket: NSObject, GCDAsyncSocketDelegate, RawTCPSocketProtocol {
|
|
fileprivate let socket: GCDAsyncSocket
|
|
fileprivate var enableTLS: Bool = false
|
|
fileprivate var host: String?
|
|
|
|
/**
|
|
Initailize an instance with `GCDAsyncSocket`.
|
|
|
|
- parameter socket: The socket object to work with. If this is `nil`, then a new `GCDAsyncSocket` instance is created.
|
|
*/
|
|
public init(socket: GCDAsyncSocket? = nil) {
|
|
if let socket = socket {
|
|
self.socket = socket
|
|
self.socket.setDelegate(nil, delegateQueue: QueueFactory.getQueue())
|
|
} else {
|
|
self.socket = GCDAsyncSocket(delegate: nil, delegateQueue: QueueFactory.getQueue(), socketQueue: QueueFactory.getQueue())
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.socket.synchronouslySetDelegate(self)
|
|
}
|
|
|
|
// MARK: RawTCPSocketProtocol implementation
|
|
|
|
/// The `RawTCPSocketDelegate` instance.
|
|
weak open var delegate: RawTCPSocketDelegate?
|
|
|
|
/// If the socket is connected.
|
|
open var isConnected: Bool {
|
|
return !socket.isDisconnected
|
|
}
|
|
|
|
/// The source address.
|
|
open var sourceIPAddress: IPAddress? {
|
|
guard let localHost = socket.localHost else {
|
|
return nil
|
|
}
|
|
return IPAddress(fromString: localHost)
|
|
}
|
|
|
|
/// The source port.
|
|
open var sourcePort: Port? {
|
|
return Port(port: socket.localPort)
|
|
}
|
|
|
|
/// The destination address.
|
|
///
|
|
/// - note: Always returns `nil`.
|
|
open var destinationIPAddress: IPAddress? {
|
|
return nil
|
|
}
|
|
|
|
/// The destination port.
|
|
///
|
|
/// - note: Always returns `nil`.
|
|
open var destinationPort: Port? {
|
|
return nil
|
|
}
|
|
|
|
/**
|
|
Connect to remote host.
|
|
|
|
- parameter host: Remote host.
|
|
- parameter port: Remote port.
|
|
- parameter enableTLS: Should TLS be enabled.
|
|
- parameter tlsSettings: The settings of TLS.
|
|
|
|
- throws: The error occured when connecting to host.
|
|
*/
|
|
open func connectTo(host: String, port: Int, enableTLS: Bool = false, tlsSettings: [AnyHashable: Any]? = nil) throws {
|
|
self.host = host
|
|
try connectTo(host: host, withPort: port)
|
|
self.enableTLS = enableTLS
|
|
if enableTLS {
|
|
startTLSWith(settings: tlsSettings)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Disconnect the socket.
|
|
|
|
The socket will disconnect elegantly after any queued writing data are successfully sent.
|
|
*/
|
|
open func disconnect() {
|
|
socket.disconnectAfterWriting()
|
|
}
|
|
|
|
/**
|
|
Disconnect the socket immediately.
|
|
*/
|
|
open func forceDisconnect() {
|
|
socket.disconnect()
|
|
}
|
|
|
|
/**
|
|
Send data to remote.
|
|
|
|
- parameter data: Data to send.
|
|
- warning: This should only be called after the last write is finished, i.e., `delegate?.didWriteData()` is called.
|
|
*/
|
|
open func write(data: Data) {
|
|
write(data: data, withTimeout: -1)
|
|
}
|
|
|
|
/**
|
|
Read data from the socket.
|
|
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
open func readData() {
|
|
socket.readData(withTimeout: -1, tag: 0)
|
|
}
|
|
|
|
/**
|
|
Read specific length of data from the socket.
|
|
|
|
- parameter length: The length of the data to read.
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
open func readDataTo(length: Int) {
|
|
readDataTo(length: length, withTimeout: -1)
|
|
}
|
|
|
|
/**
|
|
Read data until a specific pattern (including the pattern).
|
|
|
|
- parameter data: The pattern.
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
open func readDataTo(data: Data) {
|
|
readDataTo(data: data, maxLength: 0)
|
|
}
|
|
|
|
/**
|
|
Read data until a specific pattern (including the pattern).
|
|
|
|
- parameter data: The pattern.
|
|
- parameter maxLength: Ignored since `GCDAsyncSocket` does not support this. The max length of data to scan for the pattern.
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
open func readDataTo(data: Data, maxLength: Int) {
|
|
readDataTo(data: data, withTimeout: -1)
|
|
}
|
|
|
|
// MARK: Other helper methods
|
|
/**
|
|
Send data to remote.
|
|
|
|
- parameter data: Data to send.
|
|
- parameter timeout: Operation timeout.
|
|
- warning: This should only be called after the last write is finished, i.e., `delegate?.didWriteData()` is called.
|
|
*/
|
|
func write(data: Data, withTimeout timeout: Double) {
|
|
guard data.count > 0 else {
|
|
QueueFactory.getQueue().async {
|
|
self.delegate?.didWrite(data: data, by: self)
|
|
}
|
|
return
|
|
}
|
|
|
|
socket.write(data, withTimeout: timeout, tag: 0)
|
|
}
|
|
|
|
/**
|
|
Read specific length of data from the socket.
|
|
|
|
- parameter length: The length of the data to read.
|
|
- parameter timeout: Operation timeout.
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
func readDataTo(length: Int, withTimeout timeout: Double) {
|
|
socket.readData(toLength: UInt(length), withTimeout: timeout, tag: 0)
|
|
}
|
|
|
|
/**
|
|
Read data until a specific pattern (including the pattern).
|
|
|
|
- parameter data: The pattern.
|
|
- parameter timeout: Operation timeout.
|
|
- warning: This should only be called after the last read is finished, i.e., `delegate?.didReadData()` is called.
|
|
*/
|
|
func readDataTo(data: Data, withTimeout timeout: Double) {
|
|
socket.readData(to: data, withTimeout: timeout, tag: 0)
|
|
}
|
|
|
|
/**
|
|
Connect to remote host.
|
|
|
|
- parameter host: Remote host.
|
|
- parameter port: Remote port.
|
|
|
|
- throws: The error occured when connecting to host.
|
|
*/
|
|
func connectTo(host: String, withPort port: Int) throws {
|
|
try socket.connect(toHost: host, onPort: UInt16(port))
|
|
}
|
|
|
|
/**
|
|
Secures the connection using SSL/TLS.
|
|
|
|
- parameter tlsSettings: TLS settings, refer to documents of `GCDAsyncSocket` for detail.
|
|
*/
|
|
func startTLSWith(settings: [AnyHashable: Any]!) {
|
|
if let settings = settings as? [String: NSObject] {
|
|
socket.startTLS(ensureSendPeerName(tlsSettings: settings))
|
|
} else {
|
|
socket.startTLS(ensureSendPeerName(tlsSettings: nil))
|
|
}
|
|
}
|
|
|
|
// MARK: Delegate methods for GCDAsyncSocket
|
|
open func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
|
|
delegate?.didWrite(data: nil, by: self)
|
|
}
|
|
|
|
open func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
|
|
delegate?.didRead(data: data, from: self)
|
|
}
|
|
|
|
open func socketDidDisconnect(_ socket: GCDAsyncSocket, withError err: Error?) {
|
|
delegate?.didDisconnectWith(socket: self)
|
|
delegate = nil
|
|
socket.setDelegate(nil, delegateQueue: nil)
|
|
}
|
|
|
|
open func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
|
|
if !enableTLS {
|
|
delegate?.didConnectWith(socket: self)
|
|
}
|
|
}
|
|
|
|
open func socketDidSecure(_ sock: GCDAsyncSocket) {
|
|
if enableTLS {
|
|
delegate?.didConnectWith(socket: self)
|
|
}
|
|
}
|
|
|
|
private func ensureSendPeerName(tlsSettings: [String: NSObject]? = nil) -> [String: NSObject] {
|
|
var setting = tlsSettings ?? [:]
|
|
guard setting[kCFStreamSSLPeerName as String] == nil else {
|
|
return setting
|
|
}
|
|
|
|
setting[kCFStreamSSLPeerName as String] = host! as NSString
|
|
return setting
|
|
}
|
|
}
|