【iOS乾貨】☞ 初識 Socket 網絡通訊

1、概念

  • Socket 字面意思又稱「套接字」git

  • 網絡上的兩個程序(如,客戶端和服務器端)經過一個雙向的通訊鏈接實現數據的交換,這個鏈接的一端稱爲一個Socket。github

  • 應用程序通常是先經過Socket來創建一個通訊鏈接,再向網絡發出請求或響應網絡請求。

  

  說明:json

    ☞ 客戶端向服務器端發送網絡請求前,必需要先在底層創建一個通訊鏈接(通訊管道),才能發送網絡請求。數組

客戶端向服務器端發送http請求,服務器返回數據,這個過程就是一個數據交換的過程。安全

客戶端與服務器端進行數據交換,須要先創建一個雙向的通訊鏈接(即一條線、一個通道)服務器

    ☞ 客戶端和服務端 兩端都有一個Socket,經過Socket創建一個鏈接(雙向通訊管道),有了管道就能夠進行數據傳輸。網絡

    ☞ Socket 就是通訊管道的兩個端口,能夠理解爲管道的入口/出口app

2、網絡通訊的要素

  網絡上的請求就是經過Socket來創建鏈接而後互相通訊框架

  1. IP地址(網絡上主機設備的惟一標識)——>尋找服務器主機socket

  2. 端口號定位程序 ——> 尋找程序

    • 用於標示進程的邏輯地址,不一樣進程的標示
    • 有效端口:0~65535,其中0~1024由系統使用或者保留端口,開發中建議使用1024以上的端口

  3. 傳輸協議(就是用什麼樣的方式進行交互)

    • 通信的規則
    • 常見協議:TCP、UDP

3、傳輸協議 TCP/UDP

  TCP和UDP:數據傳輸的兩種方式,即把數據從一端傳到另外一端的兩種方式

  1. TCP(傳輸控制協議) —>要創建鏈接(如:發送HTTP請求,客戶端向服務端發送網絡請求)

☞ 創建鏈接,造成傳輸數據的通道

☞ 在鏈接中進行大數據傳輸(數據大小不受限制)

☞ 經過三次握手完成鏈接,是可靠協議,安全送達

        說明:在創建通訊鏈接(打通管道)以前有三次握手,目的是爲了數據的安全性和可靠性(讓數據安全可靠的傳輸到對方)。

        舉例:打電話 (理解三次握手)

第一次握手:拿起電話,進行撥號。這個撥號的過程稱爲第一次握手。【開始準備鏈接】

第二次握手:撥通了,對方""了一聲(響應了一聲),我聽到了,稱爲第二次握手。【說明我鏈接你 沒問題】

第三次握手:我聽到了對方""了一聲(響應了一聲),我也習慣性的""了一聲,對方聽到了。【說明你鏈接我 沒問題

若是這三個過程都沒有問題,就能夠肯定通話鏈接創建成功。

    ☞ 必須創建鏈接,效率會稍低。(每次請求都要創建鏈接)

  2. UDP(用戶數據報協議)—>不創建鏈接 (如:廣播用這個,不斷的發送數據包)

    ☞ 將 數據 目的 封裝成數據包中,不須要創建鏈接

    ☞ 每一個數據報的大小限制在64KB以內

    ☞ 由於無需鏈接,所以是不可靠協議

      舉例:看老師廣播講課,網絡卡主了,再看到的是最新的視頻內容,不能接着看,可能錯過了一些內容。

    ☞ 不須要創建鏈接,速度快 (省掉了三次握手操做)

4、Socket 通訊流程圖

☞ bind():綁定端口 (80、3306)

☞ listen():監聽端口(服務器監聽客戶端有沒有鏈接到這個端口來)

☞ accept():若是有鏈接到這個端口,就接收這個鏈接。(通訊管道打通,接下來就能夠傳輸數據了)

☞ write():發請求/寫請求/發數據

☞ read():讀請求/讀數據

  • HTTP底層就是Socket通訊,經過Socket創建鏈接(通訊管道),實現數據傳輸,鏈接的方式(數據傳輸的方式)是TCP。
  • HTTP是一個TCP的傳輸協議(方式),它是一個可靠、安全的協議。

5、體驗 Socket

  實現Socket服務端監聽:

1使用C語言實現。

2使用 CocoaAsyncSocket 第三方框架(OC),內部是對C的封裝。

    telnet命令:是鏈接服務器上的某個端口對應的服務。

    telnet命令:telnet host port 

      如:telnet www.baidu.com 80  (IP地址和域名同樣,都能找到主機。)

