注册

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进行传输数据的流程可能像这样:


098c512dc5032e0353295a471eee493d.png


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


52487bc5cdc0ef6596dedde8277ce31d.png


或者这样:


928a39029038dbb8449b34b9176c7745.png


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

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

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

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


7a04d5ab3aa40ffa30dfcbf0bf89ce1e.png


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

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

7d665f9009b3fdccf98e51e4267f31ee.png

因为客户端和服务端都可以发送和接收数据, 为了方便描述, 这里我们对客户端发出的报文统一称为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;
});
}



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





0 个评论

要回复文章请先登录注册