一、简介
CocoaAsyncSocket为Mac和iOS提供了易于利用和功能强大的异步套接字库,主要包含两个类:
GCDAsyncSocket:用GCD搭建的基于TCP/IP协议的socket网络库
GCDAsyncUdpSocket:用GCD搭建的基于UDP/IP协议的socket网络库.
本文主要先容 GCDAsyncSocket的利用,他是一个TCP库,建在Grand Central Dispatch上面的。
二 客户端的利用
2.1 常用的API方法
2.1.1 初始化
- @property (nonatomic, strong) GCDAsyncSocket *clientSocket;
- self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
复制代码 需要delegate和delegate_queue才气使GCDAsyncSocket调用您的委托方法。提供delegateQueue是一个新概念。delegateQueue要求必须是一个串行队列,使得委托方法在delegateQueue队列中实行。
2.1.2 毗连服务器
- NSError *err = nil;
- if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口号" error:&err]) //异步!
- {
- // 如果有错误,很可能是"已经连接"或"没有委托集"
- NSLog(@"I goofed: %@", err);
- }
复制代码 毗连方法是异步的。这意味着当您调用connect方法时,它们会启动后台操作以毗连到所需的主机/端口。
2.1.3 发送数据
- // 发送数据
- - (void)sendData:(NSData *)data{
- // -1表示超时时间无限大
- // tag:消息标记
- [self.clientSocket writeData:data withTimeout:-1 tag:0];
- }
复制代码 通过调用writeData: withTimeout: tag:方法,即可发送数据给服务器。
2.2 常用的委托方法
2.2.1 毗连乐成
- // socket连接成功会执行该方法
- - (void)socket:(GCDAsyncSocket*)sock didConnectToHost:(NSString*)host port:(UInt16)port{
- NSLog(@"--连接成功--");
- [sock readDataWithTimeout:-1 tag:0];
- }
复制代码 2.2.2 收到服务端数据
- // 收到服务器发送的数据会执行该方法
- - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
- NSString *serverStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- NSLog(@"服务端回包了--回包内容--%@---长度%lu",serverStr,(unsigned long)data.length);
- [sock readDataWithTimeout:-1 tag:0];
- }
复制代码 2.2.3 断开
- // 断开连接会调取该方法
- - (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err{
- NSLog(@"--断开连接--");
- // sokect断开连接时,需要清空代理和客户端本身的socket.
- self.clientSocket.delegate = nil;
- self.clientSocket = nil;
- }
复制代码 以上几个委托方法,使我们比较利用比较频繁的委托署理方法。
5、心跳包
心跳包就是在客户端和服务器间定时通知对方本身状态的一个本身界说的下令,按照一定的时间间隔发送,类似于心跳
用来判断对方(设备,历程或别的网元)是否正常运行,接纳定时发送简单的通讯包,如果在指定时间段内未收到对方相应,则判断对方已经离线。
- @property(nonatomic, strong) NSTimer *heartbeatTimer;
- - (void)beginSendHeartbeat{
- // 创建心跳定制器
- self.heartbeatTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(sendHeartbeat:) userInfo:nil repeats:YES];
- [[NSRunLoop mainRunLoop] addTimer:self.heartbeatTimer forMode:NSRunLoopCommonModes];
- }
- - (void)sendHeartbeat:(NSTimer *)timer {
- if (timer != nil) {
- char heartbeat[4] = {0xab,0xcd,0x00,0x00}; // 心跳字节,和服务器协商
- NSData *heartbeatData = [NSData dataWithBytes:&heartbeat length:sizeof(heartbeat)];
- [self.clientSocket writeData:heartbeatData withTimeout:-1 tag:0];
- }
- }
复制代码 2.2 服务端
GCDAsyncSocket还允许您创建服务器,并接受传入的毗连; 服务端利用基本和客户类似,只不外需要开启端口举行监听客户端毗连。
1、初始化
- @property(nonatomic, strong) GCDAsyncSocket *serverSocket;
- // 初始化服务端socket
- self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
复制代码 2、开放服务端的指定端口
- // 开放服务端的指定端口.
- NSError *error = nil;
- BOOL result = [self.serverSocket acceptOnPort:port error:&error];
- if (result) {
- NSLog(@"端口开启成功,并监听客户端请求连接...");
- }else {
- NSLog(@"端口开启失...");
- }
复制代码 3、发送数据给客户端
- // 发送数据,和客户端使用一致
- - (void)sendData:(NSData *)data{
- // -1表示超时时间无限大
- // tag:消息标记
- [self.serverSocket writeData:data withTimeout:-1 tag:0];
- }
复制代码 4、委托方法
(1) 监听到新的客户端socket毗连委托:
- /* 存储所有连接的客户端 socket*/
- @property(nonatomic, strong) NSMutableArray *arrayClient;
- // 监听到新的客户端socket连接,会执行该方法
- - (void)socket:(GCDAsyncSocket *)serveSock didAcceptNewSocket:(GCDAsyncSocket *) newSocket{
-
- NSLog(@"%@ IP: %@: %hu 客户端请求连接...",newSocket,newSocket.connectedHost,newSocket.localPort);
- // 将客户端socket保存起来
- if (![self.arrayClient containsObject:newSocket]) {
- [self.arrayClient addObject:newSocket];
- }
- [newSocket readDataWithTimeout:-1 tag:0];
- }
- 监听到客户端连接,将客户端 socket 保存起来,因为服务器可能会收到很多客户端连接。
复制代码 (2) 读取客户端数据:
- // 读取客户端发送的数据
- - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
- // 记录客户端心跳
- char heartbeat[4] = {0xab,0xcd,0x00,0x00}; // 心跳
- NSData *heartbeatData = [NSData dataWithBytes:&heartbeat length:sizeof(heartbeat)];
- if ([data isEqualToData:heartbeatData]) {
- NSLog(@"*************心跳**************");
- self.heartbeatDateDict[sock.connectedHost] = [NSDate date];
- }
- [sock readDataWithTimeout:-1 tag:0];
- }
复制代码 (3) 断开毗连:
- // 断开连接
- - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
- NSLog(@"断开连接");
- }
复制代码 (4) 监听心跳包:
- // 子线程用于监听心跳包
- @property(nonatomic, strong) NSThread *checkThread;
- // 记录每个心跳缓存
- @property (nonatomic, strong) NSMutableDictionary *heartbeatDateDict;
- // 初始化子线程,并启动
- self.checkThread = [[NSThread alloc]initWithTarget:sharedInstance selector:@selector(checkClientOnline) object:nil];
- [self.checkThread start];
- #pragma checkTimeThread
- // 这里设置10检查一次 数组里所有的客户端socket 最后一次通讯时间,这样的话会有周期差(最多差10s),可以设置为1s检查一次,这样频率快
- // 开启线程 启动runloop 循环检测客户端socket最新time
- - (void)checkClientOnline{
-
- @autoreleasepool {
- [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(repeatCheckClinetOnline) userInfo:nil repeats:YES];
- [[NSRunLoop currentRunLoop] run];
- }
- }
- // 移除 超过心跳时差的 client
- - (void)repeatCheckClinetOnline{
-
- if (self.arrayClient.count == 0) {
- return;
- }
- NSDate *date = [NSDate date];
- for (GCDAsyncSocket *socket in self.arrayClient ) {
- if ([date timeIntervalSinceDate:self.heartbeatDateDict[socket.connectedHost]]>10) {
- [self.arrayClient removeObject:socket];
- }
- }
- }
复制代码 三、CocoaAsyncSocket 读/写操作
3.1 排队 读/写 操作
CocoaAsyncSocket库的最佳功能之一是“排队 读/写 操作”。
写操作: 套接字未毗连,但我还是可以开始写它,库将排队我的全部写操作,在套接字毗连后,它将主动开始实行我的写操作!
- NSError * err = nil ;
- NSError *err = nil;
- if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口号" error:&err]) //异步!
- {
- NSLog(@"I goofed: %@", err);
- }
- //此时套接字未连接。
- //但我还是可以开始写它!
- //库将排队我的所有写操作,
- //在套接字连接后,它将自动开始执行我的写操作!
- [socket writeData: data1 withTimeout: - 1 tag: 1 ];
- [socket writeData: data2 withTimeout: - 1 tag: 2 ];
复制代码 排队读: 我们可以通过长度获取到相应长度的数据,可以很好办理粘包问题。
- [socket readDataToLength: datalength withTimeout: -1 tag: 0];
复制代码 3.2 Tag参数相识
CocoaAsyncSocket中的tag参数是不通过套接字发送的,也不是从套接字读取的。tag参数只需通过各种委托方法回显给您。它旨在帮助简化委托方法中的代码。
- [socket writeData: data1 withTimeout: - 1 tag: 1 ];
- [socket writeData: data2 withTimeout: - 1 tag: 2 ];
- // 当我们发送数据时候使用 tag 标记后,发送后可以在委托方法中根据 tag 看到那条数据已经发送出去了。
- - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag :( long)tag{
- if(tag == 1)
- NSLog(@"First request sent ");
- else if(tag == 2)
- NSLog(@"Second request sent ");
- }
复制代码 在读取时数据时,tag 也很有帮助:
- [socket readDataWithTimeout:-1 tag:0];
- // 读取 tag 与上面方法的 tag 值是一一对应的。
- #define TAG_WELCOME 10
- #define TAG_CAPABILITIES 11
- #define TAG_MSG 12
- - (void)socket:(GCDAsyncSocket *)sender didReadData :( NSData *)data withTag :( long)tag{
- if(tag == TAG_WELCOME)
- {
- // 忽略欢迎信息
- }
- else if(tag == TAG_CAPABILITIES)
- {
- [self processCapabilities:data];
- }
- else if (tag == TAG_MSG)
- {
- [self processMessage: data];
- }
- }
复制代码 四、Tcp 粘包
4.1 什么是tcp粘包?
TCP是面向毗连的,面向流的,提供高可靠性服务。收发两头(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往吸收端的包,更有用的发到对方,利用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,归并成一个大的数据块,然后举行封包。如许,吸收端,就难于分辨出来了,就会出现粘包现象。
4.2 TCP粘包办理方案
现在应用最广泛的是在消息的头部添加数据包长度,吸收方根据消息长度举行吸收;在一条TCP毗连上,数据的流式传输在吸收缓冲区里是有序的,其主要的问题就是第一个包的包尾与第二个包的包头共存吸收缓冲区,所以根据长度读取是十分符合的;
4.2.1 办剃头送方粘包
方案一: 发送产生是由于Nagle算法归并小数据包,那么可以禁用掉该算法;
方案二: TCP提供了强制数据立即传送的操作指令push,当填入数据后调用操作指令就可以立即将数据发送,而不必期待发送缓冲区填充主动发送;
方案三: 数据包中加头,头部信息为整个数据的长度(最广泛最常用);
- // `方案三`发送方解决粘包的代码部分:
- - (void)sendData:(NSData *)data{
-
- NSMutableData *sendData = [NSMutableData data];
- // 获取数据长度
- NSInteger datalength = data.length;
- // NSInteger长度转 NSData
- NSData *lengthData = [NSData dataWithBytes:&datalength length:sizeof(datalength)];
- // 长度几个字节和服务器协商好。这里我们用的是4个字节存储长度信息
- NSData *newLengthData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
- // 拼接长度信息
- [sendData appendData:newLengthData];
- //拼接数据
- [sendData appendData:data];
- // 发送加了长度信息的包
- [self.clientSocket writeData:[sendData copy] withTimeout:-1 tag:0];
- }
复制代码 4.2.2 办理吸收方粘包
1、解析数据包头部信息,根据长度来吸收;(最广泛最常用)
- /**
- 数据缓冲区
- */
- @property (nonatomic, strong) NSMutableData *dataBuffer;;
- // 读取客户端发送的数据,通过包头长度进行拆包
- - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
-
- // 数据存入缓冲区
- [self.dataBuffer appendData:data];
-
- // 如果长度大于4个字节,表示有数据包。4字节为包头,存储包内数据长度
- while (self.dataBuffer.length >= 4) {
-
- NSInteger datalength = 0;
- // 获取包头,并获取长度
- [[self.dataBuffer subdataWithRange:NSMakeRange(0, 4)] getBytes:&datalength length:sizeof(datalength)];
- // 判断缓存区内是否有包
- if (self.dataBuffer.length >= (datalength+4)) {
- // 获取去掉包头的数据
- NSData *realData = [[self.dataBuffer subdataWithRange:NSMakeRange(4, datalength)] copy];
- // 解析处理
- [self handleData:realData socket:sock];
-
- // 移除已经拆过的包
- self.dataBuffer = [NSMutableData dataWithData:[self.dataBuffer subdataWithRange:NSMakeRange(datalength+4, self.dataBuffer.length - (datalength+4))]];
- }else{
- break;
- }
- }
- [sock readDataWithTimeout:-1 tag:0];
- }
复制代码 自界说数据格式:在数据中放入开始、竣事标识;解析时根据格式抓取数据,缺点是数据内不能含有开始或竣事标识;
短毗连传输,建立一次毗连只传输一次数据就关闭;(不推荐)
注:以上代码仅提供粘包的办理思绪,具体如何解包以及包头数据结构可以和服务器举行商定。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |