iOS Socket编程学习笔记

Socket是基于传输层TCP、UDP协议封装的一套API接口,它本身并不是一种协议。

网络通信模型

传输层中的协议

传输控制协议 (TCP)

TCP全称是Transmission Control Protocol,中文名为传输控制协议,它可以提供可靠的、面向连接的网络数据传递服务。

传输控制协议主要包含下列任务和功能:
  • 确保IP数据报的成功传递

  • 对程序发送的大块数据进行分段和重组

  • 确保正确排序及按顺序传递分段的数据

  • 通过计算校验和,进行传输数据的完整性检查

  • 根据数据是否接收成功发送肯定消息。通过使用选择性确认,也对没有收到的数据发送否定确认

TCP工作流程

TCP的连接建立过程又称为TCP三次握手

一旦初始的三次握手完成,client和server就可以相互发送信息。

而断开连接时,就不分client和server了,先请求断开的即为主动方,主动方会先发送断开请求,被动发在收到断开请求后会先给主动方一个确认,然后待被动方处理完所有的传输事务后,再给主动方一个断开连接的请求,
主动方收到被动方的断开请求,给被动方一个断开确认。至此断开成功。

TCP工作过程比较复杂,包括的内容如下:

  • TCP连接关闭:发送方主机和目的主机建立TCP连接并完成数据传输后,会发送一个将结束标记置1的数据包,以关闭这个TCP连接,并同时释放该连接占用的缓冲区空间。

  • TCP重置:TCP允许在传输的过程中突然中断连接。

  • TCP数据排序和确认*:在传输的过程中使用序列号和确认号来跟踪数据的接收情况。

  • TCP重传:在TCP的传输过程中,如果在重传超时时间内没有收到接收方主机对某数据包的确认回复,发送方主机就认为此数据包丢失,并再次发送这个数据包给接收方。

  • TCP延迟确认:TCP并不总是在接收到数据后立即对其进行确认,它允许主机在接收数据的同时发送自己的确认信息给对方。

  • TCP数据保护(校验):TCP是可靠传输的协议,它提供校验和计算来实现数据在传输过程中的完整性。

tcp报文格式

  • 源端口、目标端口:计算机上的进程要和其他进程通信是要通过计算机端口的,而一个计算机端口某个时刻只能被一个进程占用,所以通过指定源端口和目标端口,就可以知道是哪两个进程需要通信。源端口、目标端口是用16位表示的,可推算计算机的端口个数为2^16个。

  • 序列号:表示本报文段所发送数据的第一个字节的编号。在TCP连接中所传送的字节流的每一个字节都会按顺序编号。由于序列号由32位表示,所以每2^32个字节,就会出现序列号回绕,再次从 0 开始。那如何区分两个相同序列号的不同TCP报文段就是一个问题了,后面会有答案,暂时可以不管。

  • 确认号:表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。也就是告诉发送发:我希望你(指发送方)下次发送的数据的第一个字节数据的编号是这个确认号。也就是告诉发送方:我希望你(指发送方)下次发送给我的TCP报文段的序列号字段的值是这个确认号。

  • TCP首部长度:由于TCP首部包含一个长度可变的选项部分,所以需要这么一个值来指定这个TCP报文段到底有多长。或者可以这么理解:就是表示TCP报文段中数据部分在整个TCP报文段中的位置。该字段的单位是32位字,即:4个字节。

  • URG:表示本报文段中发送的数据是否包含紧急数据。URG=1,表示有紧急数据。后面的紧急指针字段只有当URG=1时才有效。

  • ACK:表示是否前面的确认号字段是否有效。ACK=1,表示有效。只有当ACK=1时,前面的确认号字段才有效。TCP规定,连接建立后,ACK必须为1。

  • PSH:告诉对方收到该报文段后是否应该立即把数据推送给上层。如果为1,则表示对方应当立即把数据提交给上层,而不是缓存起来。

  • RST:只有当RST=1时才有用。如果你收到一个RST=1的报文,说明你与主机的连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。或者说明你上次发送给主机的数据有问题,主机拒绝响应。

  • SYN:在建立连接时使用,用来同步序号。当SYN=1,ACK=0时,表示这是一个请求建立连接的报文段;当SYN=1,ACK=1时,表示对方同意建立连接。SYN=1,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中SYN才置为1。

  • FIN:标记数据是否发送完毕。如果FIN=1,就相当于告诉对方:“我的数据已经发送完毕,你可以释放连接了”

  • 窗口大小:表示现在运行对方发送的数据量。也就是告诉对方,从本报文段的确认号开始允许对方发送的数据量。

  • 校验和:提供额外的可靠性。具体如何校验,参考其他资料。

  • 紧急指针:标记紧急数据在数据字段中的位置。

  • 选项部分:其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,那么选项部分最长为:(2^4-1)*4-20=40字节。