1. 【案例】寫個10086服務,體驗客戶端與服務端的Socket通訊

☞ 本身寫一個服務端,用終端代替客戶端來演示

☞ 掌握:經過Socket對象在服務器裏怎麼去接收數據和返回數據。

/// ----- MyServiceListener.h -----
@interface MyServiceListener : NSObject
//開啓服務
- (void)start;
@end

/// ----- MyServiceListener.m -----
#import "MyServiceListener.h"
#import "GCDAsyncSocket.h"
/**
 *  服務的監聽者(服務端監聽客戶端鏈接)
 */
@interface MyServiceListener()<GCDAsyncSocketDelegate>
/** 保存服務端的Socket對象 */
@property (nonatomic, strong) GCDAsyncSocket *serviceSocket;
/** 保存客戶端的全部Socket對象 */
@property (nonatomic, strong) NSMutableArray *clientSocketArr;

@end

@implementation MyServiceListener
- (GCDAsyncSocket *)serviceSocket {
    if (!_serviceSocket) {
        //1.建立一個Socket對象
                //serviceSocket 服務端的Socket只監聽 有沒有客戶端請求鏈接
               //隊列:代理的方法在哪一個隊列裏調用 (子線程的隊列)
        _serviceSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    }
    return _serviceSocket;
}

- (NSMutableArray *)clientSocketArr {
    if(!_clientSocketArr) {
        _clientSocketArr = [NSMutableArray array];
    }
    return _clientSocketArr;
}

- (void)start {
    //開啓10086服務:5288
    //2.綁定端口 + 開啓監聽
    NSError *error = nil;
    //框架裏的這個方法作了兩件事情:綁定端口和開啓監聽
    [self.serviceSocket acceptOnPort:5288 error:&error];
    if (!error) {
        NSLog(@"10086服務開啓成功!");
    } else {
        //失敗的緣由是端口被其它程序佔用
        NSLog(@"10086服務開啓失敗:%@", error);
    }    
}

