TCP的初识
TCP 是一种面向连接的,可靠的,基于字节流的传输层通信协议.TCP工作在网络OSI七层模型中的第四层-传输层,下面一张图展示OSI七层模型及每一层的作用和对应的协议.
TCP是传输层协议,在进行数据传输之前使用三次握手协议建立连接,大体的过程是客户端发出SYN连接请求后,服务端接收请求后应答SYN+ACK,客户端收到服务端应答后应答ACK,这种建立连接的方法可以防止产生错误的连接,防止已失效的连接请求报文段突然又传送到了服务端。TCP三次握手过程图示如下:
TCP三次握手过程描述如下:
1.客户端发送SYN标志位为1,Sequence Number为x的连接请求报文段,然后客户端进入SYN_SEND状态,等待服务器的确认响应;
2.服务器收到客户端的连接请求,对这个SYN报文段进行确认,然后发送Acknowledgment Number为x+1(Sequence Number+1),SYN标志位和ACK标志位均为1,Sequence Number为y的报文段(即SYN+ACK报文段)给客户端,此时服务器进入SYN_RECV状态;
3.客户端收到服务器的SYN+ACK报文段,确认ACK后,发送Acknowledgment Number为y+1,SYN标志位为0,ACK标志位为1的报文段,发送完成后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手,客户端和服务器端成功地建立连接,可以开始传输数据了。
当数据传送完成后,为了正确完整的完成数据传输,需要经过四次挥手断开连接。TCP四次挥过程图示如下:
TCP四次挥手过程描述如下:
1.客户端发送Sequence Number为x+2,Acknowledgment Number为y+1的FIN报文段,客户端进入FIN_WAIT_1状态,即告诉服务端没有数据需要传输了,请求关闭连接;
2.服务端收到客户端的FIN报文段后,向客户端应答一个Acknowledgment Number为Sequence Number+1的ACK报文段,即应答客户端你的请求我收到了,但是我还没准备好,请等待我的关闭请求。客户端收到后进入FIN_WAIT_2状态;
3.服务端完成数据传输后向客户端发送Sequence Number为y+1的FIN报文段,请求关闭连接,服务器进入LAST_ACK状态;
4.客户端收到服务端的FIN报文段后,向服务端应答一个Acknowledgment Number为Sequence Number+1的ACK报文段,然后客户端进入TIME_WAIT状态;服务端收到客户端的ACK报文段后关闭连接进入CLOSED状态,客户端等待2MSL后依然没有收到回复,则证明服务端已正常关闭,客户端此时关闭连接进入CLOSED状态。
TCP的使用
上面的那些都是理论的知识,在我们实际应用中不必过分钻研(当然除了你本来就是研究这个的或者你很感兴趣),我们要做的,要学习的就是怎么在项目中使用它,下面我就先讲一下我在项目中的使用以及遇到的问题.
* 我们的需求:在我们的项目中有一个微课模块,我们的需求就是要做到当老师或者管理员进入微课的时候能够通知到所有人,针对这个问题,我跟总监经过讨论,决定使用TCP.(至于为什么不走IM自定义消息就不在累述)
* 我们的实现:我们使用Socket来完成的TCP链接 ,服务端是用MINA2搭建,IOS 使用CocoaAsyncSocket,安卓也是用的MINA2
其实在这里有些人还搞不清楚什么的TCP 什么是UDP 什么是HTTP 什么是Socket,那我就大概说下我的理解:
# socket是对TCP/IP协议的封装和应用(程序员层面上)。也可以说,TPC/IP协议是传输层协议,主要解决数据 如何在网络中传输,HTTP是应用层协议,主要解决如何包装数据。socket是让我们更简单的使用TCP/IP协议
我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如 果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也 可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。 实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现 只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、 listen、connect、accept、send、read和write等等。
在这里我就着重讲下IOS端的使用和问题
使用到的是CocoaAsyncSocket 中的GCDAsyncSocket (当然CocoaAsyncSocket里也有创建UDP的就不累述)
- 创建链接 以及对应的回调
//建立链接
TcpClient *tcp = [TcpClient sharedInstance];
[tcp setDelegate_ITcpClient:self];
if(tcp.asyncSocket.isConnected)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"网络已经连接好啦!" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
[alert show];
}else
{
[tcp openTcpConnection:HOST port:[PORT intValue]];
}
这里的TcpClient 是拥有GCDAsyncSocket属性的单例 从中可以看到连接的时候只是需要HOST 和 port 就是地址和端口
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
DLog(@"链接成功啦socket:%p didConnectToHost:%@ port:%hu", sock, host, port);
[[NSNotificationCenter defaultCenter] postNotificationName:@"didConnectToHost" object:nil userInfo:nil];
if ([itcpClient respondsToSelector:@selector(didConnectToHost)]) {
[itcpClient didConnectToHost];
}
[self read];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
if (err) {
DLog(@"连接失败");
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnConnectionError:)]) {
[itcpClient OnConnectionError:err];
}
});
}else{
DLog(@"正常断开");
}
}
-
发送消息
// 进入微课 NSDictionary *params = @{@"requestCode":@"10001",@"token":[LoginDataHelper shareInstance].userInfo.token,@"cId":self.model.cId}; NSString *json = [params JSONString]; NSString *strn = [NSString stringWithFormat:@"%@\n",json]; [tcp writeString:strn]; // TcpClient 中的方法 -(void)writeString:(NSString*)datastr; { NSString *requestStr = [NSString stringWithFormat:@"%@",datastr]; NSData *requestData = [requestStr dataUsingEncoding:NSUTF8StringEncoding]; [self writeData:requestData]; } -(void)writeData:(NSData*)data; { TAG_SEND++; [asyncSocket writeData:data withTimeout:-1. tag:TAG_SEND]; }
当然发送消息也有对应的 回调
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
DLog(@"发送消息socket:%p didWriteDataWithTag:%ld", sock, tag);
[[NSNotificationCenter defaultCenter] postNotificationName:@"didWriteDataWithTag" object:nil userInfo:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnSendDataSuccess:)]) {
[itcpClient OnSendDataSuccess:[NSString stringWithFormat:@"tag:%li",tag]];
}
});
} 收到服务器的消息
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
DLog(@"收到消息啦socket:%p didReadData:withTag:%ld", sock, tag);
NSString *httpResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
TAG_RECIVED = tag;
NSDictionary *dic = [NSDictionary dictionaryWithJsonString:httpResponse];
if (dic) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"didReadData" object:nil userInfo:dic];
if(![httpResponse isEqualToString:@""])
[recivedArray addObject:httpResponse];
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnReciveData:)]) {
[itcpClient OnReciveData:dic];
}
});
}
[self read];
}
当然这里你们发送的消息和接收的消息,前后端要先针对其格式做好对接,定好格式,按照这个格式去发送和解析
-
关于保活问题
TCP长时间处于非活动状态可能会被杀死,所以做好保活是很有必要的
这里我做的处理是创建心跳机制 发送心跳包//心跳 { _heartTime = [NSTimer timerWithTimeInterval:50 target:self selector:@selector(reconnectTP) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.heartTime forMode:NSDefaultRunLoopMode]; [_heartTime fire]; } - (void)reconnectTP{ TcpClient *tcp = [TcpClient sharedInstance]; [tcp reconnect]; { TcpClient *tcp = [TcpClient sharedInstance]; if(tcp.asyncSocket.isDisconnected) { DLog(@"网络不通"); }else if(tcp.asyncSocket.isConnected) { NSDictionary *params = @{@"requestCode":@"10001",@"token":[LoginDataHelper shareInstance].userInfo.token,@"cId":self.posterModel.cId}; NSString *json = [params JSONString]; NSString *strn = [NSString stringWithFormat:@"%@\n",json]; [tcp writeString:strn]; }else{ DLog(@"TCP没有建立链接"); } } }
这里就是定时检测TCP是否在连线状态,如果不在就重连,如果在就发送心跳包给后台。从而保证TCP的活性
- 中间出现过的问题
开始我们的TCP一直都很正常,但是在服务器集群之后就出现问题了,IOS怎么也接收不到服务器发送的消息,链接很正常就是收不到消息,但是安卓却没有任何问题,当初这个问题困扰我们了很久,大家都把责任推到IOS 这边,当时我也是倍感压力,很不解,为啥之前就行,集群之后就出现问题了呢,后来经过我不断地努力和测试才发现问题是:
服务端在发送消息之后并没有用\r\n 或者\n 作为结束标志,这在之前是没问题的,但是集群之后在Ruby语言里面就出现问题,没有结束标志,IOS这边就一直收不到消息。因为他一直认为在传送数据没有结束。
# 所以一定要在发送消息之后以\r\n或者\n 作为结束符,避免不必要的麻烦。
目前只想起来这些,至于其他问题,可以留言给我,我们公共探讨,也可以加我的Q:719967870,下面我贴出 基于GCDAsyncSocket封装的单例大家可以直接使用
// TcpClient.h
// ConnectTest
//
// Created by yuchen on 2016.
#import <Foundation/Foundation.h>
#import "GCDAsyncSocket.h"
#import "ITcpClient.h"
@interface TcpClient : NSObject
{
long TAG_SEND;
long TAG_RECIVED;
id<ITcpClient> itcpClient;
NSMutableArray *recivedArray;
}
@property (nonatomic,retain) GCDAsyncSocket *asyncSocket;
+ (TcpClient *)sharedInstance;
-(void)setDelegate_ITcpClient:(id<ITcpClient>)_itcpClient;
// 链接
-(void)openTcpConnection:(NSString*)host port:(NSInteger)port;
-(void)reconnect ;
-(void)read;
//发消息
-(void)writeString:(NSString*)datastr;
-(void)writeData:(NSData*)data;
-(long)GetSendTag;
-(long)GetRecivedTag;
//断开
-(void)disconnect;
@end
//.m
// TcpClient.m
// ConnectTest
//
// Created by yuchen on 2016.
//
//
#import "TcpClient.h"
#import "GCDAsyncSocket.h"
#import "LZ_DevKit.h"
#import "NSDictionary+JSON.h"
@implementation TcpClient
@synthesize asyncSocket;
+ (TcpClient *)sharedInstance;
{
static TcpClient *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[TcpClient alloc] init];
});
return _sharedInstance;
}
-(id)init;
{
self = [super init];
recivedArray = [NSMutableArray arrayWithCapacity:10];
return self;
}
-(void)setDelegate_ITcpClient:(id<ITcpClient>)_itcpClient;
{
itcpClient = _itcpClient;
}
-(void)openTcpConnection:(NSString*)host port:(NSInteger)port;
{
// dispatch_queue_create("bin.queue", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t mainQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:mainQueue];
[asyncSocket setAutoDisconnectOnClosedReadStream:NO];
NSError *error = nil;
if (![asyncSocket connectToHost:host onPort:port error:&error])
{
DLog(@"Error connecting: %@", error);
}
}
-(void)disconnect{
itcpClient = nil;
[asyncSocket setDelegate:nil delegateQueue:NULL];
[asyncSocket disconnect];
}
// 重新连接
-(void)reconnect {
NSError* err;
if([asyncSocket isDisconnected]) {
BOOL result = [asyncSocket connectToHost:HOST onPort:[PORT integerValue] error:&err];
if(result)
{
DLog(@"重新连接--主机%@-Port%@",HOST,PORT);
}
else {
DLog(@"连接失败ERROR %@",[err description]);
}
}else{
DLog(@"已经连接");
}
}
-(void)writeString:(NSString*)datastr;
{
NSString *requestStr = [NSString stringWithFormat:@"%@",datastr];
NSData *requestData = [requestStr dataUsingEncoding:NSUTF8StringEncoding];
[self writeData:requestData];
}
-(void)writeData:(NSData*)data;
{
TAG_SEND++;
[asyncSocket writeData:data withTimeout:-1. tag:TAG_SEND];
}
-(void)read;
{
[asyncSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
-(long)GetSendTag;
{
return TAG_SEND;
}
-(long)GetRecivedTag;
{
return TAG_RECIVED;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Socket Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////😄哈哈
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
DLog(@"链接成功啦socket:%p didConnectToHost:%@ port:%hu", sock, host, port);
[[NSNotificationCenter defaultCenter] postNotificationName:@"didConnectToHost" object:nil userInfo:nil];
if ([itcpClient respondsToSelector:@selector(didConnectToHost)]) {
[itcpClient didConnectToHost];
}
[self read];
}
//是否加密
- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
DLog(@"socketDidSecure:%p", sock);
NSString *requestStr = [NSString stringWithFormat:@"GET / HTTP/1.1\r\nHost: %@\r\n\r\n", HOST];
NSData *requestData = [requestStr dataUsingEncoding:NSUTF8StringEncoding];
[sock writeData:requestData withTimeout:-1 tag:0];
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
DLog(@"发送消息socket:%p didWriteDataWithTag:%ld", sock, tag);
[[NSNotificationCenter defaultCenter] postNotificationName:@"didWriteDataWithTag" object:nil userInfo:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnSendDataSuccess:)]) {
[itcpClient OnSendDataSuccess:[NSString stringWithFormat:@"tag:%li",tag]];
}
});
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
DLog(@"收到消息啦socket:%p didReadData:withTag:%ld", sock, tag);
NSString *httpResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
TAG_RECIVED = tag;
NSDictionary *dic = [NSDictionary dictionaryWithJsonString:httpResponse];
if (dic) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"didReadData" object:nil userInfo:dic];
if(![httpResponse isEqualToString:@""])
[recivedArray addObject:httpResponse];
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnReciveData:)]) {
[itcpClient OnReciveData:dic];
}
});
}
[self read];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
if (err) {
DLog(@"连接失败");
dispatch_async(dispatch_get_main_queue(), ^{
if ([itcpClient respondsToSelector:@selector(OnConnectionError:)]) {
[itcpClient OnConnectionError:err];
}
});
}else{
DLog(@"正常断开");
}
}
- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock
{
}
@end
CocoaAsyncSocket :https://github.com/robbiehanson/CocoaAsyncSocket