被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效

被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效
文章图片

被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效
文章图片

近日,被波哥催稿,营业时间被迫提前,就翻了数十年来翻深不见的代码仓库,找到了多年前在移动端做长链接开发中的一种实践方法,可以让长链接的业务代码和短链接一样高效、简洁的小技巧,就拿来应付交个差。
此文并不会针对 TCP 做详尽的展开说明,也不会针对如何使用 Socket 库进行说明、协议格式定义、如何传输等都不在本文范围之内,本文关注的是移动端如何进行高效并简洁地编写基于 TCP 的业务代码,让长链接的业务代码和短链接的业务代码写起来一样高效。
先过一下基础知识:
被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效
文章图片

记住重点:

  • 短链接:一个 request 对应一个 respones,必是成对出现,有来有回,他们的关系为:R:Q = N:N = 1
  • 长链接:发送和接收只是把数据写入缓存区或者从缓存区读取数据,并没有明确的 request & respones 的概念,他们的关系为:R:Q = M:N
被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效
文章图片

正因为这种区别,TCP 在带来链路复用、主动推送、高效、快速等很多优势的同时也引入较多的复杂性,其中的问题之一就是基于 TCP 写业务代码比较繁琐。
本文会以 iOS 开发为例,对问题进行说明,其它语言同学们可以自行同理实现。
短链接例子 在App上进行一个短链接的操作十分常见,通过 block 的形式进行网络请求,比如广泛使用的 AFNetworking 库中是这样使用的:
NSString *url = @"http://api.xxx.com/method"; [[self shareAFNManager] GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSLog(@"responseObject-->%@",responseObject); UpdateUI() } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"error-->%@",error); }]; ````此处的 block 是 Objective-C 语言中的一种叫法,理解为函数指针即可,因为这种写法可以在对应的 block 中直接进行业务处理,比如:更新 UI 等,这种操作就给写业务带来了十分方便的作用,本质上也是因为短链接的形式满足 R:Q = N:N = 1的关系## 长链接那么在长链接中要实现上面的功能就十分麻烦,除了 Socket 基本功能以外,响应数据更新UI就得处理网络线程切换UI线程,UI 生命周期和数据生命周期匹配,数据通知等诸多问题。在 handle 或者 dispatch 方法的实现中,很多同学会陷入一个陷阱,要不就是实现的特别麻烦,要不然就是实现的漏洞百出,在不频繁的情况下可能会选择 notification 或者类似的简陋方案顶一下,即便这种写起来也会存在很多坑:通知的注册、管理、销毁等都是要小心应付的内容,这也是很多同学写不好长链接业务代码的问题所在。## 方案既然有了问题,本着「**凡是痛苦的就是需求之所在**」,按照短链接的思路进行简单地梳理:> 提炼 request & respones,约定关系为:R:Q = N:N = 1核心问题就这一条,只要能抽象出这个关系,那么长链接就可以像短链接一样方便,包括很多同学以为`TCP` 的缓存模块较难实现的问题也会变得非常之简单。在 `TCP` 的协议定制中,约定 `message{} & message_reply{}` 对应处理一个网络请求,比如在 protobuf 中定义一个 ping 包

message ping{
string request_id = 1;

}
message ping_reply{
string request_id = 1;

}
协议设计时按照这种约定就实现了 R:Q = N:N = 1 的问题### request_id其中的 request_id 是一个较为「骚」的操作,引一个字段完美解决大问题。![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f959f1a9b87746ee8691eb2ea67dd54a~tplv-k3u1fbpfcp-zoom-1.image)过程中牢记短链接的特点,有来有回,有来必有回(不许抬杠,没有网络也会有超时或者网络未链接等错误返回)。request_id 由 client 产生和维护,在发送请求中发给服务器,服务器将此值原封不动返回给 client,client 就可以根据此 request_id 找到对应的网络请求原始位置。比如起一个名字叫 SendCore 来实现网络层,包含的主要结构如下:

typedef void (^SocketDidReadBlock)(NSError *__nullable error, id __nullable data);
@interface SendCore : NSObject
  • (nullable SendCore *)sharedManager;
  • (void) sendEnterChatRoom:(nullable EnterChatRoom *) data completion:(__nullable SocketDidReadBlock)callback;
  • (void) CloseSocket;
    @end
SocketDidReadBlock 约定函数指针的形式,必须满足简单、可扩展的要求。以进入聊天室的函数为例:

  • (void) sendEnterChatRoom:(nullable EnterChatRoom *) data completion:(__nullable SocketDidReadBlock)callback
    {
    if (data =https://www.it610.com/article/= nil ) {
    return;

    }
    NSString * blockRequestID =[self createRequestID];
    data.requestId = blockRequestID;
    if (callback) {
    [self addRequestIDToMap:blockRequestID block:callback];

    }
    [self sendProtocolWithCmd:CmdType_Enter1V1MovieRoom withCmdData:[data data] completion:callback];
    }
