iOS高级理论:CocoaAsyncSocket 介绍与使用

2024-03-01 01:04

本文主要是介绍iOS高级理论:CocoaAsyncSocket 介绍与使用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、简介

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长度转 NSDataNSData *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];
}

自定义数据格式:在数据中放入开始、结束标识;解析时根据格式抓取数据,缺点是数据内不能含有开始或结束标识;
短连接传输,建立一次连接只传输一次数据就关闭;(不推荐)
注:以上代码仅提供粘包的解决思路,具体如何解包以及包头数据结构可以和服务器进行商定。

这篇关于iOS高级理论:CocoaAsyncSocket 介绍与使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/760672

相关文章

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

Android中Dialog的使用详解

《Android中Dialog的使用详解》Dialog(对话框)是Android中常用的UI组件,用于临时显示重要信息或获取用户输入,本文给大家介绍Android中Dialog的使用,感兴趣的朋友一起... 目录android中Dialog的使用详解1. 基本Dialog类型1.1 AlertDialog(

Python使用自带的base64库进行base64编码和解码

《Python使用自带的base64库进行base64编码和解码》在Python中,处理数据的编码和解码是数据传输和存储中非常普遍的需求,其中,Base64是一种常用的编码方案,本文我将详细介绍如何使... 目录引言使用python的base64库进行编码和解码编码函数解码函数Base64编码的应用场景注意

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定