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,进行衔接。代码如下:
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认证通道建立起来了。
// 设置服务端证书验证的回调
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
为证书的暗码,客户端设置好之后,服务端会对该证书进行认证。
/// 设置客户端证书
/// - 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衔接状况回调
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协议发送事务层数据
咱们先界说一个两边通信的协议,如下
/** Socket包包头界说 */
typedef struct {
/** 包头+包体(2Bytes) */
UInt16 totalLen;
/** 包序列号(2Bytes) */
UInt16 serialNo;
/** 功用号(2Bytes) */
UInt16 funcId;
/** 扩展字段(1Byte) */
UInt8 extension;
} PackageHeader;
一个完好的数据包由包头 + 包体组成, 包头用2个字节表明包的总长度,2个字节表明序列号(回包时对应到相关恳求),2个字节表明功用号(发送的是什么样的音讯),1个字节的扩展字段。介绍完包头的界说之后,咱们来组装一个事务层登录的数据包。
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这个类)。
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
}
}