本文是介绍Socket系列的第一篇
前言
作为半路出家的非CS(Computer Science)专业的iOS程序猿,对于计算机网络的相关知识非常薄弱。
原因也是很简单,如果不更深入的了解网络,而是只知道如何使用AFNetworking
、Alamofire
等等的三方网络库,那么我们如何才能成长?
什么是Socket
socket 是一种抽象的定义,我们广义上的计算机网络系统有一个7层模型
层 | OSI定义 |
---|---|
7 | 应用层 |
6 | 表示层 |
5 | 会话层 |
4 | 传输层 |
3 | 网络层 |
2 | 数据链路层 |
1 | 物理层 |
这里,socket 代表的是其中的5、6层,也就是会话层、表示层
其中会话层负责建立客户端和服务端(一般称主动发起连接的一方为客户端,另一方为服务端)的连接
表示层负责数据格式的转化、加密解密等操作
看到这里,就有可能产生疑惑了,socket究竟是什么呢?
在广义的定义下(所有操作系统),socket就是一个普通的c语言的头文件(.h),这个头文件中定义了一些数据结构体,一些操作函数(创建、连接...)。 不同的厂商,不同的系统对于这个头文件的实现细节(.c或者用objective-c中的表示.m)均不相同。
在类Unix系统下,socket抽象类的具象实现一般封装了一些底层的连接协议如(TCP/IP,UDP/IP等)
但是作为上层程序猿的我们不必去关心他们的是如何实现的,只需要知道,这个头文件中的函数的功能,就可以借助这个抽象实现网络通信的功能。
socket是很抽象的定义,在linux下,我们可以使用netstat -anp
查看 socket连接。一般来说,socket是成对出现的(客户端和服务端)。
一个例子:
socket在现实中最像的就是打电话了,电话想要拨通,双方都得有一部电话(socket端口号),电话线(物理层连接)。socket连接就像是打电话
socket长连接(电话长时间沟通)
经常会有面试官问,有没有使用过socket长连接?
这个时候可能就会有疑问了,socket不是一个抽象的定义么,怎么能和长连接结合起来?
通常情况下,如果说到了socket长连接,他们一般特指TCP/IP连接协议,这个协议是面向连接的可靠的数据流服务,它可以维持长时间,持续的交换数据包。因而我们所说的长连接便是基于这个协议实现的,同时socket又为我们实现了这个功能,因此,这个连接协议的长时间维持,也被称为socket长连接
socket短连接(目的明确的电话呼叫)
与长连接是长时间维持住连接以方便持续发送数据,但是很多时候我们并不需要长时间发送,我们只需要确认我们发送了,服务器处理了,就可以了。所以,短连接就产生了,它是TCP/IP连接协议的使用完即关闭。
我们的http协议便是基于此而来
socket的使用
1. socket测试工具的准备
http://sockettest.sourceforge.net/
用法:
需要安装Java jdk, 安装完成之后直接点击jar包即可
我是分割线--------------------------------------
现在的iOS系统是类Unix,因此它的socket是BSD socket,我们介绍的也将是BSD socket的使用
注: 接下来的所有代码演示都是Swift
本文的前言部分: 关于Swift指针的那些事
2. socket的创建
let socketFD = Darwin.socket(domain, SOCK_STREAM, 0)
因为所有的socket相关操作都是Module:Darwin
提供的,我们为了防止函数命名冲突,因此在函数前加上Module名称
domain: 地址描述
-
AF_INET
: 代表IPv4协议(例:192.0.0.1) -
AF_INET6
: 代表IPv6协议(例:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789)
SOCK_STREAM: 代表着建立连接的类型
-
SOCK_STREAM:
代表建立流连接,基于TCP,这个连接建立后对于数据传输会有保障,当我们希望传输文件或者需要做一些确保数据能被接收的操作的时候,我们会使用这个。
-
SOCK_DGRAM:
建立的数据连接(对方是否接收数据包无保证),基于UDP,连接建立后,无法确保数据包准确送达
0: 代表着协议类型,如果未确定填0即可
这个字段并不进行特殊说明(因为我也没有太清楚这个参数的含义-_-)
返回值: Int32
返回值为一个Int32
的整数,需要记录下来,它是操作socket的句柄。
这里,我对这个返回值的理解是,是有可能,有一个全局的链表,专门记录着对应需要操作的socket,返回给我们的就是这个socket所对应的index,我们接下来所有的操作都是基于这个index的
若返回值为-1,则表示socket创建失败,具体的错误码可以通过Darwin.errno
获取
2. socket的设置
socket在建立的时候是有一系列默认的设置的,比如默认socket的操作(读写)是阻塞的(需要等待操作完成,函数才有返回值),但是作为服务器的我们并不希望被阻塞,所以socket提供了对应的修改方法
// 设置非阻塞
// var status = Darwin.fcntl(socketFD, F_SETFL, O_NONBLOCK)
// 设置socket重用
// var resultOn = 1
//status = setsockopt(socketFD,
// SOL_SOCKET,
// SO_REUSEADDR,
// &resultOn,
// socklen_t(MemoryLayout.size(ofValue: resultOn)))
具体的setsockopt()操作细节:
请参照百度百科:https://baike.baidu.com/item/setsockopt/10069288?fr=aladdin
3. socket连接(客户端)
在进行连接前,我们需要做一个场外操作,就是打开我们的socket测试工具,并且开启TCP server 设置端口为9090
// 这个函数的作用是将 "127.0.0.1" 转化为 socket 所需的UInt32 整形
func converIPToUInt32(a: Int, b: Int, c: Int, d: Int) -> in_addr {
return Darwin.in_addr(s_addr: __uint32_t((a << 0) | (b << 8) | (c << 16) | (d << 24)))
}
var sock4: sockaddr_in = sockaddr_in()
sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
// 将ip转换成UInt32
sock4.sin_addr = converIPToUInt32(a: 127, b: 0, c: 0, d: 1)
// 因内存字节和网络通讯字节相反,顾我们需要交换大小端 我们连接的端口是9090
sock4.sin_port = CFSwapInt16HostToBig(9090)
// 设置sin_family 为 AF_INET表示着这个为IPv4 连接
sock4.sin_family = sa_family_t(AF_INET)
// Swift 中指针强转比OC要复杂
let pointer: UnsafePointer<sockaddr> = withUnsafePointer(to: &sock4, {$0.withMemoryRebound(to: sockaddr.self, capacity: 1, {$0})})
var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
guard result != -1 else {
fatalError("Error in connect() function code is \(errno)")
}
这里主要使用到的函数是Darwin.connect()
代码中我们需要的流程是
1). 找出之前获得的socket操作句柄
2). 组装出sockaddr_in结构体,并赋值
3). 将socketaddr_in强转为sockaddr(因为connect需要此类型指针)
4). 调用connect()函数并传参
5). 根据返回值判断连接成功(返回值不能是-1,一般成功即返回0)
这阵我们从测试软件上已经能看到socket已经连接上了
4. 发送数据包
在步骤3中,我们已经完成了socket的连接建立,操作结束后我们已经经历了TCP的3次握手,但是作为上层的调用者,我们对此并无感知,在此我特地单独说一下
接下来就是发送或者接收数据了
-
发送数据
// 这里在"你好"前面增加一个空格,是为了避免SocketTest3 识别文字乱码 let data = " 你好,我是阿帕奇".data(using: .utf8) ?? Data() // 我们将data转为rawPointer指针 也就是c语言中的 (void *) 指针 let rawPointer = data.withUnsafeBytes({UnsafeRawPointer($0)}) // 我们需要提供指针(数据的首地址) 和 数据的长度给函数 var resultWrite = Darwin.write(socketFD, rawPointer, data.count) guard resultWrite != -1 else { fatalError("Error in write() function code is \(errno)") }
写数据还是非常简单的,但是对于Swift来说,就费事在了数据转指针的地方
-
接收数据
// 初始化数据接收区 let readData = Data(count: 500) // 我们将data转为rawPointer指针 let readRawPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)}) // 最后的长度为 缓存区的长度 let resultRead = Darwin.read(socketFD, readRawPointer, readData.count) // 打印socket的返回值 print("\(String(data: readData, encoding: .utf8) ?? "")")
接收数据需要我们在socketTest上输入信息并点击send,随后我们就可以在打印控制台中看到对应的返回值了
总结
其实socket并不神秘,我们对socket感觉神秘,只是因为我们一直用的都是socket的高级用法,对socket的一些高级封装,使我们感受不到socket的存在,神秘感由此而生。
我们在真正的使用过程中,Objective-c有一个库CocoaAsyncSocket是对socket的高级封装;
接下来要说重点了:
我基于CocoaAsyncSocket重写的SwiftAsyncSocket库在历时一个多月的时间里,用纯Swift语言的形式重写完成,重构部分逻辑使得代码更容易阅读,使用方法目前和CocoaAsyncSocket一样。
写这个库的原因也很简单,学习Swift同时学习Socket。
希望大家也能试试我的这个库,如果有问题,也请多交流。
SwiftAsyncSocket开源仓库地址:
https://github.com/chouheiwa/SwiftAsnycSocket
如果觉得还好用,希望能给star哦,如果发现不足,请在github中提issue
结尾
其实在网络的模型中,socket层已经算是很高级的封装了,我们在使用socket的过程中,已经对于网络ip寻址、数据丢失重发等等的操作无感知了。
我们现在前行的每一步,都是前人为我们铺好的道路。
文章中如果有错误,还请各位评论指出
本文首发于,本人博客与公众号(见下图),如果希望转载到公众号,请联系本人开通权限。
网友评论