iOS-TCP网络框架

TCP概述

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC793定义. 在因特网协议族中,TCP属于传输层, 位于网络层之上,应用层之下.

需要注意的是, TCP只是协议声明, 仅对外声明协议提供的功能, 但本身并不进行任何实现. 因此, 在介绍通信协议时, 通常我们还会提及另一个术语: Socket.
Socket并不是一种协议, 而是一组接口(即API). 协议的实现方通过Socket对外提供具体的功能调用. TCP协议的实现方提供的接口就是TCPSocket, UDP协议的实现方提供的接口就是UDPSocket...

通常, 协议的使用方并不直接面对协议的实现方, 而是通过对应的Socket使用协议提供的功能. 因此, 即使以后协议的底层实现进行了任何改动, 但由于对外的接口Socket不变, 使用方也不需要做出任何变更.

TCP协议基于IP协议, 而IP协议属于不可靠协议, 要在一个不可靠协议的的基础上实现一个可靠的数据传输协议是困难且复杂的, TCP的定义者也并不指望所有程序员都能自行实现一遍TCP协议. 所以, 与其说本文是在介绍TCP编程, 倒不如说是介绍TCPSocket编程.

建立通讯连接

通过Socket建立TCP连接是非常简单的, 连接方(客户端)只需要提供被连接方(服务端)的IP地址和端口号去调用连接接口即可, 被连接方接受连接的话, 接口会返回成功, 否则返回失败, 至于底层的握手细节, 双方完全不用关心. 但考虑到网络波动, 前后台切换, 服务器重启等等可能导致的连接主动/被动断开的情况, 客户端这边我会加上必要的重连处理. 主要代码如下:

//HHTCPSocket.h

@class HHTCPSocket;
@protocol HHTCPSocketDelegate <NSObject>

@optional
- (void)socket:(HHTCPSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; //连接成功

- (void)socketCanNotConnectToService:(HHTCPSocket *)sock; //重连失败
- (void)socketDidDisconnect:(HHTCPSocket *)sock error:(NSError *)error; //连接失败并开始重连

@end

@interface HHTCPSocket : NSObject

@property (nonatomic, weak) id<HHTCPSocketDelegate> delegate;
@property (nonatomic, assign) NSUInteger maxRetryTime; //最大重连次数

- (instancetype)initWithService:(HHTCPSocketService *)service; //service提供ip地址和端口号

- (void)close;
- (void)connect; //连接
- (void)reconnect; //重连
- (BOOL)isConnected;

@end

//HHTCPSocket.m

@implementation HHTCPSocket

- (instancetype)initWithService:(HHTCPSocketService *)service {
    if (self = [super init]) {
        self.service = service ?: [HHTCPSocketService defaultService];

        //1\. 初始化Socket
        const char *delegateQueueLabel = [[NSString stringWithFormat:@"%p_socketDelegateQueue", self] cStringUsingEncoding:NSUTF8StringEncoding];
        self.reconnectTime = self.maxRetryTime;
        self.delegateQueue = dispatch_queue_create(delegateQueueLabel, DISPATCH_QUEUE_SERIAL);
        self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.delegateQueue];

        //2\. 初始化Socket连接线程
        self.machPort = [NSMachPort port];
        self.keepRuning = YES;
        self.socket.IPv4PreferredOverIPv6 = NO; //支持ipv6
        [NSThread detachNewThreadSelector:@selector(configSocketThread) toTarget:self withObject:nil];

        //3\. 处理网络波动/前后台切换
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedNetworkChangedNotification:) name:kRealReachabilityChangedNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceivedAppBecomeActiveNotification:) name:UIApplicationDidBecomeActiveNotification object:nil];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark - Interface

- (void)connect {
    if (self.isConnecting || !self.isNetworkReachable) { return; }
    self.isConnecting = YES;

    [self disconnect];

    //去Socket连接线程进行连接 避免阻塞UI
    BOOL isFirstTimeConnect = (self.reconnectTime == self.maxRetryTime);
    int64_t delayTime = isFirstTimeConnect ? 0 : (arc4random() % 3) + 1;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_global_queue(2, 0), ^{
        [self performSelector:@selector(connectOnSocketThread) onThread:self.socketThread withObject:nil waitUntilDone:YES];
    });
}

