1.为啥要写这篇文章?
之前作者接触的都是用Objective-C
语言及CocoaAsyncSocket
进行Socket方面的编程,后来为了节约通信的流量,又加入了Protobuf
来进行数据传输。从一开始一条Socket
通道来解析所有数据(后台根据消息重要程度划分不同的优先级消息队列,再进行相关推送),并通过通知的形式把业务数据抛给业务层到证券行业的两条Socket
通道,一条负责Query(查询),一条负责Push(推送),Query Socket封装成Http格式的请求并可设置请求超时时间,Push Socket数据解析成功之后直接以通知的形式抛给业务层。同时,在2016年直播答题比较火的时候,也用过腾讯的Mars
框架开发过直播答题的App。今年也接触了对Socket
进行TLS
双向认证
来保证数据传输的安全性和可靠性。
最近,作者在进行Swift
开发的时候,突然想到Objective-C
有CocoaAsyncSocket
这个框架来进行Socket
开发,那么Swift
有没有什么好的框架呢?在网上查找Swift
Socket
编程框架的时候发现了iOS原生开发框架Network
,作者抱着试一试的态度对这个框架研究了一下,下面就来给大家分享一下。
2.设置TLS配置并发起连接
这里有几点:1.需要配置成TLS
; 2.可选设置禁止那些网络类型;3.使用ipv6协议;4.preferNoProxies
要不要禁止开启代理的情况下进行Socket
连接;5.最后设置服务端的IP + Port,进行连接。代码如下:
Swift
var myQueue = DispatchQueue.global()
let options = NWProtocolTLS.Options()
self?.params = NWParameters(tls: options)
// self.params = NWParameters.tcp
// params.prohibitedInterfaceTypes = [.wifi, .cellular]
// 使用ipv6协议
if let ipOption = self?.params.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
ipOption.version = .v6
}
// 禁止代理
self?.params.preferNoProxies = true
self?.connection = NWConnection(host: NWEndpoint.Host("101.***.***.***"), port: NWEndpoint.Port(integerLiteral: 8902), using: self?.params ?? NWParameters())
self?.connection.start(queue: self!.myQueue)
3.TLS-验证服务端证书
由于我们采用的是TLS
双向认证,TLS
双向认证分为两步:第一步是认证服务端证书,第二步是认证客户端,只有服务端和客户端都认证完成之后,才认为TLS认证通道建立起来了。
Swift
// 设置服务端证书验证的回调
sec_protocol_options_set_verify_block(options.securityProtocolOptions, { [weak self] (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
// 为信任证书链设置自签名根证书
let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
if let curBundle = self?.curBundle(), let url = curBundle.url(forResource: "server_cert", withExtension: "cer"),
let data = try? Data(contentsOf: url), let cert = SecCertificateCreateWithData(nil, data as CFData) {
if SecTrustSetAnchorCertificates(trust, [cert] as CFArray) != errSecSuccess {
sec_protocol_verify_complete(false)
return
}
}
// 设置验证策略
let policy = SecPolicyCreateSSL(true, nil)
SecTrustSetPolicies(trust, policy)
SecTrustSetAnchorCertificatesOnly(trust, true)
// 验证证书链
var error: CFError?
if SecTrustEvaluateWithError(trust, &error) {
sec_protocol_verify_complete(true)
} else {
sec_protocol_verify_complete(false)
print(error!)
}
第二行配置了TLS
认证方式,第五行设置了服务端证书认证的回调。我们这里采用的是证书链验证,其中server_cert.cert是服务端自签名的根证书,内置在客户端。这里的验证原理是,首先从本地把服务端根证书读出来,并把它设置成锚点证书。其次,设置验证策略,SecTrustSetAnchorCertificatesOnly(trust, true)
这行表示,只用自己设置的证书链进行验证,不用系统的证书链。最后SecTrustEvaluateWithError(trust, &error)
就是对服务端证书进行本地证书链验证并把最后结果回调出去。
由于我们采用的是自签名证书,在进行上述这一步服务端证书校验的过程中,要事先将证书下载到手机上并进行安装,安装成功之后要去系统设置里面把"信任"开关打开,安装过Charles
Https抓包证书的小伙伴应该知道这个步骤。对用户来说不太现实,所以这一步我们一般对服务端证书校验诸如PublicKey、Host、Signature(通过导入OpenSSL
来进行相关校验)。
4.TLS-设置要验证的客户端证书
读取本地app内p12
格式的证书,这里是"client_cert.p12",内置在bundle文件中。password
为证书的密码,客户端设置好之后,服务端会对该证书进行认证。
Swift
/// 设置客户端证书
/// - Parameter options: NWProtocolTLS.Options
func setClientCert(options: NWProtocolTLS.Options, complete: (() -> Void)?) {
guard let curBundle = curBundle(), let certUrl = curBundle.url(forResource: "client_cert", withExtension: "p12") else {
return
}
guard let certData = try? Data(contentsOf: certUrl) else {
return
}
let cfCertData = certData as CFData
let password = "******"
let importOptions = [kSecImportExportPassphrase: password ] as NSDictionary
var rawItems: CFArray?
myQueue.async {
let status = SecPKCS12Import(cfCertData as CFData, // Data from imported Identity.
importOptions as CFDictionary,
&rawItems)
DispatchQueue.main.async {
guard status == errSecSuccess, let items = rawItems, let dictionaryItems = items as? Array<Dictionary<String, Any>> else {
return
}
let secIdentity: SecIdentity = dictionaryItems[0][kSecImportItemIdentity as String] as! SecIdentity
let identity = sec_identity_create(secIdentity)
sec_protocol_options_set_local_identity(options.securityProtocolOptions, identity!)
complete?()
}
}
}
证书校验过程中如果出现错误,可在<Security/SecBase.h>
查找对应错误码的解释。
5.监听Socket连接状态回调
Swift
self?.connection.stateUpdateHandler = { (newState) in
switch newState {
case .ready:
print("state ready")
// 当状态为ready状态时,就可以发送业务数据了
case .cancelled:
print("state cancel")
case .waiting(let error):
print("state waiting \(error)")
case .failed(let error):
print("state failed \(error)")
default:
break
}
}
当connection处于ready
状态下,才能发送业务数据。其它几种状态可以查看苹果的说明文档。
6.通过ProtoBuf协议发送业务层数据
我们先定义一个双方通信的协议,如下
Objective-C
/** Socket包包头定义 */
typedef struct {
/** 包头+包体(2Bytes) */
UInt16 totalLen;
/** 包序列号(2Bytes) */
UInt16 serialNo;
/** 功能号(2Bytes) */
UInt16 funcId;
/** 扩展字段(1Byte) */
UInt8 extension;
} PackageHeader;
一个完整的数据包由包头 + 包体组成, 包头用2个字节表示包的总长度,2个字节表示序列号(回包时对应到相关请求),2个字节表示功能号(发送的是什么样的消息),1个字节的扩展字段。介绍完包头的定义之后,我们来组装一个业务层登录的数据包。
Swift
func sendLoginRequest() {
let loginRequest = LoginRequest();
loginRequest.userId = "12345678";
loginRequest.password = "888888";
let bodyData = loginRequest.data()
if let bodyData {
var header = PackageHeader();
let headerLen = MemoryLayout<PackageHeader>.size
header.totalLen = CFSwapInt16HostToBig(UInt16(headerLen + bodyData.count))
header.serialNo = CFSwapInt16HostToBig(1)
header.funcId = CFSwapInt16HostToBig(10001)
header.extension = 0
let headerData = withUnsafeBytes(of: header) { Data($0) }
let finalData: Data? = headerData + bodyData
self.connection.send(content: finalData, completion: .contentProcessed({ error in
if let sendError = error {
print(sendError)
} else {
print("消息已发送,内容为: ")
}
}))
}
}
在PackageHeader实例转化成Data之前,要对其中的大于1个字节的整型字段进行字节序转化,关于大端序
、小端序
、主机字节序
、网络字节序
相关的介绍大家可自行百度进行了解。下面列了几个大家在组包和拆包过程中要用到几个函数:
函数 | 说明 |
---|---|
MemoryLayout<PackageHeader>.size | 取结构体的大小 |
CFSwapInt16HostToBig | 把Int16类型的变量从主机字节序转为网络字节序 |
CFSwapInt32HostToBig | 把Int32类型的变量从主机字节序转为网络字节序 |
CFSwapInt64HostToBig | 把Int64类型的变量从主机字节序转为网络字节序 |
CFSwapInt16BigToHost | 把Int16类型的变量从网络字节序转为主机字节序 |
CFSwapInt32BigToHost | 把Int32类型的变量从网络字节序转为主机字节序 |
CFSwapInt64BigToHost | 把Int32类型的变量从网络字节序转为主机字节序 |
把包头和包体数据组装成finalData,调用connection的send
方法,就可以把数据发送出去,根据回调我们可以知道消息发送成功与否。
7. 解析收到的回包数据为ProtoBuf对应的类
receive
这个函数有两个参数,minimumIncompleteLength
表示最小接受的字节长度,这里因为我们协议头的长度为7,所以这里设置为7;maximumLength
表示接受的最大数据长度,这里简单设置了一下,仅做测试用。网上比较常用的做法的是,第一次把minimumIncompleteLength
和maximumLength
都设置成包头的长度,在这里就是7。然后通过解析包头后得到包体的长度比如是30,然后通过receive函数设置minimumIncompleteLength
和maximumLength
均为30,就可以读到一个完整的包体数据并解析成具体的类(这里是LoginResponse这个类)。
Swift
self?.connection.receive(minimumIncompleteLength: 7, maximumLength: 1024) { content, contentContext, isComplete, error in
if let error = error {
print(error)
self?.connection.cancel()
return
}
if let data = content {
DispatchQueue.main.async {
let bodyData = data.subdata(in: 7..<data.count)
do {
let loginResponse = try LoginResponse.parse(from: bodyData)
print("loginresponse: \(String(describing: loginResponse))")
} catch {
print("error \(error)")
}
print("total len:\(data.count)")
}
}
if isComplete {
self?.connection.cancel()
self?.connection = nil
}
}