由 client 生成一个 request_id,并将 callback 存入 block_table中,在服务器返回数据中解出 request_id,然后在 block_table 中找到对应的函数指针并执行。

-(void) handerProtocol:(CmdType) protocolID packet:(NSData *) packet
{
NSError *error = nil; id reslutData = nil; NSString *requestId = nil; switch (protocolID) { case CmdType_Enter1V1MovieRoomReply: error = nil; syncObjInfo =[SyncObjInfo parseFromData:packet error:&error]; reslutData = syncObjInfo; requestId = syncObjInfo.requestId; didReadBlock = [self getBlockWith:requestId]; break; }if (didReadBlock) { didReadBlock(error, reslutData); }if (requestId != nil && requestId.length > 0) { [self removeRequestIDFormMap:requestId]; }

}
执行后记得清理相关内容和并发锁等信息。到此,主要功能已经完成,效果如下:

[[SendCore sharedManager] sendEnterChatRoom:room completion:^(NSError * _Nullable error, id _Nullable data) {
NSLog(@"%@", error); if (error == nil) { NSLog(@"进入聊天室成功"); }

}];
这样长链接的业务代码就和短链接的业务代码一样的简洁方便。### 超时还有一些意外的情况需要处理,上面的例子只是在网络正常的情况下可以工作,比如超时,`TCP` 并不会有消息回来,所以在 SendCore 中还要在做多一些事情。所以我们走入上面 addRequestIDToMap 函数:

  • (void) addRequestIDToMap:(NSString *) requestID block:(nullable SocketDidReadBlock)callback withTime:(BOOL) timeout{
    if (requestID == nil ||
    requestID.length == 0) { return;

    }
    if (callback == nil) {
    return;

    }
    [self.requestsMapLock lock];
    [self.requestsMap setObject:callback forKey:requestID];
    if (timeout) {
    [self.requestsTimeMap setObject:[NSDate date] forKey:requestID];

    }
    【被催稿了,所以聊聊|被催稿了,所以聊聊 长链接在移动端开发中如何做到和短链接一样高效】[self.requestsMapLock unlock];
    }
    其中有一个 requestsTimeMap,记录了 request_id 请求的发起时间,有了这个时间,就可以在本地做好超时错误的处理

    -(void) checkRequestProcessTimeout
    {
    NSError * Socket_WAIT_PROCESS_TIMEOUT_SECOND = [NSError errorWithDomain:@"_Socket_WAIT_PROCESS_TIMEOUT_SECOND_" code:408 userInfo:nil];
    NSMutableArray * timeoutRequestIDs = [NSMutableArray array];
    NSDate * now = [NSDate date];
    for (NSString * requestID in [self.requestsTimeMap allKeys]) {
    if (requestID == nil || [requestID length] <= 0) { continue; }NSDate * fireDate = [self.requestsTimeMap objectForKey:requestID]; NSDate * timeOutTime = [NSDate dateWithTimeInterval:_Socket_WAIT_PROCESS_TIMEOUT_SECOND_ sinceDate:fireDate]; if ([timeOutTime compare:now] == NSOrderedAscending) { [timeoutRequestIDs addObject:requestID]; }

    }
    for (NSString * requestID in timeoutRequestIDs) {
    if (requestID == nil || [requestID length] <= 0) { continue; }SocketDidReadBlock didReadBlock = [self getBlockWith:requestID]; didReadBlock(Socket_WAIT_PROCESS_TIMEOUT_SECOND, nil); [self removeRequestIDFormMap:requestID];

    }
    }
    通过定时器,每秒执行 checkRequestProcessTimeout 函数,发现超时的请求就直接调整其 block,并返回 Socket_WAIT_PROCESS_TIMEOUT_SECOND 错误。通过双 table 的形式就很好地解决了这个问题。### 缓存同理缓存也就比较容易实现了,参考 AFNetworking 库就可以实现一套好用的缓存功能。### request_id 的小妙用request_id 由客户端产生并维护,一般这样产生就可以:

  • (NSString *)createRequestID {
    NSInteger timeInterval = [NSDate date].timeIntervalSince1970 * 1000000;
    NSString *randomRequestID = [NSString stringWithFormat:@"%ld%d", timeInterval, arc4random() % 100000];
    return randomRequestID;
    }
    某种场景中存在一些固定的网络请求,或者为了配合缓存库,可以定义一些固定的或者特定格式的 id。

    const USER_INFO_GET_REQUEST_ID = @“04e1c446-b918-40f7-9061-d06b569a9cf0”
    @“/大功能/小功能/xxx”
request_id 的巧妙定义,会带来意想不到的效果,同学们可以自由发挥。## 结束到此,所有的功能就结束了,从业务代码上完全看不出这段代码是长链接还是短链接,很好地达到了预期的目的,在长链接的业务中业务代码效率和短链接一样简洁高效。

    推荐阅读