BlueSocket 2.0.4

BlueSocket 2.0.4

测试已测试
语言语言 SwiftSwift
许可证 Apache-2.0
发布最新版本2022年10月
SPM支持 SPM

Matthaus WoolardTaylor FranklinBill AbtDanny SungSwift-at-IBM 维护。



  • IBM 和 Kitura 项目作者

APIDoc Build Status - Master macOS iOS Linux Apache 2 Slack Status

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 服务器,可能需要按照以下步骤操作

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 属性。您可以通过使用 majorVersionminor 参数来选择版本。例如

	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
  • 类型
    • 流:Socket.SocketType.stream
    • 数据报:Socket.SocketType.datagram
  • 协议
    • TCP:Socket.SocketProtocol.tcp
    • UDP:Socket.SocketProtocol.udp
    • UNIX:Socket.SocketProtocol.unix

创建套接字。

BlueSocket提供四种不同的工厂方法来创建实例,包括

  • create - 该方法创建一个完全配置的默认套接字。默认套接字使用family: .inettype: .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实例。监听的实例不受影响。如果invokeDelegatefalseSocket有一个附加的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 允许您根据提供的 hostnameport 连接到服务器。注意:如果 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的重要注意事项

以上使用NSDataNSMutableData的读写API 可能会 在不久的将来被弃用。

杂项实用函数

  • hostnameAndPort(from address: Address) - 这个类函数提供了一种从给定的Socket.Address中提取主机名和端口号的方法。成功完成后,将返回一个包含hostnameport的元组。
  • 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 字节数已写入,对于 TCPUNIX 套接字,将 errno 设置为 EAGAIN,而对于 UDP,即使超时,写入操作也将成功。
  • udpBroadcast(enable: Bool) - 此 实例函数 用于在 UDP 套接字上启用广播模式。传入 true 启用广播,传入 false 禁用。如果 Socket 实例不是 UDP 套接字,此函数将抛出一个异常。

完整示例

以下示例展示了如何使用新的基于 GCDDispatch 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中查看。