注册

iOS-TCP网络框架(二)

现在我们已经有了TCP连接, Request, Response和Task, 接下来要做的就是把这一切串起来. 具体来说, 我们需要一个管理方建立并管理TCP连接, 提供接口让调用方通过Request向连接中写入数据, 监听连接中读取到的粘包数据并将数据拆分成单个Response返回给调用方.

TCP连接部分比较简单, 这里我们直接跳过, 从发起数据请求部分开始.

发起数据请求

站在调用方的角度, 发起一个TCP请求与发起一个HTTP请求并没有什么区别. 调用方通过Request提供URL和相应参数, 然后通过completionHandler回调处理请求对应的响应数据, 就像这样:


// SomeViewController.m

- (void)fetchData {

HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:aTCPUrl parameters:someParams header:someHeader];
HHTCPSocketTask *task = [[HHTCPSocketClient sharedInstance] dataTaskWithRequest:request completionHandler:^(NSError *error, id result) {
if (error) {
//handle error
} else {
//handle result
}
}
[task resume];
}
站在协议实现方的角度, 发起网络请求做的事情会多一些. 我们需要将调用方提供的Request和completionHandler打包成一个Task并保存起来, 当调用方调用Task.resume时, 我们再将Request.data写入Socket. 这部分的主要代码如下:

//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

@property (nonatomic, strong) HHTCPSocket *socket;

//任务派发表 以序列号为键保存所有已发出但还未收到响应的Request 待收到响应后再根据序列号一一分发
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, HHTCPSocketTask *> *dispatchTable;

...其他逻辑 略
@end

@implementation HHTCPSocketClient

...其他逻辑 略

#pragma mark - Interface(Public)

//新建数据请求任务 调用方通过此接口定义Request的收到响应后的处理逻辑
- (HHTCPSocketTask *)dataTaskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {

__block NSNumber *taskIdentifier;
//1\. 根据Request新建Task
HHTCPSocketTask *task = [HHTCPSocketTask taskWithRequest:request completionHandler:^(NSError *error, id result) {

//4\. Request已收到响应 从派发表中删除
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable removeObjectForKey:taskIdentifier];
dispatch_semaphore_signal(lock);

!completionHandler ?: completionHandler(error, result);
}];
//2\. 设置Task.client为HHTCPSocketClient 后续会通过Task.client向Socket中写入数据
task.client = self;
taskIdentifier = task.taskIdentifier;

//3\. 将Task保存到派发表中
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
[self.dispatchTable setObject:task forKey:taskIdentifier];
dispatch_semaphore_signal(lock);

return task;
}

- (NSNumber *)dispatchTask:(HHTCPSocketTask *)task {
if (task == nil) { return @-1; }

[task resume];// 通过task.resume接口发起请求 task.resume会调用task.client.resumeTask方法 task.client就是HHTCPSocketClient
return task.taskIdentifier;
}

#pragma mark - Interface(Friend)

//最终向Socket中写入Request.data的地方 此接口只提供给HHTCPSocketTask使用 对外不可见
- (void)resumeTask:(HHTCPSocketTask *)task {

// 向Socket中写入Request格式化好的数据
if (self.socket.isConnected) {
[self.socket writeData:task.request.requestData];
} else {

NSError *error;
if (self.isNetworkReachable) {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorTimeOut);
} else {
error = HHError(HHNetworkErrorNotice, HHNetworkTaskErrorCannotConnectedToInternet);
}
[task completeWithResponseData:nil error:error];
}
}

@end

//HHTCPSocketTask.m

@interface HHTCPSocketTask ()

- (void)setClient:(id)client;//此接口仅提供给上面的HHTCPSocketClient使用 对外不可见

@end

//对外接口 调用方通过通过此接口发起Request
- (void)resume {
...其他逻辑 略

//通知client将task.request的数据写入Socket
[self.client resumeTask:self];
}