选项部分的应用:
  • MSS最大报文段长度(Maxium Segment Size):指明数据字段的最大长度,数据字段的长度加上TCP首部的长度才等于整个TCP报文段的长度。MSS值指示自己期望对方发送TCP报文段时那个数据字段的长度。通信双方可以有不同的MSS值。如果未填写,默认采用536字节。MSS只出现在SYN报文中。即:MSS出现在SYN=1的报文段中。

  • 窗口扩大选项(Windows Scaling):由于TCP首部的窗口大小字段长度是16位,所以其表示的最大数是65535。但是随着时延和带宽比较大的通信产生(如卫星通信),需要更大的窗口来满足性能和吞吐率,所以产生了这个窗口扩大选项。

  • SACK选择确认项(Selective Acknowledgements):用来确保只重传缺少的报文段,而不是重传所有报文段。比如主机A发送报文段1、2、3,而主机B仅收到报文段1、3。那么此时就需要使用SACK选项来告诉发送方只发送丢失的数据。那么又如何指明丢失了哪些报文段呢?使用SACK需要两个功能字节。一个表示要使用SACK选项,另一个指明这个选项占用多少字节。描述丢失的报文段2,是通过描述它的左右边界报文段1、3来完成的。而这个1、3实际上是表示序列号,所以描述一个丢失的报文段需要64位即8个字节的空间。那么可以推算整个选项字段最多描述(40-2)/8=4个丢失的报文段。

  • 时间戳选项(Timestamps):可以用来计算RTT(往返时间),发送方发送TCP报文时,把当前的时间值放入时间戳字段,接收方收到后发送确认报文时,把这个时间戳字段的值复制到确认报文中,当发送方收到确认报文后即可计算出RTT。也可以用来防止回绕序号PAWS,也可以说可以用来区分相同序列号的不同报文。因为序列号用32为表示,每2^32个序列号就会产生回绕,那么使用时间戳字段就很容易区分相同序列号的不同报文。

  • NOP(NO-Operation):它要求选项部分中的每种选项长度必须是4字节的倍数,不足的则用NOP填充。同时也可以用来分割不同的选项字段。如窗口扩大选项和SACK之间用NOP隔开。

用户数据报协议(UDP)

UDP全称是User Datagram Protocol,中文名为用户数据报协议。UDP 提供无连接的网络服务,该服务对消息中传输的数据提供不可靠的、最大努力传送。这意味着它不保证数据报的到达,也不保证所传送数据包的顺序是否正确。

虽然TCP中植入了各种安全保障功能,但是在实际执行的过程中会占用大量的系统开销,无疑使速度受到严重的影响。反观UDP由于排除了信息可靠传递机制,将安全和排序等功能移交给上层应用来完成,极大地降低了执行时间,使速度得到了保证。

Socket编程-Client

iOS中实现Socekt编程有下面几种方式:

  • BSDSocket iOS系统基于unix,所以支持底层的BSD Socket。
1
2
//相关api位于下面这个文件中
#import <sys/socket.h>
  • CFSocket 对底层BSD Socket进行轻量级的封装
