BlueSocket
使用 Swift 包管理器构建的 Socket 框架,适用于 iOS,macOS 和 Linux。
先决条件
Swift
- Swift 开源
swift-5.1-RELEASE
工具链(最新版本所需的最小要求) - Swift 开源
swift-5.4-RELEASE
工具链(推荐) - 包含在 版本 11.0 或更高版本的 Xcode 中的 Swift 工具链。
macOS
- macOS 10.14.6(高版莫扎特)或更高版本。
- Xcode 版本 11.0 或更高版本,使用上述任一工具链。
- 使用包含的工具链(推荐)进行 Xcode 版本 12.5 或更高版本。
- Secure Transport 由 macOS 提供。
iOS
- iOS 10.0 或更高版本
- Xcode 版本 11.0 或更高版本,使用上述任一工具链。
- 使用包含的工具链(推荐)进行 Xcode 版本 12.5 或更高版本。
注意
如果在 iOS 上创建 UDP 服务器,可能需要按照以下步骤操作
- 从 Apple 获取多播权限
- 将多播网络功能添加到您的应用标识符中
- 更多详细信息,请参阅问题 194 的讨论
Linux
- Ubuntu 16.04 或 18.04
- 上述列出的 Swift 开源工具链之一。
其他平台
- BlueSocket 由于设备上不支持 POSIX/BSD/Darwin 套接字(尽管在模拟器中支持),因此 不 支持 watchOS。
- BlueSocket 应该在 tvOS 上运行,但尚未经过测试。
扩展组件
- BlueSSLService 可用于添加 SSL/TLS 支持功能。
- 如果您使用此包,请注意,在 Linux 上构建时需要安装 libssl-dev 包。
构建
要从命令行构建 Socket
% cd <path-to-clone>
% swift build
测试
要从命令行运行提供的针对 Socket 的单元测试
% cd <path-to-clone>
% swift build
% swift test
使用 BlueSocket
项目中包含
Swift 包管理器
要将 BlueSocket 包含到 Swift 包管理器包中,将其添加到您 Package.swift
文件中定义的 dependencies
属性。您可以通过使用 majorVersion
和 minor
参数来选择版本。例如
dependencies: [
.Package(url: "https://github.com/Kitura/BlueSocket.git", majorVersion: <majorVersion>, minor: <minor>)
]
Carthage
要使用 Carthage 在项目中包含 BlueSocket,在您的 Cartfile
中添加一行,包含 GitHub 组织名和项目名及版本。例如
github "Kitura/BlueSocket" ~> <majorVersion>.<minor>
CocoaPods
要使用 CocoaPods 在项目中包含 BlueSocket,只需将 BlueSocket
添加到您的 Podfile
中,例如
platform :ios, '10.0'
target 'MyApp' do
use_frameworks!
pod 'BlueSocket'
end
开始之前
首先,你需要导入Socket框架。以下是操作方法
import Socket
家庭、类型和协议支持
BlueSocket支持以下家庭、类型和协议
- 家族
- IPV4:
Socket.ProtocolFamily.inet
- IPV6:
Socket.ProtocolFamily.inet6
- UNIX:
Socket.ProtocolFamily.unix
- IPV4:
- 类型
- 流:
Socket.SocketType.stream
- 数据报:
Socket.SocketType.datagram
- 流:
- 协议
- TCP:
Socket.SocketProtocol.tcp
- UDP:
Socket.SocketProtocol.udp
- UNIX:
Socket.SocketProtocol.unix
- TCP:
创建套接字。
BlueSocket提供四种不同的工厂方法来创建实例,包括
create
- 该方法创建一个完全配置的默认套接字。默认套接字使用family: .inet
,type: .stream
,以及proto: .tcp
。create(family: ProtocolFamily, type: SocketType, proto: SocketProtocol)
- 该API允许你创建一个针对您需求进行自定义配置的Socket
实例。您可以自定义协议家族、套接字类型和套接字协议。create(connectedUsing signature: Signature)
- 该API将允许你创建一个Socket
实例,并根据您在Socket.Signature
中传入的信息尝试连接到服务器。create(fromNativeHandle nativeHandle: Int32, address: Address?)
- 该API允许您在新的Socket
实例中封装描述现有套接字的本地文件描述符。
设置读写缓冲区大小。
BlueSocket 允许您设置读取缓冲区的大小。然后,根据应用程序的需求,您可以将它更改为更高的值或更低的值。默认值设置为 Socket.SOCKET_DEFAULT_READ_BUFFER_SIZE
,其值为 4096
。最小读取缓冲区大小为 Socket.SOCKET_MINIMUM_READ_BUFFER_SIZE
,设置为 1024
。以下示例说明如何更改读取缓冲区大小(为简洁起见,省略了异常处理)
let mySocket = try Socket.create()
mySocket.readBufferSize = 32768
上述示例将默认读取缓冲区大小设置为 32768。此设置应该在首次使用 Socket
实例之前完成。
关闭套接字。
为了关闭一个打开的实例的套接字,提供了以下函数
close()
- 此函数将执行必要的任务以干净地关闭一个打开的套接字。
在套接字上监听(TCP/UNIX)。
为了使用 BlueSocket 在套接字上监听连接,提供了以下 API
listen(on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG, allowPortReuse: Bool = true, node: String? = nil)
第一个参数port
是要用于监听端口的参数。第二个参数maxBacklogSize
允许您设置用于保存挂起连接的队列的大小。函数将根据指定的port
确定适当的套接字配置。为了方便起见,在 macOS 上,可以将常量Socket.SOCKET_MAX_DARWIN_BACKLOG
设置为使用最大的允许的后缀数量。默认值对于所有平台均为Socket.SOCKET_DEFAULT_MAX_BACKLOG
,目前设置为 50。对于服务器使用,可能需要增加此值。为了允许重用监听端口,将allowPortReuse
设置为true
。如果设置为false
,则在尝试监听已被使用的端口时将发生错误。默认行为是允许端口重用。最后一个参数node
可以用于监听 特定地址。传递的值是一个包含数字网络地址的 可选字符串(对于 IPv4,使用数字和点符号,对于 IPv6,使用十六进制字符串)。默认行为是搜索合适的接口。如果node
格式不正确,将返回 SOCKET_ERR_GETADDRINFO_FAILED 错误。如果node
格式正确但指定的地址不可用,将返回 SOCKET_ERR_BIND_FAILED 错误。listen(on path: String, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG)
该API仅适用于.unix
协议族。第一个参数path
,是用于监听的路径。第二个参数maxBacklogSize
允许您设置等待连接队列的大小。该函数将根据指定的port
确定相应的套接字配置。为了在macOS上的方便性,可以设置常量Socket.SOCKET_MAX_DARWIN_BACKLOG
以使用最大允许的backlog大小。所有平台的默认值是Socket.SOCKET_DEFAULT_MAX_BACKLOG
,目前设置为50。对于服务器使用,可能需要增加此值。
示例
以下示例创建了一个默认的Socket
实例,然后立即在端口1337
上开始监听。请注意:为了简洁起见,省略了异常处理,下面提供了异常处理的完整示例。
var socket = try Socket.create()
try socket.listen(on: 1337)
从监听套接字接受连接(TCP/UNIX)。
当监听套接字检测到入站连接请求时,控制权返回到您的程序。您可以选择接受连接、继续监听或两者同时进行,如果您的应用程序是多线程的。BlueSocket支持接受入站连接的两种不同方式。它们是
acceptClientConnection(invokeDelegate: Bool = true)
- 此函数接受连接并基于新连接的套接字返回一个新的Socket
实例。监听的实例不受影响。如果invokeDelegate
为false
且Socket
有一个附加的SSLService
代理,您必须使用此函数返回的Socket
实例调用invokeDelegateOnAccept
方法。invokeDelegateOnAccept(for newSocket: Socket)
- 如果Socket
实例有一个SSLService
代理,这将调用代理的接受函数以执行SSL协商。它应该与由acceptClientConnection
返回的Socket
实例一起调用。如果用错误的Socket
实例调用、多次调用或Socket
实例没有SSLService
代理,此函数将抛出异常。acceptConnection()
- 此函数接受入站连接,替换并关闭现有的监听套接字。与监听套接字相关联的属性将替换为新连接套接字的相关属性。
将套接字连接到服务器(TCP/UNIX)。
除了上述描述的 create(connectedUsing:)
工厂方法外,BlueSocket 还支持三个额外的实例函数,用于将 Socket
实例连接到服务器。它们是:
connect(to host: String, port: Int32, timeout: UInt = 0)
- 此 API 允许您根据提供的hostname
和port
连接到服务器。注意:如果port
的值不在1-65535
范围内,此函数将抛出异常。您可以将timeout
设置为等待连接的毫秒数。注意:如果套接字处于阻塞模式,并且提供了一个大于零(0)的timeout
,它将暂时改为非阻塞模式。返回的套接字将 恢复到原始设置(阻塞或非阻塞)。如果套接字设置为 非阻塞 且 未提供超时值,将抛出异常。或者,您可以在成功连接后设置套接字为 非阻塞。connect(to path: String)
- 此 API 仅可用于.unix
协议族。它允许您根据提供的path
连接到服务器。connect(using signature: Signature)
- 此 API 允许您通过提供一个包含信息的Socket.Signature
实例来指定连接信息。有关更多信息,请参阅 Socket.swift 中的Socket.Signature
。
从套接字读取数据(TCP/UNIX)。
BlueSocket 支持四种不同的方法从套接字读取数据。它们的顺序(推荐使用顺序)如下:
read(into data: inout Data)
- 此函数读取套接字上所有可用的数据,并在传入的Data
对象中返回。read(into data: NSMutableData)
- 此函数读取套接字上所有可用的数据,并在传入的NSMutableData
对象中返回。readString()
- 此函数读取套接字上所有可用的数据,并将其作为String
返回。如果没有数据可供读取,则返回nil
。read(into buffer: UnsafeMutablePointer<CChar>, bufSize: Int, truncate: Bool = false)
- 此函数允许您通过提供一个指向该缓冲区的不安全指针和一个表示该缓冲区大小的整数,将数据读取到指定大小的缓冲区。此API(除其他类型的异常外)将引发Socket.SOCKET_ERR_RECV_BUFFER_TOO_SMALL
,如果提供的缓冲区太小,除非truncate = true
,在这种情况下,套接字将表现得好像只读取了bufSize
字节(未检索的字节将在下一次调用中返回)。如果truncate = false
,您需要再次调用,并使用适当的缓冲区大小(有关更多信息,请参阅Socket.swift中的Error.bufferSizeNeeded
)。- 注意:除了
readString()
之外,上述所有read API都可以返回零(0)。这可以表示远程连接已关闭,也可能表示套接字将阻塞(假设您已禁用阻塞)。为了区分两种情况,可以检查remoteConnectionClosed
属性。如果为真,则套接字远程伙伴已关闭连接,并且应该关闭此Socket
实例。
通过套接字(TCP/UNIX)写入数据。
除了从套接字读取外,BlueSocket还提供了四种向套接字写入数据的方法。这些方法使用顺序如下:
write(from data: Data)
- 此函数将Data
对象中包含的数据写入套接字。write(from data: NSData)
- 此函数将NSData
对象中包含的数据写入套接字。write(from string: String)
- 此函数将提供的String
中包含的数据写入套接字。write(from buffer: UnsafeRawPointer, bufSize: Int)
- 此函数通过提供一个指向该缓冲区的不安全指针和一个表示该缓冲区大小的整数,将指定大小的缓冲区中的数据写入。
监听数据报消息(UDP)。
BlueSocket支持三种监听传入数据报的不同方式。这些方法使用顺序如下:
listen(forMessage data: inout Data, on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG)
- 此函数监听传入的数据报,读取它并将其返回到传入的Data
对象。它返回一个包含读取的字节数和数据来源的Address
的元组。listen(forMessage data: NSMutableData, on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG)
- 此函数监听传入的数据报,读取它并将其返回到传入的NSMutableData
对象。它返回一个包含读取的字节数和数据来源的Address
的元组。listen(forMessage buffer: UnsafeMutablePointer<CChar>, bufSize: Int, on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG)
- 该函数监听传入的数据报,读取它并将它返回到通过Data
对象传递的。它返回一个元组,包含读取的字节数和数据发起的Address
。- 注意 1:这些函数将根据指定的
port
决定适当的套接字配置。将port
的值设置为零(0)将导致函数确定一个合适的空闲端口。 - 注意 2:
maxBacklogSize
参数允许您设置接收连接队列的大小。函数将根据指定的port
决定适当的套接字配置。为了在 macOS 上方便起见,常数Socket.SOCKET_MAX_DARWIN_BACKLOG
可以设置为可使用的最大 backlog 大小。默认值适用于所有平台为Socket.SOCKET_DEFAULT_MAX_BACKLOG
,目前设置为 50。对于服务器使用,可能需要增加此值。
读取数据报(UDP)。
BlueSocket 支持三种读取传入数据报的不同方式。它们是(按推荐使用顺序):
readDatagram(into data: inout Data)
- 这个函数读取一个传入的数据报,并将其返回到通过Data
对象传递的。它返回一个包含读取的字节数和数据发起的Address
的元组。readDatagram(into data: NSMutableData)
- 这个函数读取一个传入的数据报,并将其返回到通过NSMutableData
对象传递的。它返回一个包含读取的字节数和数据发起的Address
的元组。readDatagram(into buffer: UnsafeMutablePointer<CChar>, bufSize: Int)
- 这个函数读取一个传入的数据报,并将其返回到通过Data
对象传递的。它返回一个包含读取的字节数和数据发起的Address
的元组。如果读取的数据量大于bufSize
,则只会返回bufSize
。剩余读取的数据将被丢弃。
写入数据报(UDP)。
BlueSocket 也提供了四种将数据报写入套接字的方法。它们是(按推荐使用顺序):
write(from data: Data, to address: Address)
- 这个函数将Data
对象内包含的数据报写入套接字。write(from data: NSData, to address: Address)
- 这个函数将NSData
对象内包含的数据报写入套接字。write(from string: String, to address: Address)
- 这个函数将提供的String
中包含的数据报写入套接字。write(from buffer: UnsafeRawPointer, bufSize: Int, to address: Address)
- 这个函数通过提供一个指向该缓冲区的 不安全 指针和表示该缓冲区大小的整数,将指定大小的缓冲区内包含的数据写入。- 注意:在上述所有四个 API 中,
address
参数代表你发送数据报的目的地址。
关于NSData和NSMutableData的重要注意事项
以上使用NSData
或NSMutableData
的读写API 可能会 在不久的将来被弃用。
杂项实用函数
hostnameAndPort(from address: Address)
- 这个类函数提供了一种从给定的Socket.Address
中提取主机名和端口号的方法。成功完成后,将返回一个包含hostname
和port
的元组。checkStatus(for sockets: [Socket])
- 这个类函数允许你检查一组Socket
实例的状态。完成时,将返回一个包含两个Socket
数组的元组。第一个数组包含可以读取数据的Socket
实例,第二个数组包含可以被写入的Socket
实例。此API不会阻塞。它将检查每个Socket
实例的状态,然后返回结果。wait(for sockets: [Socket], timeout: UInt, waitForever: Bool = false)
- 这个类函数允许你监视一组Socket
实例,等待超时发生或监视的某个Socket
实例上的数据可读。如果指定超时为零(0),此API将检查每个套接字并立即返回。否则,它将等待直到超时或从监视的Socket
实例之一可读取数据。如果发生超时,此API将返回nil
。如果监视的Socket
实例之一上的数据可用,则将返回这些实例的数组。如果设置waitForever
标志为true,函数将无限期等待数据变为可用,而不管指定的超时值是多少。createAddress(host: String, port: Int32)
- 这个类函数允许创建一个Address
枚举,给定一个host
和一个port
。成功时,此函数返回一个Address
,如果指定的host
不存在,则返回nil
。isReadableOrWritable(waitForever: Bool = false, timeout: UInt = 0)
- 此 实例函数 允许检查Socket
的实例是否可读或可写。返回一个包含两个Bool
值的元组。第一个值,如果为真,表示Socket
实例有可读数据,第二个值,如果为真,表示Socket
实例可写。当waitForever
为真时,此例程将等待直到Socket
变为可读或可写或出现错误。如果为假,则timeout
参数指定等待时间。如果将超时值指定为零(0)
,则此函数将检查 当前 状态并立即返回。此函数返回一个包含两个布尔值的元组,第一个为readable
(可读),第二个为writable
(可写)。如果Socket
分别可读或可写,则它们被设置为真。如果两者都未设置为真,则表示超时。注意:若尝试向新建的 Socket 写入,在尝试操作之前应确保其是 可写 的。setBlocking(shouldBlock: Bool)
- 此 实例函数 允许控制此Socket
实例是否应置于阻塞模式。 注意:所有Socket
实例默认情况下都是创建在 阻塞模式。setReadTimeout(value: UInt = 0)
- 此 实例函数 允许设置读操作的超时时间。参数value
是一个UInt
,它指定了读操作等待多长时间才返回。如果发生超时,读操作将返回0
字节数已读取,并将errno
设置为EAGAIN
。setWriteTimeout(value: UInt = 0)
- 此 实例函数 允许设置写入操作的超时时间。参数value
是一个UInt
,它指定了写入操作等待多长时间才返回。如果发生超时,写入操作将返回0
字节数已写入,对于 TCP 和 UNIX 套接字,将errno
设置为EAGAIN
,而对于 UDP,即使超时,写入操作也将成功。udpBroadcast(enable: Bool)
- 此 实例函数 用于在 UDP 套接字上启用广播模式。传入true
启用广播,传入false
禁用。如果Socket
实例不是 UDP 套接字,此函数将抛出一个异常。
完整示例
以下示例展示了如何使用新的基于 GCD
的 Dispatch API 创建一个相对简单的多线程回显服务器。以下是运行后可通过 telnet ::1 1337
访问的简单回显服务器代码。
import Foundation
import Socket
import Dispatch
class EchoServer {
static let quitCommand: String = "QUIT"
static let shutdownCommand: String = "SHUTDOWN"
static let bufferSize = 4096
let port: Int
var listenSocket: Socket? = nil
var continueRunningValue = true
var connectedSockets = [Int32: Socket]()
let socketLockQueue = DispatchQueue(label: "com.kitura.serverSwift.socketLockQueue")
var continueRunning: Bool {
set(newValue) {
socketLockQueue.sync {
self.continueRunningValue = newValue
}
}
get {
return socketLockQueue.sync {
self.continueRunningValue
}
}
}
init(port: Int) {
self.port = port
}
deinit {
// Close all open sockets...
for socket in connectedSockets.values {
socket.close()
}
self.listenSocket?.close()
}
func run() {
let queue = DispatchQueue.global(qos: .userInteractive)
queue.async { [unowned self] in
do {
// Create an IPV6 socket...
try self.listenSocket = Socket.create(family: .inet6)
guard let socket = self.listenSocket else {
print("Unable to unwrap socket...")
return
}
try socket.listen(on: self.port)
print("Listening on port: \(socket.listeningPort)")
repeat {
let newSocket = try socket.acceptClientConnection()
print("Accepted connection from: \(newSocket.remoteHostname) on port \(newSocket.remotePort)")
print("Socket Signature: \(String(describing: newSocket.signature?.description))")
self.addNewConnection(socket: newSocket)
} while self.continueRunning
}
catch let error {
guard let socketError = error as? Socket.Error else {
print("Unexpected error...")
return
}
if self.continueRunning {
print("Error reported:\n \(socketError.description)")
}
}
}
dispatchMain()
}
func addNewConnection(socket: Socket) {
// Add the new socket to the list of connected sockets...
socketLockQueue.sync { [unowned self, socket] in
self.connectedSockets[socket.socketfd] = socket
}
// Get the global concurrent queue...
let queue = DispatchQueue.global(qos: .default)
// Create the run loop work item and dispatch to the default priority global queue...
queue.async { [unowned self, socket] in
var shouldKeepRunning = true
var readData = Data(capacity: EchoServer.bufferSize)
do {
// Write the welcome string...
try socket.write(from: "Hello, type 'QUIT' to end session\nor 'SHUTDOWN' to stop server.\n")
repeat {
let bytesRead = try socket.read(into: &readData)
if bytesRead > 0 {
guard let response = String(data: readData, encoding: .utf8) else {
print("Error decoding response...")
readData.count = 0
break
}
if response.hasPrefix(EchoServer.shutdownCommand) {
print("Shutdown requested by connection at \(socket.remoteHostname):\(socket.remotePort)")
// Shut things down...
self.shutdownServer()
return
}
print("Server received from connection at \(socket.remoteHostname):\(socket.remotePort): \(response) ")
let reply = "Server response: \n\(response)\n"
try socket.write(from: reply)
if (response.uppercased().hasPrefix(EchoServer.quitCommand) || response.uppercased().hasPrefix(EchoServer.shutdownCommand)) &&
(!response.hasPrefix(EchoServer.quitCommand) && !response.hasPrefix(EchoServer.shutdownCommand)) {
try socket.write(from: "If you want to QUIT or SHUTDOWN, please type the name in all caps. 😃\n")
}
if response.hasPrefix(EchoServer.quitCommand) || response.hasSuffix(EchoServer.quitCommand) {
shouldKeepRunning = false
}
}
if bytesRead == 0 {
shouldKeepRunning = false
break
}
readData.count = 0
} while shouldKeepRunning
print("Socket: \(socket.remoteHostname):\(socket.remotePort) closed...")
socket.close()
self.socketLockQueue.sync { [unowned self, socket] in
self.connectedSockets[socket.socketfd] = nil
}
}
catch let error {
guard let socketError = error as? Socket.Error else {
print("Unexpected error by connection at \(socket.remoteHostname):\(socket.remotePort)...")
return
}
if self.continueRunning {
print("Error reported by connection at \(socket.remoteHostname):\(socket.remotePort):\n \(socketError.description)")
}
}
}
}
func shutdownServer() {
print("\nShutdown in progress...")
self.continueRunning = false
// Close all open sockets...
for socket in connectedSockets.values {
self.socketLockQueue.sync { [unowned self, socket] in
self.connectedSockets[socket.socketfd] = nil
socket.close()
}
}
DispatchQueue.main.sync {
exit(0)
}
}
}
let port = 1337
let server = EchoServer(port: port)
print("Swift Echo Server Sample")
print("Connect with a command line window by entering 'telnet ::1 \(port)'")
server.run()
您可以通过使用 Swift 4 并指定以下 Package.swift
文件来构建此服务器。
import PackageDescription
let package = Package(
name: "EchoServer",
dependencies: [
.package(url: "https://github.com/Kitura/BlueSocket.git", from:"1.0.8"),
],
targets: [
.target(
name: "EchoServer",
dependencies: [
"Socket"
]),
]
)
或者,如果您仍在使用 Swift 3,可以通过指定以下 Package.swift
文件来构建。
import PackageDescription
let package = Package(
name: "EchoServer",
dependencies: [
.Package(url: "https://github.com/Kitura/BlueSocket.git", majorVersion: 1, minor: 0),
],
exclude: ["EchoServer.xcodeproj"]
)
以下命令序列将在 Linux 上构建和运行回显服务器。如果在 macOS 或较 8/18 工具链 更新的 工具链上运行,可以省略 -Xcc -fblocks
开关,因为不再需要。
$ swift build -Xcc -fblocks
$ .build/debug/EchoServer
Swift Echo Server Sample
Connect with a command line window by entering 'telnet ::1 1337'
Listening on port: 1337
社区
我们热爱讨论服务器端Swift和Kitura。加入我们的Slack,结识团队吧!
许可证
此库受Apache 2.0许可证的许可。完整许可证文本可在LICENSE中查看。