简单描述一下代码流程:

  1. 调用方提供Request和completionHandler回调从HHTCPSocketClient获得一个打包好的Task(通过dataTaskWithRequest:completionHandler:接口), HHTCPSocketClient内部会以(Request.serNum: Task)的形式将其保存在dispatchTable中.

  2. 调用方通过Task.resume发起TCP请求, 待收到服务端响应后HHTCPSocketClient会根据Response.serNum从dispatchTable取出Task然后执行调用方提供的completionHandler回调.(这里为了和系统的NSURLSessionTask保持一致的接口, 我给TCPClient和TCPTask加了一些辅助方法, 代码上绕了一个圈, 实际上, Task.resume就是Socket.writeData:Task.Request.Data).

处理请求响应

正常情况下, 请求发出后, 很快就就会收到服务端的响应二进制数据, 我们要做的就是, 从这些二进制数据中切割出单个Response报文, 然后一一进行分发. 代码如下:


//HHTCPSocketClient.m

@interface HHTCPSocketClient()<HHTCPSocketDelegate>

//保存所有收到的服务端数据 等待解析
@property (nonatomic, strong) NSMutableData *buffer;
...其他逻辑 略
@end

#pragma mark - HHTCPSocketDelegate

//从Socket从读取到数据
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.buffer appendData:data]; //1\. 保存读取到的二进制数据

[self readBuffer];//2\. 根据协议解析二进制数据
}

#pragma mark - Parse

//递归截取Response报文 因为读取到的数据可能已经"粘包" 所以需要递归
- (void)readBuffer {
if (self.isReading) { return; }

self.isReading = YES;
NSData *responseData = [self getParsedResponseData];//1\. 从已读取到的二进制中截取单个Response报文数据
[self dispatchResponse:responseData];//2\. 将Response报文派发给对应的Task
self.isReading = NO;

if (responseData.length == 0) { return; }
[self readBuffer]; //3\. 递归解析
}

//根据定义的协议从buffer中截取出单个Response报文
- (NSData *)getParsedResponseData {

NSData *totalReceivedData = self.buffer;
//1\. 每个Response报文必有的16个字节(url+serNum+respCode+contentLen)
uint32_t responseHeaderLength = [HHTCPSocketResponseParser responseHeaderLength];
if (totalReceivedData.length < responseHeaderLength) { return nil; }

//2\. 根据定义的协议读取出Response.content的长度
NSData *responseData;
uint32_t responseContentLength = [HHTCPSocketResponseParser responseContentLengthFromData:totalReceivedData];
//3\. Response.content的长度加上必有的16个字节即为整个Response报文的长度
uint32_t responseLength = responseHeaderLength + responseContentLength;
if (totalReceivedData.length < responseLength) { return nil; }

//4\. 根据上面解析出的responseLength截取出单个Response报文
responseData = [totalReceivedData subdataWithRange:NSMakeRange(0, responseLength)];
self.buffer = [[totalReceivedData subdataWithRange:NSMakeRange(responseLength, totalReceivedData.length - responseLength)] mutableCopy];
return responseData;
}

//将Response报文解析Response 然后交由对应的Task进行派发
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url > TCP_max_notification) {/** 请求响应 */

HHTCPSocketTask *task = self.dispatchTable[@(response.serNum)];
[task completeWithResponse:response error:nil];
} else {/** 推送或心跳 略 */
...
}
}

简单描述下代码流程:

  1. TCPClient监听Socket读取数据回调方法, 将读取到的服务端二进制数据添加到buffer中.

  2. 根据定义的协议从buffer头部开始, 不停地截取出单个Response报文, 直到buffer数据取无可取.

  3. 从2中截取到的Response报文中解析出Response.serNum, 根据serNum从dispatchTable中取出对应的Task(Response.serNum == Request.serNum), 将Response交付给Task. 至此, TCPClient的工作完成.

  4. Task拿到Response后通过completionHandler交付给调用方. 至此, 一次TCPTask完成.