- (void)reconnect {

    self.reconnectTime = self.maxRetryTime;
    [self connect];
}

- (void)disconnect {
    if (!self.socket.isConnected) { return; }

    [self.socket setDelegate:nil delegateQueue:nil];
    [self.socket disconnect];
}

- (BOOL)isConnected {
    return self.socket.isConnected;
}

#pragma mark - GCDAsyncSocketDelegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    //连接成功 通知代理方
    if ([self.delegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) {
        [self.delegate socket:self didConnectToHost:host port:port];
    }

    self.reconnectTime = self.maxRetryTime;
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {

    if ([self.delegate respondsToSelector:@selector(socketDidDisconnect:error:)]) {
        [self.delegate socketDidDisconnect:self error:error];
    }
    [self tryToReconnect];//连接失败 尝试重连
}

#pragma mark - Action

- (void)configSocketThread {

    if (self.socketThread == nil) {
        self.socketThread = [NSThread currentThread];
        [[NSRunLoop currentRunLoop] addPort:self.machPort forMode:NSDefaultRunLoopMode];
    }
    while (self.keepRuning) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    [[NSRunLoop currentRunLoop] removePort:self.machPort forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]];
    [self.socketThread cancel];
    self.socket = nil;
    self.machPort = nil;
    self.socketThread = nil;
    self.delegateQueue = nil;
}

- (void)connectOnSocketThread {//实际的调用连接操作在这里

    [self.socket setDelegate:self delegateQueue:self.delegateQueue];
    [self.socket connectToHost:self.service.host onPort:self.service.port error:nil];
    self.isConnecting = NO;
}

#pragma mark - Notification

- (void)didReceivedNetworkChangedNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

- (void)didReceivedAppBecomeActiveNotification:(NSNotification *)notif {
    [self reconnectIfNeed];
}

#pragma mark - Utils

- (void)tryToReconnect {
    if (self.isConnecting || !self.isNetworkReachable) { return; }

    self.reconnectTime -= 1;
    if (self.reconnectTime >= 0) {
        [self connect];
    } else if ([self.delegate respondsToSelector:@selector(socketCanNotConnectToService:)]) {
        [self.delegate socketCanNotConnectToService:self];
    }
}

- (NSUInteger)maxRetryTime {
    return _maxRetryTime > 0 ? _maxRetryTime : 5;
}

@end

这边因为需要添加重连操作, 所以我在GCDAsyncSocket的基础上又封装了一下, 但总体代码不多, 应该比较好理解. 这里需要注意的是GCDAsyncSocket的连接接口(connectToHost: onPort: error:)是同步调用的, 慢网情况下可能会阻塞线程一段时间, 所以这里我单开了一个线程来做连接操作.

连接建立以后, 就可以读写数据了, 写数据的接口如下:

- (void)writeData:(NSData *)data {
    if (!self.isConnected || data.length == 0) { return; }

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

至于读数据, 这里我们并不走接口, 而是通过回调方法将读到的数据以参数的形式将数据给到调用方. 这是因为连接的另一端时时刻刻都有可能发送数据过来, 所以通常在连接建立后接收方都会进入一个死循环反复读取数据, 处理数据, 读取数据... 伪代码大概像这样:

 //连接成功...
 while (1) {

        Error *error;
        Data *readData = [socket readToLength:1024 error:&error];//同步 读不到数据就阻塞
        if (error) { return; }

        [self handleData:readData];//同步异步皆可 多为异步
}

具体到我们的代码中, 则是这个样子:

// HHTCPSocket.h

@protocol HHTCPSocketDelegate <NSObject>
//...其他回调方法
- (void)socket:(HHTCPSocket *)sock didReadData:(NSData *)data;//读取到数据回调方法
@end

// HHTCPSocket.m

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    //Socket连接成功 开始读数据
    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    //Socket写数据成功 继续读取数据
    [self.socket readDataWithTimeout:-1 tag:socketTag];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

    //从Socket中读到数据 交由调用方处理
    if ([self.delegate respondsToSelector:@selector(socket:didReadData:)]) {
        [self.delegate socket:self didReadData:data];
    }
    [self.socket readDataWithTimeout:-1 tag:socketTag];//继续读取数据
}