1
2
//相关api位于下面这个文件中
#import <CoreFoundation/CFSocket.h>
  • CocoaAsyncSocket 使用OC封装的一个socket框架(支持TCP和UDP)
    github

这里使用的是CFSocket结合BSDSocket

创建socket对象并连接服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (void)connectServerWithServerIP:(NSString *)serverIP{
CFSocketContext socketContext = {
0,//结构体的版本,必须为0
(__bridge void *)(self),
NULL,
NULL,
NULL,
};
_mSocket = CFSocketCreate(kCFAllocatorDefault,//为新对象分配内存,可以为nil
PF_INET,//协议族,如果为0或者负数,则默认为PF_INET
SOCK_STREAM,//套接字类型,如果协议族为PF_INET,则它会默认为SOCK_STREAM
IPPROTO_TCP,//套接字协议,如果协议族是PF_INET且协议是0或者负数,它会默认为IPPROTO_TCP
kCFSocketConnectCallBack,//触发回调函数的socket消息类型,具体见Callback Types
socketMessageCallBack,//上面情况下触发的回调函数
&socketContext);//一个持有CFSocket结构信息的对象,可以为nil
if (_mSocket != NULL) {
struct sockaddr_in addr4;//IPV4
memset(&addr4, 0, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(8888);
addr4.sin_addr.s_addr = inet_addr([serverIP UTF8String]);//把字符串的地址转换为机器可识别的网络地址
//把sockaddr_in结构体中的地址转换为Data
CFDataRef address = CFDataCreate(kCFAllocatorDefault,
(UInt8 *)&addr4, sizeof(addr4));
CFSocketConnectToAddress(_mSocket,//当前连接的socket
address,//CFDataRef类型的包含上面socket的远程地址的对象
-1);//连接超时时间,如果为负,则不尝试连接,而是把连接放在后台进行,如果_socket消息类型为kCFSocketConnectCallBack,将会在连接成功或失败的时候在后台触发回调函数
CFRunLoopRef cRunRef = CFRunLoopGetCurrent();//获取当前线程的循环
//创建一个循环,但并没有真正加如到循环中,需要调用CFRunLoopAddSource
CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _mSocket, 0);
CFRunLoopAddSource(cRunRef, sourceRef, kCFRunLoopCommonModes);
CFRelease(cRunRef);
CFRelease(sourceRef);
NSLog(@"start connect");
}
}

接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case kCFSocketConnectCallBack:
{
NSLog(@"connect callback");
//data为回调的错误码,为NULL则为成功
if (data != NULL)
{
NSLog(@"连接失败");
return;
}
else{
//更新界面需要在主线程
dispatch_async(dispatch_get_main_queue(), ^{
[mSelf ShowMsg:@"服务器连接成功,现在可以开始通信了!!!"];
});
//持续的接收数据,不能在主线程做,会阻塞当前线程,放到一个子线程去做。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[mSelf readMessageLoop];
});
}
}
break;

判断当前连接成功后,可以开一个线程监听是否接收到数据。

注意这里的监听需要在子线程中进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)readMessageLoop{
while (1) {
char buffer[1024];
ssize_t recvLen = recv(CFSocketGetNative(_mSocket), buffer, sizeof(buffer), 0);
if (recvLen > 0) {
@autoreleasepool {
NSString *str = @"服务器发来数据:";
NSData *data = [NSData dataWithBytes:buffer length:recvLen];
str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//回界面显示信息
dispatch_async(dispatch_get_main_queue(), ^{
[self ShowMsg:str];
});
}
}
}
}

发送数据

1
2
3
4
5
// 发送数据
- (void)sendMessage {
const char *sData = [self.sendContentTextField.text UTF8String];
send(CFSocketGetNative(_mSocket), sData, strlen(sData) + 1, 0);
}

断开连接

1
2
3
4
5
6
7
- (IBAction)closeServerConnectBtnClicked:(id)sender {
if(CFSocketIsValid(_mSocket))
{
close(CFSocketGetNative(_mSocket));//关闭socket
CFSocketInvalidate(_mSocket);
CFRelease(_mSocket);
}}

