源码

iOS-从零开始自建TCP通道

目录

  • 前言

  • TCP通道的建立

  • 自定义应用层协议

  • 请求体

  • 响应体

  • 请求和响应的序列化

    • 序列化器

    • 请求的序列化

    • 响应的序列化

  • 任务机制

    • KTTCPSocketTask

    • 任务超时

  • 管理器

    • KTTCPSocketManager

    • 请求的发送

    • 响应的接收

    • 将响应派发给对应任务

  • Demo

  • 参考资料


前言

本文的起因是希望像《美团点评移动网络优化实践》中的方案一样、建设一个可以将HTTP请求转化成二进制数据包、并且在自建的TCP长连接通道上传输。当然、直接TCP双向通讯也是没有问题的。

以前用的Websocket、简单粗暴。如果你只想要一个全双工的TCP长连接、Websocket作为和HTTP一样的应用层协议完全够用。

但本文主要是尝试自己用socket(虽然并不是完全原生)构建一个能够像HTTP请求一样使用的TCP通道。并且最终、将HTTP请求放在自建的TCP加密通道上传输。

关于网络层一些基础知识、或许《当被尬聊网络协议、我们可以侃点什么?》可以帮到你。

自己对Socket通道的建设一开始也不太懂、所以有很多地方借鉴了《一步一步构建你的iOS网络层 - TCP篇》的思路。十分感谢


TCP通道的建立

首先、我们需要一个类似websocket的应用层协议。
参照SRWebSocket来看、除了全双工通信之外。我们还需要处理心跳、重连、粘包这三个特殊的概念(SSL在CocoaAsyncSocket下已经封装了实现)。

此外。由于原生socket比较麻烦、所以借助了一个开源框架CocoaAsyncSocket来操作scoket(类似NSLayoutConstraint与Masonry的关系)。具体使用的是基于GCD的GCDAsyncSocket(似乎以前还有个基于Runloop的AsyncSocket、但是我用的时候已经没有了。大概和NSURLCollection被NSURLSession淘汰了一样)。

CocoaAsyncSocket初始状态下就具备连接、断开、发送以及读取等基本功能。
这里主要对CocoaAsyncSocket添加了重连、专属线程等易用性的封装、并且将scoket事件通过代理进行回调。

头文件

@class KTTCPSocket;
@protocol KTTCPSocketDelegate <NSObject>

@optional

/**
 链接成功

 @param sock KTTCPSocket
 @param host 主机
 @param port 端口
 */

- (void)socket:(KTTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;


/**
 最终链接失败
 连接失败 + N次重连失败

 @param sock KTTCPSocket
 */

- (void)socketCanNotConnectToService:(KTTCPSocket *)sock;


/**
 链接失败并重连

 @param sock KTTCPSocket
 @param error error
 */

- (void)socketDidDisconnect:(KTTCPSocket *)sock error:(NSError *)error;


/**
 接收到了数据

 @param sock KTTCPSocket
 @param data 二进制数据
 */

- (void)socket:(KTTCPSocket *)sock didReadData:(NSData *)data;

@end


/**
 对GCDAsyncSocket进行封装的工具类。
 具备自动重连、读写数据等基础操作
 */

@interface KTTCPSocket : NSObject

@property (nonatomic,readonlyNSString *host;//主机
@property (nonatomic,readonly) uint16_t port;//端口
@property (nonatomicNSUInteger maxRetryCount;//重连次数
@property (nonatomicweakid delegate;


- (instancetype)init NS_UNAVAILABLE;

/**
 构造方法

 @param host 主机号
 @param port 端口号
 @return KTTCPSocket实例
 */

- (instancetype)initSocketWithHost:(NSString *)host port:(uint16_t)port NS_DESIGNATED_INITIALIZER;


/**
    关闭连接--注意关闭之后就没办法再次开启了。不然没办法判断socke对象该何时销毁
 */

- (void)close;


/**
    连接
 */

- (void)connect;

/**
    重连并且重置次数
 */

- (void)reconnect;


/**
    链接状态

 @return 是否已经链接
 */

- (BOOL)isConnected;


/**
 写入数据

 @param data 二进制数据
 */

- (void)writeData:(NSData *)data;
@end

业务代码

  • 写入数据
- (void)writeData:(NSData *)data {
    if (data.length == 0) { return; }

    [self.socket writeData:data withTimeout:-1 tag:socketTag];
}

由于TCP面向字节流、所以并不需要我们调用发送之类的方法、他会按照顺序一个字节一个字节的把数据进行传输。

  • 读取数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

    if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
        [self.delegate socket:self didReadData:data];
    }

    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

readDataWithTimeout方法会持续监听一次缓存区、当接收到数据立刻通过代理交付。这里也就相当于递归调用了。

  • 重连
链接失败的重连:
//链接失败
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
//    NSLog(@"TCPSocket--连接已断开.error:%@", error);

   if ([self.delegate respondsToSelector:@selector(socketDidDisconnect:error:)]) {
       [self.delegate socketDidDisconnect:self error:error];
   }
   [self tryToReconnect];
}

//尝试自动重连
- (void)tryToReconnect {
   if (self.isConnecting || !self.isNetworkReachable) {
       return;
   }

   self.currentRetryCount -= 1;
   //如果还有尝试次数就自动重连
   if (self.currentRetryCount >= 0) {
       NSLog(@"尝试重连");
       [self connect];
   } else if ([self.delegate respondsToSelector:@selector(socketCanNotConnectToService:)]) {
       //自动重连失败
       NSLog(@"重连失败");
       [self.delegate socketCanNotConnectToService:self];
   }
}

连接失败会监听重连次数、超过次数则宣告失败

网络波动的重连:
//网络波动
- (void)didReceivedNetworkChangedNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

//切换到后台
- (void)didReceivedAppBecomeActiveNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

- (void)reconnectIfNeed {

    if (self.isConnecting || self.isConnected) { return; }

    [self reconnect];
}

网络波动会重置连接次数并重连

  • 线程的常驻
- (void)socketWillBeConnect {
    if (self.socketThread == nil) {
        //保存异步线程
        self.socketThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] addPort:self.machPort forMode:NSDefaultRunLoopMode];
        while (self.machPort) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    }
}

由于为长连接新开辟了一个线程、所以需要使用Runloop来维持线程的生存。


自定义通讯协议报文

这里需要解释一下TCP的两个概念

面向字节流传输

TCP协议将数据看做有序排列的二进制位、并按照8位分割成有序的字节流。

(1)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/ios-conglingkaishizijiantcptongdao-2.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:8 月 27, 2018 at 07:54 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复