这里需要注意的是, Socket的回调方法我这边默认都是在串行队列中执行的, 所以对buffer的操作并不没有加锁, 如果是在并行队列中执行Socket的回调, 请记得对buffer操作加锁.

处理后台推送

除了Request对应的Response, 服务端有时也会主动发送一些推送数据给客户端, 我们也需要处理一下:


//HHTCPSocketClient.m

- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url > TCP_max_notification) {/** 请求响应 略*/
//...
} else if (response.url == TCP_heatbeat) {/** 心跳 略 */
//...
} else {/** 推送 */
[self dispatchRemoteNotification:response];
}
}

//各种推送 自行处理
- (void)dispatchRemoteNotification:(HHTCPSocketResponse *)notification {

switch (notification.url) {
case TCP_notification_xxx: ...
case TCP_notification_yyy: ...
case TCP_notification_zzz: ...
default:break;
}
}

请求超时和取消

TCP协议的可靠性规定了数据会完整的, 有序的进行传输, 但并未规定数据传输的最大时长. 这意味着, 从发起Request到收到Response的时间间隔可能比我们能接受的时间间隔要长. 这里我们也简单处理一下, 代码如下:


//HHTCPSocketTask.m

#pragma mark - Interface

- (void)cancel {
if (![self canResponse]) { return; }

self.state = HHTCPSocketTaskStateCanceled;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorCanceled]];
}

