zoukankan      html  css  js  c++  java
  • 【iOS干货】☞ 初识 Socket 网络通信

    一、概念

    • Socket 字面意思又称“套接字”

    • 网络上的两个程序(如,客户端和服务器端)通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。

    • 应用程序一般是先通过Socket来建立一个通信连接,再向网络发出请求或响应网络请求。

      

      说明:

        ☞ 客户端向服务器端发送网络请求前,必须要先在底层建立一个通信连接(通信管道),才能发送网络请求。

    客户端向服务器端发送http请求,服务器返回数据,这个过程就是一个数据交换的过程。

    客户端与服务器端进行数据交换,需要先建立一个双向的通信连接(即一条线、一个通道)

        ☞ 客户端和服务端 两端都有一个Socket,通过Socket建立一个连接(双向通信管道),有了管道就可以进行数据传输。

        ☞ Socket 就是通信管道的两个端口,可以理解为管道的入口/出口

    二、网络通信的要素

      网络上的请求就是通过Socket来建立连接然后互相通信

      1. IP地址(网络上主机设备的唯一标识)——>寻找服务器主机

      2. 端口号定位程序 ——> 寻找程序

      • 用于标示进程的逻辑地址,不同进程的标示
      • 有效端口:0~65535,其中0~1024由系统使用或者保留端口,开发中建议使用1024以上的端口

      3. 传输协议(就是用什么样的方式进行交互)

      • 通讯的规则
      • 常见协议:TCP、UDP

    三、传输协议 TCP/UDP

      TCP和UDP:数据传输的两种方式,即把数据从一端传到另一端的两种方式

      1. TCP(传输控制协议) —>要建立连接(如:发送HTTP请求,客户端向服务端发送网络请求)

    ☞ 建立连接,形成传输数据的通道

    ☞ 在连接中进行大数据传输(数据大小不受限制)

    ☞ 通过三次握手完成连接,是可靠协议,安全送达

            说明:在建立通信连接(打通管道)之前有三次握手,目的是为了数据的安全性和可靠性(让数据安全可靠的传输到对方)。

            举例:打电话 (理解三次握手)

    第一次握手:拿起电话,进行拨号。这个拨号的过程称为第一次握手。【开始准备连接】

    第二次握手:拨通了,对方""了一声(响应了一声),我听到了,称为第二次握手。【说明我连接你 没问题】

    第三次握手:我听到了对方""了一声(响应了一声),我也习惯性的""了一声,对方听到了。【说明你连接我 没问题

    如果这三个过程都没有问题,就可以确定通话连接建立成功。

        ☞ 必须建立连接,效率会稍低。(每次请求都要建立连接)

      2. UDP(用户数据报协议)—>不建立连接 (如:广播用这个,不断的发送数据包)

        ☞ 将 数据 目的 封装成数据包中,不需要建立连接

        ☞ 每个数据报的大小限制在64KB之内

        ☞ 因为无需连接,因此是不可靠协议

          举例:看老师广播讲课,网络卡主了,再看到的是最新的视频内容,不能接着看,可能错过了一些内容。

        ☞ 不需要建立连接,速度快 (省掉了三次握手操作)

    四、Socket 通信流程图

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

    ☞ listen():监听端口(服务器监听客户端有没有连接到这个端口来)

    ☞ accept():如果有连接到这个端口,就接收这个连接。(通信管道打通,接下来就可以传输数据了)

    ☞ write():发请求/写请求/发数据

    ☞ read():读请求/读数据

    • HTTP底层就是Socket通信,通过Socket建立连接(通信管道),实现数据传输,连接的方式(数据传输的方式)是TCP。
    • HTTP是一个TCP的传输协议(方式),它是一个可靠、安全的协议。

    五、体验 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在线服务========
    "];
        [serviceStr appendString:@"请输入下面的数字选择服务...
    "];
        [serviceStr appendString:@" [0] 在线充值
    "];
        [serviceStr appendString:@" [1] 在线投诉
    "];
        [serviceStr appendString:@" [2] 优惠信息
    "];
        [serviceStr appendString:@" [3] special services
    "];
        [serviceStr appendString:@" [4] 退出
    "];
        [serviceStr appendString:@"=====================================
    "];
        // 服务端给客户端发送数据
        [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 = @"在线充值服务暂停中...
    ";
                break;
            case 1:
                responseStr = @"在线投诉服务暂停中...
    ";
                break;
            case 2:
                responseStr = @"优惠信息没有
    ";
                break;
            case 3:
                responseStr = @"没有特殊服务
    ";
                break;
            case 4:
                responseStr = @"恭喜你退出成功!
    ";
                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  

    六、长连接和短连接

      长连接和短连接:是连接的一个保存状态保存时间),长连接就是长时间连接,短连接就是短时间连接。

    • http网络请求是短连接。
    • 长连接用在即时通信(实时聊天,要随时随地的发送信息,考虑到性能,用长连接)

    七、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 —》相当于 信的内容格式 (可以是中文/英文/…等

     

  • 相关阅读:
    WPF / Win Form:多线程去修改或访问UI线程数据的方法( winform 跨线程访问UI控件 )
    TCP 流模式与UDP数据报模式(转)
    hibernate 检索方式
    【UVA】1449-Dominating Patterns(AC自己主动机)
    软件项目工作流程图
    iOS7 UIKit动力学-碰撞特性UICollisionBehavior 下
    东莞无人工厂变成现实,中国无人工厂将非常快普及,保住世界工厂地位
    小米手机与魅族的PK战结果 说明了什么
    python Debug 单步调试
    合并两个排序的单链表
  • 原文地址:https://www.cnblogs.com/bossren/p/7206072.html
Copyright © 2011-2022 走看看