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

相关文章

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

oracle DBMS_SQL.PARSE的使用方法和示例

《oracleDBMS_SQL.PARSE的使用方法和示例》DBMS_SQL是Oracle数据库中的一个强大包,用于动态构建和执行SQL语句,DBMS_SQL.PARSE过程解析SQL语句或PL/S... 目录语法示例注意事项DBMS_SQL 是 oracle 数据库中的一个强大包,它允许动态地构建和执行

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详