- (void)resume {
if (self.state != HHTCPSocketTaskStateSuspended) { return; }

//发起Request的同时也启动一个timer timer超时直接返回错误并忽略后续的Response
self.timer = [NSTimer scheduledTimerWithTimeInterval:self.request.timeoutInterval target:self selector:@selector(requestTimeout) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

self.state = HHTCPSocketTaskStateRunning;
[self.client resumeTask:self];
}

#pragma mark - Action

- (void)requestTimeout {
if (![self canResponse]) { return; }

self.state = HHTCPSocketTaskStateCompleted;
[self completeWithResult:nil error:[self taskErrorWithResponeCode:HHNetworkTaskErrorTimeOut]];
}

#pragma mark - Utils

- (BOOL)canResponse {
return self.state <= HHTCPSocketTaskStateRunning;
}

代码很简单, 只是在写入Task.Request的同时也开启一个timer, timer超时就直接忽略Response并返回错误给调用方而已. 对于类似HTTP的GET请求而言, 忽略和取消几乎是等价的. 但对于POST请求而言, 我们需要的可能就是直接断开连接了

心跳

目前为止, 我们已经有了一个简单的TCP客户端, 它可以发送数据请求, 接收数据响应, 还能处理服务端推送. 最后, 我们做一下收尾工作: 心跳

单向的心跳就不说了, 这里我们给到一张Ping-Pong的简易图:

b7bba27b07d46ac2ba731ff71afb48bb.png



当发送方为客户端时, Ping-Pong通常用来验证TCP连接的有效性. 具体来说, 如果Ping-Pong正常, 那么证明连接有效, 数据传输没有问题, 反之, 要么连接已断开, 要么连接还在但服务器已经过载无力进行恢复, 此时客户端可以选择断开重连或者切换服务器.

当发送方为服务端时, Ping-Pong通常用来验证数据传输的即时性. 具体来说, 当服务端向客户端发送一条即时性消息时通常还会马上Ping一下客户端, 如果客户端即时进行回应, 那么说明Ping之前的即时性消息已经到达, 反之, 消息不够即时, 服务端可能会走APNS再次发送该消息.

Demo中我简单实现了一下Ping-Pong, 代码如下:


//HHTCPSocketHeartbeat

static NSUInteger maxMissTime = 3;
@implementation HHTCPSocketHeartbeat

+ (instancetype)heartbeatWithClient:(id)client timeoutHandler:(void (^)(void))timeoutHandler {

HHTCPSocketHeartbeat *heartbeat = [HHTCPSocketHeartbeat new];
heartbeat.client = client;
heartbeat.missTime = -1;
heartbeat.timeoutHandler = timeoutHandler;
return heartbeat;
}

- (void)start {

[self stop];
self.timer = [NSTimer timerWithTimeInterval:60 target:self selector:@selector(sendHeatbeat) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)stop {
[self.timer invalidate];
}

- (void)reset {
self.missTime = -1;
[self start];
}

- (void)sendHeatbeat {

self.missTime += 1;
if (self.missTime >= maxMissTime && self.timeoutHandler != nil) {//心跳超时 执行超时回调
self.timeoutHandler();
self.missTime = -1;
}

HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(TCP_heatbeat)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}

- (void)handleServerAckNum:(uint32_t)ackNum {
if (ackNum == TCP_heatbeat) {//服务端返回的心跳回应Pong 不用处理
self.missTime = -1;
return;
}

//服务端发起的Ping 需要回应
HHTCPSocketRequest *request = [HHTCPSocketRequest requestWithURL:TCP_heatbeat parameters:@{@"ackNum": @(ackNum)} header:nil];
[self.client dispatchDataTaskWithRequest:request completionHandler:nil];
}

@end

HHTCPSocketHeartbeat每隔一段时间就会发起一个serNum固定为1的心跳请求Ping一下服务端, 在超时时间间隔内当收到任何服务端回应, 我们认为连接有效, 心跳重置, 否则执行调用方设置的超时回调. 另外, HHTCPSocketHeartbeat还负责回应服务端发起的serNum为随机数的即时性Response(这里的随机数我给的是时间戳).

//HHTCPSocketClient.m

- (void)configuration {

self.heatbeat = [HHTCPSocketHeartbeat heartbeatWithClient:self timeoutHandler:^{//客户端心跳超时回调
// [self reconnect];
SocketLog(@"heartbeat timeout");
}];
}

#pragma mark - HHTCPSocketDelegate

- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
[self.heatbeat reset];//连接成功 客户端心跳启动
}

- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error {
[self.heatbeat stop];//连接断开 客户端心跳停止
}

- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data {
[self.heatbeat reset];//收到服务端数据 说明连接有效 重置心跳
//...其他 略
}

//获取到服务端Response
- (void)dispatchResponse:(NSData *)responseData {
HHTCPSocketResponse *response = [HHTCPSocketResponse responseWithData:responseData];
if (response == nil) { return; }

if (response.url == TCP_heatbeat) {/** 心跳 */
[self.heatbeat handleServerAckNum:response.serNum];//回复服务端心跳请求 如果有必要的话
}
}
文件下载/上传?

到目前为止, 我们讨论的都是类似DataTask的数据请求, 并未涉及到文件下载/上传请求, 事实上, 我也没打算在通讯协议上加上这两种请求的支持. 这部分我是这样考虑的:

如果传输的文件比较小, 那么仿照HTTP直接给协议加上ContentType字段, Content以特殊分隔符进行分隔即可.

如果传输的文件比较大, 那么直接在当前连接进行文件传输可能会阻塞其他的数据传输, 这是我们不希望看到的, 所以一定是另起一条连接专用于大文件传输. 考虑到文件传输不太可能像普通数据传输那样需要即时性和服务端推送, 为了节省服务端开销, 文件传输完成后连接也没有必要继续保持. 这里的"建立连接-文件传输-断开连接"其实已经由HTTP实现得很好了, 而且功能还多, 我们没必要再做重复工作.

基于以上考虑, 文件传输这块我更趋向于直接使用HTTP而不是自行实现.

至此, TCP部分的讨论就结束了.



作者:Cooci
链接:https://www.jianshu.com/p/c0df2690e9d4



0 个评论

要回复文章请先登录注册