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

2022 年什么会火?什么该学?本文正在参与“聊聊 2022 技能趋势”征文活动

近来,被波哥催稿,营业时刻被迫提早,就翻了数十年来翻深不见的代码库房,找到了多年前在移动端做长链接开发中的一种实践办法,能够让长链接的事务代码和短链接相同高效、简练的小技巧,就拿来敷衍交个差。

此文并不会针对 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 是一个较为「骚」的操作,引一个字段完美处理大问题。

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

过程中牢记短链接的特点,有来有回,有来必有回(不许抬杠,没有网络也会有超时或许网络未链接等过错回来)。

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 == 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 的奇妙界说,会带来意想不到的作用,同学们能够自由发挥。

完毕

到此,一切的功用就完毕了,从事务代码上彻底看不出这段代码是长链接还是短链接,很好地达到了预期的目的,在长链接的事务中事务代码功率和短链接相同简练高效。

更多精彩请重视咱们的大众号「百瓶技能」,有不定期福利呦!