Socket编程-Server

在本机搭建一个MAC APP模拟Server,实现和Client的通信。

创建Socket对象绑定端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (int)setupSocket{
int bRet = 0;
CFSocketContext sockContext = {0, // 结构体的版本,必须为0
(__bridge void *)(self),
NULL, // 一个定义在上面指针中的retain的回调, 可以为NULL
NULL,
NULL};
_mSocket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, serverAcceptCallBack, &sockContext);
if (NULL == _mSocket) {
NSLog(@"Cannot create socket!");
return 0;
}
int optval = 1;
setsockopt(CFSocketGetNative(_mSocket), SOL_SOCKET, SO_REUSEADDR, // 允许重用本地地址和端口
(void *)&optval, sizeof(optval));
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(6666);
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&addr4, sizeof(addr4));
if (kCFSocketSuccess != CFSocketSetAddress(_mSocket, address))
{
NSLog(@"Bind to address failed!");
if (_mSocket)
CFRelease(_mSocket);
_mSocket = NULL;
bRet = 0;
return bRet;
}
CFRunLoopRef cfRunLoop = CFRunLoopGetCurrent();
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _mSocket, 0);
CFRunLoopAddSource(cfRunLoop, source, kCFRunLoopCommonModes);
CFRelease(source);
bRet = 1;
return bRet;
}

和Client相比多了一个setsockopt的过程。

Accept回调处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (type == kCFSocketAcceptCallBack) {
// 本地套接字句柄
CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
uint8_t name[SOCK_MAXADDRLEN];
socklen_t nameLen = sizeof(name);
if (0 != getpeername(nativeSocketHandle, (struct sockaddr *)name, &nameLen)) {
NSLog(@"error");
exit(1);
}
CFReadStreamRef iStream;
CFWriteStreamRef oStream;
// 创建一个可读写的socket连接
CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &iStream, &oStream);
if (iStream && oStream)
{
CFStreamClientContext streamContext = {0, info, NULL, NULL};
if (!CFReadStreamSetClient(iStream, kCFStreamEventHasBytesAvailable,readStream, &streamContext))
{
exit(1);
}
if (!CFWriteStreamSetClient(oStream, kCFStreamEventCanAcceptBytes, writeStream, &streamContext))
{
exit(1);
}
CFReadStreamScheduleWithRunLoop(iStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
CFWriteStreamScheduleWithRunLoop(oStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
CFReadStreamOpen(iStream);
CFWriteStreamOpen(oStream);
} else
{
close(nativeSocketHandle);
}
}

创建一个可读写的Socket连接,当接收或者发送数据时,都会走到回调接口中。

读数据回调

1
2
3
4
5
6
7
8
9
10
11
12
13
// 读取数据
void readStream(CFReadStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
{
UInt8 buff[255];
CFReadStreamRead(stream, buff, 255);
///根据delegate显示到主界面去
NSString *strMsg = [[NSString alloc]initWithFormat:@"客户端传来消息:%s",buff];
ViewController *mSelf = (__bridge ViewController*)clientCallBackInfo;
dispatch_async(dispatch_get_main_queue(), ^{
[mSelf ShowMsg:strMsg];
});
}

写数据回调

1
2
3
4
void writeStream (CFWriteStreamRef stream, CFStreamEventType eventType, void *clientCallBackInfo)
{
outputStream = stream;
}

保存当前写数据的实例。

发送数据

1
2
3
4
5
6
7
8
9
10
11
- (IBAction)sendMessage:(id)sender {
const char *str = [self.sendTextField.stringValue UTF8String];
uint8_t *sendS = (uint8_t *)str;
if (outputStream != NULL) {
CFWriteStreamWrite(outputStream, sendS, strlen(str) + 1);
}
else{
NSLog(@"send failed");
}
}

通信过程如下图:

iOS Socket Demo