TCP 看我就够了

TCP的初识

TCP 是一种面向连接的,可靠的,基于字节流的传输层通信协议.TCP工作在网络OSI七层模型中的第四层-传输层,下面一张图展示OSI七层模型及每一层的作用和对应的协议.


图.png

TCP是传输层协议,在进行数据传输之前使用三次握手协议建立连接,大体的过程是客户端发出SYN连接请求后,服务端接收请求后应答SYN+ACK,客户端收到服务端应答后应答ACK,这种建立连接的方法可以防止产生错误的连接,防止已失效的连接请求报文段突然又传送到了服务端。TCP三次握手过程图示如下:

图片.png

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四次挥过程图示如下:

图.png

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

好了文章就到这里了谢谢大家!大家也可以加Q 群 229309298 一个iOS学习交流群

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

推荐阅读更多精彩内容