使用 Swift Package Manager 的 Swift Socket 框架。可在 iOS、macOS 和 Linux 上运行。
swift-5.1-RELEASE
工具链 (最新版本最低要求)swift-5.4-RELEASE
工具链 (推荐)注意
如果在 iOS 上创建 UDP 服务器,您可能需要遵循以下几个步骤
要从命令行构建 Socket,请执行以下操作
% cd <path-to-clone>
% swift build
要从命令行运行 Socket 的提供的单元测试,请执行以下操作
% cd <path-to-clone>
% swift build
% swift test
要将 BlueSocket 包含到 Swift Package Manager 软件包中,请将其添加到您的 Package.swift
文件中定义的 dependencies
属性。您可以使用 majorVersion
和 minor
参数选择版本。例如
dependencies: [
.Package(url: "https://github.com/Kitura/BlueSocket.git", majorVersion: <majorVersion>, minor: <minor>)
]
要使用 Carthage 将 BlueSocket 包含到项目中,请在您的 Cartfile
中添加一行,其中包含 GitHub 组织和项目名称以及版本。例如
github "Kitura/BlueSocket" ~> <majorVersion>.<minor>
要使用 CocoaPods 将 BlueSocket 包含到项目中,只需将 BlueSocket
添加到您的 Podfile
中,例如
platform :ios, '10.0'
target 'MyApp' do
use_frameworks!
pod 'BlueSocket'
end
您需要做的第一件事是导入 Socket 框架。这可以通过以下方式完成
import Socket
BlueSocket 支持以下系列、类型和协议
Socket.ProtocolFamily.inet
Socket.ProtocolFamily.inet6
Socket.ProtocolFamily.unix
Socket.SocketType.stream
Socket.SocketType.datagram
Socket.SocketProtocol.tcp
Socket.SocketProtocol.udp
Socket.SocketProtocol.unix
BlueSocket 提供了四种不同的工厂方法来创建实例。 它们是
create()
- 这将创建一个完全配置的默认 socket。 默认 socket 是使用 family: .inet
, type: .stream
, 和 proto: .tcp
创建的。create(family family: ProtocolFamily, type: SocketType, proto: SocketProtocol)
- 此 API 允许您创建一个针对您的需求定制的配置 Socket
实例。 您可以自定义协议系列、socket 类型和 socket 协议。create(connectedUsing signature: Signature)
- 此 API 允许您创建一个 Socket
实例,并使其尝试根据您在 Socket.Signature
中传递的信息连接到服务器。create(fromNativeHandle nativeHandle: Int32, address: Address?)
- 此 API 允许您将描述现有 socket 的本机文件描述符包装在 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
实例之前完成此设置。
要关闭打开的实例的 socket,提供了以下函数
close()
- 此函数将执行必要的任务,以便干净地关闭打开的 socket。要使用 BlueSocket 监听 socket 上的连接,提供了以下 API
listen(on port: Int, maxBacklogSize: Int = Socket.SOCKET_DEFAULT_MAX_BACKLOG, allowPortReuse: Bool = true, node: String? = nil)
第一个参数 port
是要用于监听的端口。 第二个参数 maxBacklogSize
允许您设置保存挂起连接的队列的大小。 该函数将根据指定的 port
确定适当的 socket 配置。 为了方便起见,在 macOS 上,可以设置常量 Socket.SOCKET_MAX_DARWIN_BACKLOG
以使用允许的最大积压大小。 所有平台的默认值为 Socket.SOCKET_DEFAULT_MAX_BACKLOG
,当前设置为 50。 对于服务器使用,可能需要增加此值。 要允许重用监听端口,请将 allowPortReuse
设置为 true
。 如果设置为 false
,如果您尝试监听已使用的端口,则会发生错误。 DEFAULT
行为是 allow
端口重用。 最后一个参数 node
可用于监听特定地址。 传递的值是包含数字网络地址的可选字符串(对于 IPv4,数字和点表示法,对于 iPv6,十六进制字符串)。 DEFAULT
行为是搜索合适的接口。 如果 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
确定适当的 socket 配置。 为了方便起见,在 macOS 上,可以设置常量 Socket.SOCKET_MAX_DARWIN_BACKLOG
以使用允许的最大积压大小。 所有平台的默认值为 Socket.SOCKET_DEFAULT_MAX_BACKLOG
,当前设置为 50。 对于服务器使用,可能需要增加此值。以下示例创建一个默认的 Socket
实例,然后立即开始在端口 1337
上监听。 注意:为了简洁起见,省略了异常处理,有关异常处理的示例,请参见下面的完整示例。
var socket = try Socket.create()
try socket.listen(on: 1337)
当监听 socket 检测到传入的连接请求时,控制权将返回到您的程序。 然后,您可以接受连接或继续监听,或者如果您的应用程序是多线程的,则可以同时接受连接和继续监听。 BlueSocket 支持两种不同的接受传入连接的方式。 它们是
acceptClientConnection(invokeDelegate: Bool = true)
- 此函数接受连接并返回基于新连接的 socket 的新 Socket
实例。 正在监听的实例不受影响。 如果 invokeDelegate
为 false
并且 Socket
附加了 SSLService
委托,则您必须使用此函数返回的 Socket
实例调用 invokeDelegateOnAccept
方法。invokeDelegateOnAccept(for newSocket: Socket)
- 如果 Socket
实例具有 SSLService
委托,这将调用委托的接受函数来执行 SSL 协商。 应该使用 acceptClientConnection
返回的 Socket
实例调用它。 如果使用错误的 Socket
实例调用、多次调用或者如果 Socket
实例不具有 SSLService
委托,则此函数将引发异常。acceptConnection()
- 此函数接受传入的连接,替换并关闭现有的监听 socket。 以前与监听 socket 关联的属性将替换为与新连接的 socket 相关的属性。除了上面描述的 create(connectedUsing:)
工厂方法之外,BlueSocket 还支持三个额外的实例函数,用于将 Socket
实例连接到服务器。 它们是
connect(to host: String, port: Int32, timeout: UInt = 0)
- 此 API 允许您根据您提供的 hostname
和 port
连接到服务器。 注意:如果 port
的值不在 1-65535
的范围内,则此函数将抛出 exception
。 (可选)您可以将 timeout
设置为等待连接的毫秒数。 注意:如果 socket 处于阻塞模式,则如果提供了大于零 (0) 的 timeout
,它将暂时更改为非阻塞模式。 返回的 socket 将设置回其原始设置(阻塞或非阻塞)。 如果将 socket 设置为非阻塞并且未提供超时值,则将抛出异常。 或者,您可以在成功连接后将 socket 设置为非阻塞。connect(to path: String)
- 此 API 只能与 .unix
协议系列一起使用。 它允许您根据您提供的 path
连接到服务器。connect(using signature: Signature)
- 此 API 允许您通过提供包含信息的 Socket.Signature
实例来指定连接信息。 有关更多信息,请参阅 Socket.swift 中的 Socket.Signature
。BlueSocket 支持四种不同的从 socket 读取数据的方式。 这些是(按推荐使用顺序)
read(into data: inout Data)
- 此函数读取 socket 上的所有可用数据,并在传递的 Data
对象中返回它。read(into data: NSMutableData)
- 此函数读取 socket 上的所有可用数据,并在传递的 NSMutableData
对象中返回它。readString()
- 此函数读取 socket 上的所有可用数据,并将其作为 String
返回。 如果没有可读取的数据,则返回 nil
。read(into buffer: UnsafeMutablePointer<CChar>, bufSize: Int, truncate: Bool = false)
- 此函数允许你将数据读取到指定大小的缓冲区中,通过提供指向该缓冲区的unsafe指针和一个表示缓冲区大小的整数。除了其他类型的异常之外,如果提供的缓冲区太小,此API将抛出一个 Socket.SOCKET_ERR_RECV_BUFFER_TOO_SMALL
异常,除非 truncate = true
,在这种情况下,socket的行为就像只读取了 bufSize
个字节(未检索的字节将在下次调用中返回)。如果 truncate = false
,你需要使用适当的缓冲区大小再次调用(有关更多信息,请参见Socket.swift中的 Error.bufferSizeNeeded
)。readString()
之外,上述所有 read API 都可以返回零 (0)。这可能表明远程连接已关闭,或者可能表明 socket 将阻塞(假设你已关闭阻塞)。为了区分两者,可以检查属性 remoteConnectionClosed
。如果为 true
,则表示 socket 的远程伙伴已关闭连接,并且应该关闭此 Socket
实例。除了从 socket 读取数据之外,BlueSocket 还提供了四种将数据写入 socket 的方法。它们是(按建议的使用顺序):
write(from data: Data)
- 此函数将 Data
对象中包含的数据写入 socket。write(from data: NSData)
- 此函数将 NSData
对象中包含的数据写入 socket。write(from string: String)
- 此函数将提供的 String
中包含的数据写入 socket。write(from buffer: UnsafeRawPointer, bufSize: Int)
- 此函数通过提供指向该缓冲区的unsafe指针和一个表示缓冲区大小的整数,将指定大小的缓冲区中包含的数据写入 socket。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
。port
确定适当的 socket 配置。将 port
的值设置为零 (0) 将导致该函数确定一个合适的空闲端口。maxBacklogSize
允许你设置保存挂起连接的队列的大小。该函数将根据指定的 port
确定适当的 socket 配置。为了方便起见,在 macOS 上,可以将常量 Socket.SOCKET_MAX_DARWIN_BACKLOG
设置为使用允许的最大 backlog 大小。所有平台的默认值为 Socket.SOCKET_DEFAULT_MAX_BACKLOG
,当前设置为 50。对于服务器使用,可能需要增加此值。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
。读取的剩余数据将被丢弃。BlueSocket 还提供了四种将数据报写入 socket 的方法。它们是(按建议的使用顺序):
write(from data: Data, to address: Address)
- 此函数将 Data
对象中包含的数据报写入 socket。write(from data: NSData, to address: Address)
- 此函数将 NSData
对象中包含的数据报写入 socket。write(from string: String, to address: Address)
- 此函数将提供的 String
中包含的数据报写入 socket。write(from buffer: UnsafeRawPointer, bufSize: Int, to address: Address)
- 此函数通过提供指向该缓冲区的unsafe指针和一个表示缓冲区大小的整数,将指定大小的缓冲区中包含的数据写入 socket。address
参数表示你要将数据报发送到的目标的地址。使用 NSData
或 NSMutableData
的上述 read 和 write 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 并立即返回。否则,它将等待直到超时到期或数据从一个或多个受监视的 Socket
实例变为可读。如果发生超时,此 API 将返回 nil
。如果数据在一个或多个受监视的 Socket
实例上可用,这些实例将在一个数组中返回。如果 waitForever
标志设置为 true,则该函数将无限期地等待数据变为可用无论指定的超时值如何。createAddress(host: String, port: Int32)
- 此类函数允许给定 host
和 port
创建 Address
枚举。成功后,此函数返回一个 Address
,如果指定的 host
不存在,则返回 nil
。isReadableOrWritable(waitForever: Bool = false, timeout: UInt = 0)
- 此实例函数允许确定 Socket
实例是否可读和/或可写。返回一个包含两个 Bool
值的元组。第一个值为 true 表示 Socket
实例有数据可读,第二个值为 true 表示 Socket
实例可以写入。如果 waitForever
为 true,则此例程将等待直到 Socket
可读或可写或发生错误。如果为 false,则 timeout
参数指定等待时间。如果为超时值指定了零 (0)
值,此函数将检查当前状态并立即返回。此函数返回一个包含两个布尔值的元组,第一个是 readable
,第二个是 writable
。如果 Socket
分别可读或可写,则将它们设置为 true。 如果两者都没有设置为 true,则表示发生了超时。注意: 如果你试图写入新连接的Socket,你应该确保在尝试操作之前它是可写的。setBlocking(shouldBlock: Bool)
- 此实例函数允许你控制是否应将此 Socket
实例置于阻塞模式。注意: 所有 Socket
实例在默认情况下都是以阻塞模式创建的。setReadTimeout(value: UInt = 0)
- 此实例函数允许你设置读取操作的超时时间。value
是一个 UInt
,用于指定读取操作在返回之前等待的时间。如果发生超时,读取操作将返回 0
字节已读,并且 errno
将设置为 EAGAIN
。setWriteTimeout(value: UInt = 0)
- 此实例函数允许你设置写入操作的超时时间。value
是一个 UInt
,用于指定写入操作在返回之前等待的时间。如果发生超时,写入操作将为 TCP 和 UNIX socket 返回 0
字节已写入,并且 errno
将设置为 EAGAIN
,对于 UDP,无论超时值如何,写入操作都将成功。udpBroadcast(enable: Bool)
- 此实例函数用于在 UDP socket 上启用广播模式。传递 true
以启用广播,传递 false
以禁用广播。如果 Socket
实例不是 UDP socket,此函数将抛出异常。以下示例展示了如何使用基于新的 GCD based
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 中找到。