TCP 看我就夠了

陳雨塵發表於2018-07-18

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學習交流群

相關文章