#pragma mark -- 實現代理的方法 若是有客戶端的Socket鏈接到服務器,就會調用這個方法。
- (void)socket:(GCDAsyncSocket *)serviceSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket {
    static NSInteger index = 1;
    NSLog(@"客戶端【%ld】已鏈接到服務器!", index++);
    //1.保存客戶端的Socket(客戶端的Socket被釋放了,鏈接就會關閉)
    [self.clientSockets addObject:clientSocket];
    
    //提供服務(客戶端一鏈接到服務器,就打印下面的內容)
    NSMutableString *serviceStr = [[NSMutableString alloc]init];
    [serviceStr appendString:@"========歡迎來到10086在線服務========\n"];
    [serviceStr appendString:@"請輸入下面的數字選擇服務...\n"];
    [serviceStr appendString:@" [0] 在線充值\n"];
    [serviceStr appendString:@" [1] 在線投訴\n"];
    [serviceStr appendString:@" [2] 優惠信息\n"];
    [serviceStr appendString:@" [3] special services\n"];
    [serviceStr appendString:@" [4] 退出\n"];
    [serviceStr appendString:@"=====================================\n"];
    // 服務端給客戶端發送數據
    [clientSocket writeData:[serviceStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    
    //2.監聽客戶端有沒有數據上傳 (參數1:超時時間,-1表明不超時)
    /**
     *  timeout: 超時時間,-1 表明不超時
     *  tag:標識做用,如今不用就寫0
     */
    [clientSocket readDataWithTimeout:-1 tag:0];
}

#pragma mark -- 服務器端 讀取 客戶端請求(發送)的數據。在服務端接收客戶端數據,這個方法會被調用
- (void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag {
    //1.獲取客戶端發送的數據
    NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSInteger index = [self.clientSocketArr indexOfObject:clientSocket];
    NSLog(@"接收到客戶端【%ld】發送的數據:%@", index + 1, str);
    //把字符串轉成數字
    NSInteger num = [str integerValue];
    NSString *responseStr = nil;
    //服務器對應的處理的結果
    switch (num) {
        case 0:
            responseStr = @"在線充值服務暫停中...\n";
            break;
        case 1:
            responseStr = @"在線投訴服務暫停中...\n";
            break;
        case 2:
            responseStr = @"優惠信息沒有\n";
            break;
        case 3:
            responseStr = @"沒有特殊服務\n";
            break;
        case 4:
            responseStr = @"恭喜你退出成功!\n";
            break;
        default:
            break;
    }
    
    //2.服務端處理請求,返回數據(data)給客戶端
    [clientSocket writeData:[responseStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    //寫完數據後 判斷
    if (num == 4) {
        //移除客戶端,就會關閉鏈接
        [self.clientSockets removeObject:clientSocket];
    }

    //因爲框架內部的實現,每次讀完數據後,都要調用一次監聽數據的方法(保證能接收到客戶端第二次上傳的數據)
    [clientSocket readDataWithTimeout:-1 tag:0];
    
}
@end

/// ----- ViewController.m -----
#import "ViewController.h"
#import "MyServiceListener.h"
@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //1.建立一個服務監聽對象
    MyServiceListener *listener = [[MyServiceListener alloc]init];
    //2.開始監聽
    [listener start];
    //3.開啓主運行循環,讓服務不能停(服務器通常要永久開啓)
    [[NSRunLoop mainRunLoop] run];
    
}
@end
☞ 體驗Socket通訊-服務端簡單實現代碼:

Demo下載地址:https://github.com/borenfocus/Socket10086ServerDemo 

 

2. 【案例擴展】寫個轉發消息服務(羣聊服務端)

  • 多個客戶端鏈接到服務器。
  • 當一個客戶端發送消息給服務器時,服務器轉發給其它已經鏈接的客戶端。
  • 至關於一個羣聊的雛形。

  

/// MyService.h
#import <Foundation/Foundation.h>

@interface MyService : NSObject
/** 開啓服務 */
- (void)startService;

@end

/// MyService.m
#import "MyService.h"
#import "GCDAsyncSocket.h"

@interface MyService ()<GCDAsyncSocketDelegate>
/** 保存服務端的Socket對象 */
@property (nonatomic, strong) GCDAsyncSocket *serviceSocket;
/** 保存客戶端的全部Socket對象 */
@property (nonatomic, strong) NSMutableArray *clientSocketArr;

@end

@implementation MyService

//開啓10086服務:5288
- (void)startService {
    NSError *error = nil;
    // 綁定端口 + 開啓監聽
    [self.serviceSocket acceptOnPort:5288 error:&error];
    if (!error) {
        NSLog(@"服務開啓成功!");
    } else {
        NSLog(@"服務開啓失敗!");
    }
}

#pragma mark -- 實現代理的方法 若是有客戶端的Socket鏈接到服務器,就會調用這個方法。
- (void)socket:(GCDAsyncSocket *)serviceSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket {
    // 客戶端的端口號是系統分配的,服務端的端口號是咱們本身分配的
    NSLog(@"客戶端【Host:%@, Port:%d】已鏈接到服務器!", clientSocket.connectedHost, clientSocket.connectedPort);
    //1.保存客戶端的Socket(客戶端的Socket被釋放了,鏈接就會關閉)
    [self.clientSocketArr addObject:clientSocket];
    
    //2.監聽客戶端有沒有數據上傳 (參數1:超時時間,-1表明不超時;參數2:標識做用,如今不用就寫0)
    [clientSocket readDataWithTimeout:-1 tag:0];
}

#pragma mark -- 服務器端 讀取 客戶端請求(發送)的數據。在服務端接收客戶端數據,這個方法會被調用
- (void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag {
    //1.獲取客戶端發送的數據
    NSString *messageStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"接收到客戶端【Host:%@, Port:%d】發送的數據:%@",  clientSocket.connectedHost, clientSocket.connectedPort, messageStr);
    // 遍歷客戶端數組
    for (GCDAsyncSocket *socket in self.clientSocketArr) {
        if (socket != clientSocket) { // 不轉發給本身
            //2.服務端把收到的消息轉發給其它客戶端
            [socket writeData:data withTimeout:-1 tag:0];
        }
    }
    //因爲框架內部的實現,每次讀完數據後,都要調用一次監聽數據的方法(保證能接收到客戶端第二次上傳的數據)
    [clientSocket readDataWithTimeout:-1 tag:0];
}

- (GCDAsyncSocket *)serviceSocket {
    if (!_serviceSocket) {
        // 1.建立一個Socket對象
        // serviceSocket 服務端的Socket只監聽 有沒有客戶端請求鏈接
        // 隊列:代理的方法在哪一個隊列裏調用 (子線程的隊列)
        _serviceSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    }
    return _serviceSocket;
}

- (NSMutableArray *)clientSocketArr {
    if (!_clientSocketArr) {
        _clientSocketArr = [[NSMutableArray alloc]init];
    }
    return _clientSocketArr;
}

@end


/// main.m
#import <Foundation/Foundation.h>
#import "MyService.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //1.建立一個服務監聽對象
        MyService *service = [[MyService alloc]init];
        //2.開始監聽
        [service startService];
        //3.開啓主運行循環,讓服務不能停(服務器通常要永久開啓)
        [[NSRunLoop mainRunLoop] run];
    }
    return 0;
}
☞ 體驗Socket通訊-羣聊服務端實現代碼:

 Demo下載地址:https://github.com/borenfocus/SocketGroupServerDemo

3. 【案例】體驗Socket通訊-羣聊客戶端實現

  

  

///  ViewController.m
#import "ViewController.h"
#import "GCDAsyncSocket.h"

@interface ViewController ()<UITableViewDataSource, GCDAsyncSocketDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (nonatomic, strong) GCDAsyncSocket *clientSocket;

@property (nonatomic, strong) NSMutableArray *dataArr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 實現聊天室
    // 1. 鏈接到服務器
    NSError *error = nil;
    [self.clientSocket connectToHost:@"192.168.1.95" onPort:5288 error:&error];
    if (error) {
        NSLog(@"error:%@", error);
    }
}

#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)clientSock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"與服務器鏈接成功!");
    // 監聽讀取數據(在讀數據的時候,要監聽有沒有數據可讀,目的是保證數據讀取到)
    [clientSock readDataWithTimeout:-1 tag:0];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"與服務器斷開鏈接:%@", err);
}

