socket 需求 : 客户端 跟 服务器 如何进行通信 (传输信息) ? 套接字(socket)可以看成是两个网络应用程序进行通信时,各自通信连接中的一 个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机 的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机 的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据 传输要通过套接字(socket)来完成。 端与端之间就像两个不同国家的陌生人, 由于语言不同无法直接沟通 因此双方之间都聘请了一个翻译,而沟通就像这样 : 定义: socket 在哪里 ? Socket : 对数据传输协议TCP/UDP IP 等进行封装的API 接口 , 而应用层直接使 用socket 接口来完成传输 (1)套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对 其进行(2)像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O 插入到网络中,并与网络中的其他应用程序进行通信。(3)网络套接字是IP地址与 端口的组合 (1) : 专门是帮助发送和接收数据的作用 (2) : 像读写文件一样去传输数据 文件 读写 : 打开 -> 读写 -> 关闭 socket 操作 : 请求确认 -> 传输数据 -> 结束传输 请求确认(三次握手) -> 传输数据 -> 结束传输(四次分手) (3) : 如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是 socket =(210.37.145.1:23) 来源 : Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后 来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口 (socket 函 数),也是在Internet上进行应用开发最为通用的API。 但是, 在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如 果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现 意思是 : 购买的系统 已经把socket 之间沟通的过程封装好了, 并生成一套公开 API ,公开给 用户去使用, 只需要按照 接口规则使用即可. socket 有哪些 (系统封装了三种套接字) ? 3种 1. 流式套接字 : 使用TCP协议传输 : 一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送 内设流量控制 选择 : 数据大,对数据准确等要求高的时候 2. 数据报套接字 : 使用UDP协议 : 一种无连接、不可靠的双向数据传输服务 数据包以独立的形式被发送,并且保留了记录边界,不提供可靠性保证 数据在传输过程中可能会丢失(丢了就算了,不会再次发送)或重复,并且不能保证 在接收端按发送顺序接收数据 选择 : 数据校验要求低, 效率高 直播,视频聊天等 掉帧不影响使用的功能需求 3. 原始套接字 : 对较低层协议(如IP或ICMP)进行直接访问 常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网 络设备 何时调用socket ? 开始通信时 : (握手) 任何用户要进行通信都必须创建套接字,而创建套接字是通过系统调用socket() 函数实现的 结束通信时 : (分手) 当对某个套接字的通信结束时,必须调用关闭socket函数关闭指定的套接字。 端口 一个端口 同一时刻只能建立一次连接 同一时刻可以监听多个请求 数据缓冲区 数据请求 缓冲区数据-粘包 多个数据因为网络原因同时进入缓冲区没有发送,而进行了累计, 但是不同请求的 数据混合后进行发送会导致无法区分 解决粘包 设计数据格式 数据长度(4字节) + 数据类型(4字节) + data 通过上面格式区分数据 数据流 二进制数据 套接字调用流程 ? socket数据传输流程: 流程解析 : https://www.jianshu.com/p/066d99da7cbd 部分整理自 1、使用socket()函数创建套接字 int socket(int af, int type, int protocol); 参数 : Af : IP 地址类型 (IPv4 或者 IPv6 等) type : 为数据传输方式,常用的有 SOCK_STREAM(数据准确传输) 和 SOCK_DGRAM (数据可能丢失) protocol : 表示传输协议,常用的 有TCP 和 UDP 传输协议 2、bind() 绑定套接字 3、使用listen() 于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字 进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。 int listen(int sock, int backlog); sock : 待监听的sock backlog : 请求队列的最大长度 listen() : 是被动监听,当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户 端请求时,套接字才会被“唤醒”来响应请求 请求队列 : 当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的, 只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如 果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。 这个缓冲区,就称为请求队列(Request Queue) 缓冲区的长度 : 能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟 为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20 , 大一点几百上千 4、 accept() 函数 当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求 int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); accept() 会阻塞函数执行,直到有新的请求到来 (没有新请求就歇着) 5、socket缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区 write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可 以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这 些都是TCP协议负责的事情 TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网 络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决 于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制 6、I/O缓冲区特性 : 输入输出缓冲区的默认大小一般都是 8K (1)I/O缓冲区在每个TCP套接字中单独存在; (2)I/O缓冲区在创建套接字时自动生成; (3)即使关闭套接字也会继续传送输出缓冲区中遗留的数据; (4)关闭套接字将丢失输入缓冲区中的数据。 7、write()/send() 对于TCP套接字(默认阻塞模式 ) : 1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机 器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。 2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入, write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才 会被唤醒。 3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。 4) 直到所有数据被写入缓冲区 write()/send() 才能返回。 8、read()/recv() 对于TCP套接字(默认阻塞模式 ) : 1) 首先会检查缓冲区,如果输入缓冲区中有数据,那么就读取,否则函数会被阻 塞(暂停执行,等待),直到网络上有数据到来。 2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区 中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读 取。 3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。 这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动 作将暂停,直到上一步动作完成后才能继续,以保持同步性。 8、connect() 客户端向服务器建立连接,使用该函数 : int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 对应服务器的IP地址 三次握手 : 注意 : 在回应对方收到信息时,都要把对方发过来的随机数据包序号 加一 (5000 + 1) , 设置 ACK 标志位 返回 , 对方根据该值是否加一来判断是否真的接收到了 客户端发起请求(三次握手过程):   1) 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向 服务器端发送数据包,客户端就进入了SYN-SEND状态。   2) 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户 端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户 端发送的数据包   服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数 据包没有关系。   服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填 充“确认号(Ack)”字段。   服务器将数据包发出,进入SYN-RECV状态   3) 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是 服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。 数据传输 : 接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示 客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序 号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。   客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发 来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。   至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来 就可以收发数据了。 9、close() 数据传输结束了需要断开连接 断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开, 不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发 量高,服务器压力堪忧。 四次分手 : 参考网上一段话 : [Shake 1] 套接字A:“任务处理完毕,我希望断开连接。” [Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。” 等待片刻后…… [Shake 3] 套接字B:“我准备好了,可以断开连接了。” [Shake 4] 套接字A:“好的,谢谢合作。” 关于 TIME_WAIT MSL ? 数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃, 并通知源主机。这称为报文最大生存时间为 MSL(MSL,Maximum Segment Lifetime) ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间 ,如果 2MSL 后还未收到服务器重传的 FIN 包,就说 明服务器已经收到了 ACK 包 长连接 : 说明 : 长连接简单解释就是一直保持通信连接状态, 直到一方要求断开为止 Socket 如何 保持长连接 ? Socket 是 TCP/UDP 层的封装,通过 socket,我们就能进行 TCP 通信。 而socket封装一个setKeepAlive方法保持长连接, 但是时间间隔是2小时发送一 次心跳, 但是时间太长 所以,需要通过一个短时间间隔来发送心跳保持socket长连接 何为心跳 ? 心跳就是每隔一段时间就向服务端发送一次请求,以保持socket一直连接, 返回数 据简单使用0, 1来表示状态, 比如0 下线,1上线 返回状态要区别正常请求 CocoaAsyncSocket 实现长连接 #import "GCDSocketManager.h" #define SocketHost @"地址" #define SocketPort 端口 @interface GCDSocketManager()<GCDAsyncSocketDelegate> //握手次数 @property(nonatomic,assign) NSInteger pushCount; //断开重连定时器 @property(nonatomic,strong) NSTimer *timer; //重连次数 @property(nonatomic,assign) NSInteger reconnectCount; @end @implementation GCDSocketManager //全局访问点 + (instancetype)sharedSocketManager { static GCDSocketManager *_instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [[self alloc] init]; }); return _instance; } //可以在这里做一些初始化操作 - (instancetype)init { self = [super init]; if (self) { } return self; } #pragma mark 请求连接 //连接 - (void)connectToServer { self.pushCount = 0; self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; NSError *error = nil; [self.socket connectToHost:SocketHost onPort:SocketPort error:&error]; if (error) { DLog(@"SocketConnectError:%@",error); } } #pragma mark 连接成功 //连接成功的回调 - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { DLog(@"socket连接成功"); [self sendDataToServer]; } 部分3: //连接成功后向服务器发送数据 - (void)sendDataToServer { //发送数据代码省略... //发送 [self.socket writeData:jsonData withTimeout:-1 tag:1]; //读取数据 [self.socket readDataWithTimeout:-1 tag:200]; } //连接成功向服务器发送数据后,服务器会有响应 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { [self.socket readDataWithTimeout:-1 tag:200]; //服务器推送次数 self.pushCount++; //在这里进行校验操作,情况分为成功和失败两种,成功的操作一般都是拉取数 } #pragma mark 连接失败 //连接失败的回调 - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { DLog(@"Socket连接失败"); self.pushCount = 0; NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; NSString *currentStatu = [userDefaults valueForKey:@"Statu"]; //程序在前台才进行重连 if ([currentStatu isEqualToString:@"foreground"]) { //重连次数 self.reconnectCount++; //如果连接失败 累加1秒重新连接 减少服务器压力 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 * self.reconnectCount target:self selector:@selector(reconnectServer) userInfo:nil repeats:NO]; self.timer = timer; } } 部分4: //如果连接失败,5秒后重新连接 - (void)reconnectServer { self.pushCount = 0; self.reconnectCount = 0; //连接失败重新连接 NSError *error = nil; [self.socket connectToHost:SocketHost onPort:SocketPort error:&error]; if (error) { DLog(@"SocektConnectError:%@",error); } } #pragma mark 断开连接 //切断连接 - (void)cutOffSocket { DLog(@"socket断开连接"); self.pushCount = 0; self.reconnectCount = 0; [self.timer invalidate]; self.timer = nil; [self.socket disconnect]; } @end