现在我们已经可以通过Socket建立一条会自动重连的TCP连接, 然后还可以通过Socket从连接中读写数据, 接下来要做的就是定义一套自己的通讯协议了.

定义通讯协议
  • 为什么需要定义通讯协议

TCP协议定义了连接双方以字节流而不是报文段的方式进行数据传输, 这意味着任何应用层报文(image/text/html...)想要通过TCP进行传输都必须先转化成二进制数据. 另外, TCP实现出于传输效率考虑, 往往会在连接两端各自开辟一个发送数据缓冲区和一个接收数据缓冲区. 因此, 有时应用层通过Socket向连接中写入数据时, 数据其实并没有立即被发送, 而是被放入缓冲区等待合适的时机才会真正的发送. 理想情况下, TCP进行传输数据的流程可能像这样:

但实际情况中, 因为Nagle算法/网络拥堵/拥塞控制/接收方读取太慢等等各种原因, 数据很有可能会在发送缓冲区/接收缓冲区被累积. 所以, 上面的流程更可能是这样:

或者这样:

上面的图都假设应用层报文不到一个MSS(一个MSS一般为1460字节, 这对大部分非文件请求来说都足够了), 当报文超过一个MSS时, TCP底层实现会对报文进行拆分后多次传输, 这会稍微复杂些(不想画图了), 但最后导致的问题是一致的, 解决方案也是一致的.

从上面的图容易看出, 无论数据在发送缓冲区还是接收缓冲区被累积, 对于接收方程序来说都是一样的: 多个应用层报文不分彼此粘作一串导致数据无法还原(粘包).

得益于TCP协议是可靠的传输协议(可靠意味着TCP实现会保证数据不会丢包, 也不会乱序), 粘包的问题很好处理. 我们只需要在发送方给每段数据都附上一份描述信息(描述信息主要包括数据的长度, 解析格式等等), 接收方就可以根据描述信息从一串数据流中分割出单独的每段应用层报文了.

被传输数据和数据的描述一起构成了一段应用层报文, 这里我们称实际想传输的数据为报文有效载荷, 而数据的描述信息为报文头部. 此时, 数据的传输流程就成了这样:

  • 定义一个简单的通讯协议

自定义通讯协议时, 往往和项目业务直接挂钩, 所以这块其实没什么好写的. 但为了继续接下来的讨论, 这里我会给到一个非常简单的Demo版协议, 它长这样:

因为客户端和服务端都可以发送和接收数据, 为了方便描述, 这里我们对客户端发出的报文统一称为Request, 服务端发出的报文统一称为Response.

这里需要注意的是, 这里的Request和Response并不总是一一对应, 比如客户端单向的心跳请求报文服务端是不会响应的, 而服务端主动发出的推送报文也不是客户端请求的.

Request由4个部分组成:

  1. url: 类似HTTP中的统一资源定位符, 32位无符号整数(4个字节). 用于标识客户端请求的服务端资源或对资源进行的操作. 由服务端定义, 客户端使用.

  2. content(可选): 请求携带的数据, 0~N字节的二进制数据. 用于携带请求传输的内容, 传输的内容目前是请求参数, 也可能什么都没有. 解析格式固定为JSON.

  3. serNum: 请求序列号, 32位无符号整数(4个字节). 用于标示请求本身, 每个请求对应一个唯一的序列号, 即使两个请求的url和content都相同. 由客户端生成并传输, 服务端解析并回传. 客户端通过回传的序列号和请求序列号之间的对应关系进行响应数据分发.

  4. contentLen: 请求携带数据长度, 32位无符号整数(4个字节). 用于标示请求携带的数据的长度. 服务端通过contentLen将粘包的数据进行切割后一一解析并处理.