// 讀取數據(接收消息)
- (void)socket:(GCDAsyncSocket *)clientSock didReadData:(NSData *)data withTag:(long)tag {
    NSString *messageStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"接收到消息:%@", messageStr);
    messageStr = [NSString stringWithFormat:@"【匿名】:%@", messageStr];
    [self.dataArr addObject:messageStr];
    // 刷新UI要在主線程
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.tableView reloadData];
    });
    
    // 監聽讀取數據(讀完數據後,繼續監聽有沒有數據可讀,目的是保證下一次數據能夠讀取到)
    [clientSock readDataWithTimeout:-1 tag:0];
}

#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataArr.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    cell.textLabel.text = self.dataArr[indexPath.row];
    return cell;
}

- (IBAction)clickSenderBtn:(UIButton *)sender {
    NSLog(@"發送消息");
    [self.view endEditing:YES];
    NSString *senderStr = self.textField.text;
    if (senderStr.length == 0) {
        return;
    }
    // 發送數據
    [self.clientSocket writeData:[senderStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    
    senderStr = [NSString stringWithFormat:@"【我】:%@", senderStr];
    [self.dataArr addObject:senderStr];
    [self.tableView reloadData];
}

- (GCDAsyncSocket *)clientSocket {
    if (!_clientSocket) {
        _clientSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    }
    return _clientSocket;
}

- (NSMutableArray *)dataArr {
    if (!_dataArr) {
        _dataArr = [[NSMutableArray alloc]init];
    }
    return _dataArr;
}

@end
☞ 體驗Socket通訊-羣聊客戶端實現:

Demo下載地址:https://github.com/borenfocus/SocketGroupClientDemo  

6、長鏈接和短鏈接

  長鏈接和短鏈接:是鏈接的一個保存狀態保存時間),長鏈接就是長時間鏈接,短鏈接就是短期鏈接。

  • http網絡請求是短鏈接。
  • 長鏈接用在即時通訊(實時聊天,要隨時隨地的發送信息,考慮到性能,用長鏈接)

7、Socket 層上的協議

  Socket層上的協議:指的數據傳輸的格式

  1. HTTP協議:定義在網絡上數據傳輸的一種格式。

    傳輸格式:假設:這是假設,實際http的格式不是這樣的。

    http1.1,content-type:multipart/form-data,content-length:188,body:username=zhangsan&password=123456

  2. XMPP協議:是一款即時通信協議 (別人定義好的協議,咱們常常拿來用)

    是基於可擴展標記語言(XML)的協議,它用於即時消息(IM)以及在線現場探測。

    傳輸格式:

      <from>zhangsan<from>

      <to>lisi<to>

      <body>一塊兒吃晚上</body>

  3. 自定義即時通信協議,json格式。

    {

      "from": "zhangsan",

      "to": "lisi",

      "body": "中午一塊兒吃飯",

    }

  你作什麼操做,必需要有一個固定的格式,這樣服務器才知道你要作什麼。

  

  舉例:寫一封信給北京好友(區別 TCP/UDP 與 HTTP/XMMP)

  • 數據傳輸的方式:TCP/UDP —》至關於 EMS/順豐/申通/中通   
  • 數據傳輸的格式:HTTP/XMMP —》至關於 信的內容格式 (能夠是中文/英文/…等
相關文章
相關標籤/搜索