TCP的初識
TCP 是一種面向連接的,可靠的,基于字節(jié)流的傳輸層通信協(xié)議.TCP工作在網(wǎng)絡(luò)OSI七層模型中的第四層-傳輸層,下面一張圖展示OSI七層模型及每一層的作用和對應(yīng)的協(xié)議.
TCP是傳輸層協(xié)議衅谷,在進(jìn)行數(shù)據(jù)傳輸之前使用三次握手協(xié)議建立連接群嗤,大體的過程是客戶端發(fā)出SYN連接請求后,服務(wù)端接收請求后應(yīng)答SYN+ACK譬重,客戶端收到服務(wù)端應(yīng)答后應(yīng)答ACK,這種建立連接的方法可以防止產(chǎn)生錯誤的連接罐氨,防止已失效的連接請求報文段突然又傳送到了服務(wù)端臀规。TCP三次握手過程圖示如下:
TCP三次握手過程描述如下:
1.客戶端發(fā)送SYN標(biāo)志位為1,Sequence Number為x的連接請求報文段栅隐,然后客戶端進(jìn)入SYN_SEND狀態(tài)塔嬉,等待服務(wù)器的確認(rèn)響應(yīng)玩徊;
2.服務(wù)器收到客戶端的連接請求,對這個SYN報文段進(jìn)行確認(rèn)谨究,然后發(fā)送Acknowledgment Number為x+1(Sequence Number+1)恩袱,SYN標(biāo)志位和ACK標(biāo)志位均為1,Sequence Number為y的報文段(即SYN+ACK報文段)給客戶端胶哲,此時服務(wù)器進(jìn)入SYN_RECV狀態(tài)畔塔;
3.客戶端收到服務(wù)器的SYN+ACK報文段,確認(rèn)ACK后鸯屿,發(fā)送Acknowledgment Number為y+1澈吨,SYN標(biāo)志位為0,ACK標(biāo)志位為1的報文段寄摆,發(fā)送完成后谅辣,客戶端和服務(wù)器端都進(jìn)入ESTABLISHED狀態(tài),完成TCP三次握手婶恼,客戶端和服務(wù)器端成功地建立連接桑阶,可以開始傳輸數(shù)據(jù)了。
當(dāng)數(shù)據(jù)傳送完成后熙尉,為了正確完整的完成數(shù)據(jù)傳輸联逻,需要經(jīng)過四次揮手?jǐn)嚅_連接。TCP四次揮過程圖示如下:
TCP四次揮手過程描述如下:
1.客戶端發(fā)送Sequence Number為x+2检痰,Acknowledgment Number為y+1的FIN報文段包归,客戶端進(jìn)入FIN_WAIT_1狀態(tài),即告訴服務(wù)端沒有數(shù)據(jù)需要傳輸了铅歼,請求關(guān)閉連接公壤;
2.服務(wù)端收到客戶端的FIN報文段后,向客戶端應(yīng)答一個Acknowledgment Number為Sequence Number+1的ACK報文段椎椰,即應(yīng)答客戶端你的請求我收到了厦幅,但是我還沒準(zhǔn)備好,請等待我的關(guān)閉請求慨飘∑垦辏客戶端收到后進(jìn)入FIN_WAIT_2狀態(tài)衙四;
3.服務(wù)端完成數(shù)據(jù)傳輸后向客戶端發(fā)送Sequence Number為y+1的FIN報文段,請求關(guān)閉連接,服務(wù)器進(jìn)入LAST_ACK狀態(tài)钢悲;
4.客戶端收到服務(wù)端的FIN報文段后漠魏,向服務(wù)端應(yīng)答一個Acknowledgment Number為Sequence Number+1的ACK報文段澳厢,然后客戶端進(jìn)入TIME_WAIT狀態(tài)鸦采;服務(wù)端收到客戶端的ACK報文段后關(guān)閉連接進(jìn)入CLOSED狀態(tài),客戶端等待2MSL后依然沒有收到回復(fù)稽坤,則證明服務(wù)端已正常關(guān)閉丈甸,客戶端此時關(guān)閉連接進(jìn)入CLOSED狀態(tài)糯俗。
TCP的使用
上面的那些都是理論的知識,在我們實(shí)際應(yīng)用中不必過分鉆研(當(dāng)然除了你本來就是研究這個的或者你很感興趣),我們要做的,要學(xué)習(xí)的就是怎么在項(xiàng)目中使用它,下面我就先講一下我在項(xiàng)目中的使用以及遇到的問題.
* 我們的需求:在我們的項(xiàng)目中有一個微課模塊,我們的需求就是要做到當(dāng)老師或者管理員進(jìn)入微課的時候能夠通知到所有人,針對這個問題,我跟總監(jiān)經(jīng)過討論,決定使用TCP.(至于為什么不走IM自定義消息就不在累述)
* 我們的實(shí)現(xiàn):我們使用Socket來完成的TCP鏈接 ,服務(wù)端是用MINA2搭建,IOS 使用CocoaAsyncSocket,安卓也是用的MINA2
其實(shí)在這里有些人還搞不清楚什么的TCP 什么是UDP 什么是HTTP 什么是Socket,那我就大概說下我的理解:
# socket是對TCP/IP協(xié)議的封裝和應(yīng)用(程序員層面上)。也可以說睦擂,TPC/IP協(xié)議是傳輸層協(xié)議得湘,主要解決數(shù)據(jù) 如何在網(wǎng)絡(luò)中傳輸,HTTP是應(yīng)用層協(xié)議祈匙,主要解決如何包裝數(shù)據(jù)忽刽。socket是讓我們更簡單的使用TCP/IP協(xié)議
我們在傳輸數(shù)據(jù)時,可以只使用(傳輸層)TCP/IP協(xié)議夺欲,但是那樣的話跪帝,如 果沒有應(yīng)用層,便無法識別數(shù)據(jù)內(nèi)容些阅,如果想要使傳輸?shù)臄?shù)據(jù)有意義伞剑,則必須使用到應(yīng)用層協(xié)議,應(yīng)用層協(xié)議有很多市埋,比如HTTP黎泣、FTP、TELNET等缤谎,也 可以自己定義應(yīng)用層協(xié)議抒倚。WEB使用HTTP協(xié)議作應(yīng)用層協(xié)議,以封裝HTTP文本信息坷澡,然后使用TCP/IP做傳輸層協(xié)議將它發(fā)到網(wǎng)絡(luò)上托呕。實(shí)際上socket是對TCP/IP協(xié)議的封裝,Socket本身并不是協(xié)議频敛,而是一個調(diào)用接口(API)项郊,通過Socket,我們才能使用TCP/IP協(xié)議斟赚。 實(shí)際上着降,Socket跟TCP/IP協(xié)議沒有必然的聯(lián)系。Socket編程接口在設(shè)計的時候拗军,就希望也能適應(yīng)其他的網(wǎng)絡(luò)協(xié)議任洞。所以說,Socket的出現(xiàn) 只是使得程序員更方便地使用TCP/IP協(xié)議棧而已发侵,是對TCP/IP協(xié)議的抽象侈咕,從而形成了我們知道的一些最基本的函數(shù)接口,比如create器紧、 listen、connect楼眷、accept铲汪、send熊尉、read和write等等。
在這里我就著重講下IOS端的使用和問題
使用到的是CocoaAsyncSocket 中的GCDAsyncSocket (當(dāng)然CocoaAsyncSocket里也有創(chuàng)建UDP的就不累述)
- 創(chuàng)建鏈接 以及對應(yīng)的回調(diào)
//建立鏈接
TcpClient *tcp = [TcpClient sharedInstance];
[tcp setDelegate_ITcpClient:self];
if(tcp.asyncSocket.isConnected)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"網(wǎng)絡(luò)已經(jīng)連接好啦掌腰!" 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(@"正常斷開");
}
}
-
發(fā)送消息
// 進(jìn)入微課 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]; }
當(dāng)然發(fā)送消息也有對應(yīng)的 回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
DLog(@"發(fā)送消息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]];
}
});
} 收到服務(wù)器的消息
- (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];
}
當(dāng)然這里你們發(fā)送的消息和接收的消息狰住,前后端要先針對其格式做好對接,定好格式齿梁,按照這個格式去發(fā)送和解析
-
關(guān)于贝咧玻活問題
TCP長時間處于非活動狀態(tài)可能會被殺死,所以做好鄙自瘢活是很有必要的
這里我做的處理是創(chuàng)建心跳機(jī)制 發(fā)送心跳包//心跳 { _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(@"網(wǎng)絡(luò)不通"); }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是否在連線狀態(tài)创南,如果不在就重連,如果在就發(fā)送心跳包給后臺省核。從而保證TCP的活性
- 中間出現(xiàn)過的問題
開始我們的TCP一直都很正常稿辙,但是在服務(wù)器集群之后就出現(xiàn)問題了,IOS怎么也接收不到服務(wù)器發(fā)送的消息气忠,鏈接很正常就是收不到消息邻储,但是安卓卻沒有任何問題,當(dāng)初這個問題困擾我們了很久旧噪,大家都把責(zé)任推到IOS 這邊吨娜,當(dāng)時我也是倍感壓力,很不解淘钟,為啥之前就行宦赠,集群之后就出現(xiàn)問題了呢,后來經(jīng)過我不斷地努力和測試才發(fā)現(xiàn)問題是:
服務(wù)端在發(fā)送消息之后并沒有用\r\n 或者\(yùn)n 作為結(jié)束標(biāo)志日月,這在之前是沒問題的袱瓮,但是集群之后在Ruby語言里面就出現(xiàn)問題,沒有結(jié)束標(biāo)志爱咬,IOS這邊就一直收不到消息尺借。因?yàn)樗恢闭J(rèn)為在傳送數(shù)據(jù)沒有結(jié)束。
# 所以一定要在發(fā)送消息之后以\r\n或者\(yùn)n 作為結(jié)束符,避免不必要的麻煩精拟。
目前只想起來這些燎斩,至于其他問題,可以留言給我蜂绎,我們公共探討栅表,也可以加我的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;
//發(fā)消息
-(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(@"重新連接--主機(jī)%@-Port%@",HOST,PORT);
}
else {
DLog(@"連接失敗ERROR %@",[err description]);
}
}else{
DLog(@"已經(jīng)連接");
}
}
-(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(@"發(fā)送消息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