Response由5个部分组成:

  1. url: 同Request.

  2. respCode: 类似HTTP状态码, 32位无符号整数(4个字节).

  3. content(可选): 响应携带的数据, 0~N字节的二进制数据. 携带的数据可能是某个Request的响应数据, 也可能是服务端主动发出的推送数据, 或者, 什么都没有. 解析格式固定为JSON.

  4. serNum: 该Response所对应的Request序列号, 32位无符号整数(4个字节). 若Response并没有对应的Request(比如推送), Response.serNum==Response.url.

  5. contentLen: Response携带的数据长度, 32位无符号整数(4个字节). 用于标示Response携带的数据的长度. 客户端通过contentLen将粘包的数据进行切割后一一解析并处理.

因为只是Demo用, 这个协议会比较随意. 但在实际开发中, 我们应该尽量参考那些成熟的应用层协议(HTTP/FTP...). 比如考虑到后续的业务变更, 应该加上Version字段. 加上ContentType字段以传输其他类型的数据, 压缩字段字节数以节省流量...等等.

实现通讯协议

有了协议以后, 就可以写代码进行实现了. Request部分主要代码如下:

//HHTCPSocketRequest.h

/** URL类型肯定都是后台定义的 直接copy过来即可 命名用后台的 方便调试时比对 */
typedef enum : NSUInteger {
    TCP_heatbeat = 0x00000001,
    TCP_notification_xxx = 0x00000002,
    TCP_notification_yyy = 0x00000003,
    TCP_notification_zzz = 0x00000004,

    /* ========== */
    TCP_max_notification = 0x00000400,
    /* ========== */

    TCP_login = 0x00000401,
    TCP_weibo_list_public = 0x00000402,
    TCP_weibo_list_followed = 0x00000403,
    TCP_weibo_like = 0x00000404
} HHTCPSocketRequestURL;

+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header;

//HHTCPSocketRequest.m

+ (instancetype)requestWithURL:(HHTCPSocketRequestURL)url parameters:(NSDictionary *)parameters header:(NSDictionary *)header {

    NSData *content = [parameters yy_modelToJSONData];
    uint32_t requestIdentifier = [self currentRequestIdentifier];

    HHTCPSocketRequest *request = [HHTCPSocketRequest new];
    request.requestIdentifier = @(requestIdentifier);
    [request.formattedData appendData:[HHDataFormatter msgTypeDataFromInteger:url]];/** 请求URL */
    [request.formattedData appendData:[HHDataFormatter msgSerialNumberDataFromInteger:requestIdentifier]];/** 请求序列号 */
    [request.formattedData appendData:[HHDataFormatter msgContentLengthDataFromInteger:(uint32_t)content.length]];/** 请求内容长度 */

    if (content != nil) { [request.formattedData appendData:content]; }/** 请求内容 */
    return request;
}

+ (uint32_t)currentRequestIdentifier {

    static uint32_t currentRequestIdentifier;
    static dispatch_semaphore_t lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        currentRequestIdentifier = TCP_max_notification;
        lock = dispatch_semaphore_create(1);
    });

    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    if (currentRequestIdentifier + 1 == 0xffffffff) {
        currentRequestIdentifier = TCP_max_notification;
    }
    currentRequestIdentifier += 1;
    dispatch_semaphore_signal(lock);

    return currentRequestIdentifier;
}

HHTCPSocketRequest主要做两件事: 1.为每个Request生成唯一序列号; 2. 根据协议定义将应用层数据转化为相应的二进制数据.

应用层数据和二进制数据间的转化由HHDataFormatter完成, 它负责统一数据格式化接口和大小端问题 (关于大小端).

接下来是Response部分的代码:

//HHTCPSocketResponse.h

@interface HHTCPSocketResponse : NSObject

+ (instancetype)responseWithData:(NSData *)data;

- (HHTCPSocketRequestURL)url;

- (NSData *)content;
- (uint32_t)serNum;
- (uint32_t)statusCode;
@end

//HHTCPSocketResponse.m

+ (instancetype)responseWithData:(NSData *)data {
    if (data.length < [HHTCPSocketResponseParser responseHeaderLength]) {
        return nil;
    }

    HHTCPSocketResponse *response = [HHTCPSocketResponse new];
    response.data = data;
    return response;
}

- (HHTCPSocketRequestURL)url {
    if (_url == 0) {
        _url = [HHTCPSocketResponseParser responseURLFromData:self.data];
    }
    return _url;
}

- (uint32_t)serNum {
    if (_serNum == 0) {
        _serNum = [HHTCPSocketResponseParser responseSerialNumberFromData:self.data];
    }
    return _serNum;
}

- (uint32_t)statusCode {
    if (_statusCode == 0) {
        _statusCode = [HHTCPSocketResponseParser responseCodeFromData:self.data];
    }
    return _statusCode;
}

- (NSData *)content {
    return [HHTCPSocketResponseParser responseContentFromData:self.data];
}

@end

HHTCPSocketResponse比较简单, 它只做一件事: 根据协议定义将服务端返回的二进制数据解析为应用层数据.

最后, 为了方便管理, 我们再抽象出一个Task. Task将负责请求状态, 请求超时, 请求回调等等的管理. 这部分和协议无关, 但很有必要.
Task部分的代码如下:

//HHTCPSocketTask.h

typedef enum : NSUInteger {
    HHTCPSocketTaskStateSuspended = 0,
    HHTCPSocketTaskStateRunning = 1,
    HHTCPSocketTaskStateCanceled = 2,
    HHTCPSocketTaskStateCompleted = 3
} HHTCPSocketTaskState;

@interface HHTCPSocketTask : NSObject

- (void)cancel;
- (void)resume;

- (HHTCPSocketTaskState)state;
- (NSNumber *)taskIdentifier;

@end

//HHTCPSocketTask.m

//保存Request和completionHandler Request用于将调用方数据写入Socket completionHandler用于将Response交付给调用方
+ (instancetype)taskWithRequest:(HHTCPSocketRequest *)request completionHandler:(HHNetworkTaskCompletionHander)completionHandler {

    HHTCPSocketTask *task = [HHTCPSocketTask new];
    task.request = request;
    task.completionHandler = completionHandler;
    task.state = HHTCPSocketTaskStateSuspended;
    ...其他 略
    return task;
}

//处理服务端返回的Response Socket读取到相应的Response报文数据后会调用此接口
- (void)completeWithResponse:(HHTCPSocketResponse *)response error:(NSError *)error {
    if (![self canResponse]) { return; }

    NSDictionary *result;
    if (error == nil) {

        if (response == nil) {
            error = [self taskErrorWithResponeCode:HHTCPSocketResponseCodeUnkonwn];
        } else {

            error = [self taskErrorWithResponeCode:response.statusCode];
            result = [NSJSONSerialization JSONObjectWithData:response.content options:0 error:nil];
        }
    }

    [self completeWithResult:result error:error];
}

//将处理后的数据交付给调用方
- (void)completeWithResult:(id)result error:(NSError *)error {

    ...其他 略
    dispatch_async(dispatch_get_main_queue(), ^{

        !self.completionHandler ?: self.completionHandler(error, result);
        self.completionHandler = nil;
    });
}

现在我们已经有了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请求而言, 我们需要的可能就是直接断开连接了, 这部分Demo中并未进行实现, 我还没遇到类似的需求, 也没想好该不该这样做.

心跳

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

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

当发送方为客户端时, 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];//回复服务端心跳请求 如果有必要的话
    } 
}

HHTCPSocketHeartbeat由TCPClient调用, 做的事情很简单: 1)连接成功时启动心跳; 2)收到服务端数据时重置心跳; 3)收到服务端Ping时进行回复; 4)心跳超时断开重连 5)连接断开时停止心跳;

文件下载/上传?

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

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容