网络篇N:Socket

Socket的概念

  1. Socket又称”套接字”
  2. 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket
  3. 应用程序通常通过”套接字”向网络发出请求或者应答网络请求

    图1

进行网络通信的要素

  1. Socket:网络上的请求就是通过Socket来建立连接然后互相通信
  2. IP地址:(网络上主机设备的唯一标识)
  3. Port端口号:(定位程序/应用进程)
    1. 网络通信实际上是两个主机上的应用进程之间的通信,IP地址只能定位到主机,但是还需要定位到该台主机上的应用进程,因此就需要用端口号来唯一标识这台主机上的应用进程
    2. 用于标示进程的逻辑地址,不同进程的标示(简单的理解,一个进程就是一个应用程序)
    3. 有效端口:0~65535,其中0~1024由系统使用或者保留端口,开发中建议使用1024以上的端口
  4. 传输协议:(用什么样的方式进行交互)
    1. 通讯的规则
    2. 常见协议:TCP、UDP

      李四给张三发送消息: “请你吃饭” 图1 手机app的登录请求 图1

传输协议

  1. 协议:定义规则
  2. TCP/UDP 协议
  3. HTTP/HTTPS/XMPP 协议
  4. eg举例:
    1. http:实现网络数据交换的一种协议
    2. 协议头(请求头/响应头)/协议体(请求体/响应体)
    3. 请求/响应
    4. 定义数据的传输格式!

TCP & UDP 协议

  1. TCP(传输控制协议)
    1. 建立连接,形成传输数据的通道
    2. 在连接中进行大数据传输(数据大小不收限制)
    3. 通过三次握手完成连接,是可靠协议,安全送达
    4. 必须建立连接,效率会稍低
  2. UDP(用户数据报协议)
    1. 将数据及源和目的封装成数据包中,不需要建立连接
    2. 每个数据报的大小限制在64K之内
    3. 因为无需连接,因此是不可靠协议
    4. 不需要建立连接,速度快

Socket-TCP通信流程图

图1 从右边服务端TCP开始

  1. socket(): 每个端(客户端/服务端)都有一个socket,用来建立通信
  2. bind():绑定端口号,服务器端每一个应用都有对应的端口号,就是创建一个服务应用,给这个应用设置一个端口号
  3. listen(): 监听客户端随时发送的请求.(相当于有人按门铃)
  4. accept(): 是否接受外部的请求(是否开门)
  5. connect(): 客户端TCP开始建立连接
  6. write(): 客户端向服务器发送请求
  7. read(): 服务端开始读取客户端发送来的数据,然后处理
  8. write(): 服务端处理数据后,将数据发送到客户端
  9. read(): 客户端读取服务器发送来的数据,就这样重复6-8步骤,知道客户端发送close
  10. close(): 请求完成,客户端发送关闭通道
  11. read() : 服务器端接收客户端发送的请求,然后关闭close()

实例:socket服务端编程

  1. 主要实现的是socket编程,而不是http编程,有两种方式:
    1. 使用C语言实现,
    2. 使用CocoaAsyncSocket第三方框,内部是对C的封装成对象.用于socket编程
  2. 用Telnet命令来代替客户端请求
    1. 使用: telnet host port(telnet + 主机+ 端口号)
    2. 举例: telnet 192.168.10.10 5288
    3. telnet命令作用是: 连接服务器上的某个端口对应的服务,说白了,就是代替客户端,进行连接服务器.
  3. 步骤:
    1. 服务器绑定端口
    2. 监听客户端的连接
    3. 允许客户端建立连接
    4. 读取客户端的请求数据
    5. 处理客户端的请求数据
    6. 响应客户端的请求数据
  4. CocoaAsyncSocket框架:
    1. 下载框架后,在目录中: source->GCD->可以发现有2个.m文件: GCDAsyncSocket/GCDAsyncUdpSocket
    2. socket又分为TCP和UDP传输协议,TCP需要建立连接,UDP不需要建立连接.
    3. GCDAsyncSocket是TCP,GCDAsyncUdpSocket是UDP
    4. 因此,我们使用的时候只需要GCDAsyncSocket就可以了.
  5. 创建项目
    1. 打开Xcode新建项目: macOS->command Line Tool->创建
    2. 把GCDAsyncSocket.h/GCDAsyncSocket.m两个文件导入到项目中
    3. 代码举例:

       //ServerListener
       #import <Foundation/Foundation.h>
      
       @interface ServerListener : NSObject
       -(void)start;
       -(void)stop;
       @end
       #import "ServerListener.h"
       #import "GCDAsyncSocket.h"
              
       @interface ServerListener()<GCDAsyncSocketDelegate>
       //当前服务端的socket对象
       @property(strong,nonatomic)GCDAsyncSocket *serverSocket;
       //用于存储所有的客户端Socket对象(多个客户端访问该服务)
       @property(nonatomic,strong)NSMutableArray *clientSockets;
              
       @end
              
       @implementation ServerListener
              
       -(NSMutableArray *)clientSockets{
           if(!_clientSockets){
               _clientSockets = [NSMutableArray array];
           }
                  
           return _clientSockets;
       }
              
       -(void)start{
           //    1.服务器绑定端口
           //1.1 socket():创建一个Socket对象(当前服务端socket对象)
           //delegateQueue: 代理方法在那个线程调用.
           self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
                  
                  
           //1.2 bind():绑定端口(开启一个端口),监听客户端面的连接(给这个服务器设置一个端口)
           NSError *err = nil;
           [self.serverSocket acceptOnPort:5288 error:&err];
           if(err){
               NSLog(@"端口开启失败(服务开启失败)");
           }else{
               NSLog(@"服务开启成功");
           }
       }
       #pragma mark - GCDAsyncSocketDelegate
       //    2.listen():监听客户端的连接
       -(void)socket:(GCDAsyncSocket *)serverSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket{
           NSLog(@"有客户端请求连接");
           //    3.accept(): 允许客户端建立连接(实际操作就是把客户端的socket强引用起来,不让他死掉)
           //3.1把客户端socket存储到一个数组
           //如果不强制引用客户端的socket的话,就会销毁,然后断开连接
           [self.clientSockets addObject:clientSocket];
                  
           //3.2 一旦连接成功,给客户端响应数据选择
           NSMutableString *strM = [NSMutableString string];
           [strM appendString:@"欢迎来到10086在线服务,请输入下面的数字选择服务\n"];
           [strM appendString:@"[0]在线充值\n"];
           [strM appendString:@"[1]在线投诉\n"];
           [strM appendString:@"[2]优惠信息\n"];
           [strM appendString:@"[3]特殊服务\n"];
           [strM appendString:@"[4]退出\n"];
           //注意这里是客户端socket写数据
           [clientSocket writeData:[strM dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
                  
           //3.3调用客户端的读取数据的方法,这样读取数据的代理方法才会调用
           /**
            * -1:代表不超时
            * 0:随便写,现在用不到
            */
           [clientSocket readDataWithTimeout:-1 tag:0];
       }
       //    4.读取客户端的请求数据
       #warning 要使用这个代理方法读取数据前需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
       -(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
                  
           //读取数据:把data -> string
           NSString *readStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
           NSLog(@"%@",readStr);
                  
       #warning 下次还想接收数据 也需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
           [clientSocket readDataWithTimeout:-1 tag:0];
                  
           //    5.处理客户端的请求数据
           NSString *responseStr = nil;
           switch ([readStr intValue]) {
               case 0:
                   responseStr = @"系统维护中...\n";
                   break;
                          
               case 1:
                   responseStr = @"系统维护中...\n";
                   break;
               case 2:
                   responseStr = @"充1W,送5千\n";
                   break;
               case 3:
                   responseStr = @"你SB啊,真想有特殊服务呀?哈哈哈...\n";
                   break;
               case 4:
                   responseStr = @"退出\n";
                   break;
                          
           }
                  
           //    6.响应客户端的请求数据(客户端socket写数据)
           [clientSocket writeData:[responseStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
                  
                  
           // 断开连接
           if([readStr intValue] == 4){
               [self.clientSockets removeObject:clientSocket];
           }
       }
              
       -(void)stop{
       }
       @end
              
       //程序main函数调用
       #import <Foundation/Foundation.h>
       #import "ServerListener.h"
       int main(int argc, const char * argv[]) {
           @autoreleasepool {
               // insert code here...
               NSLog(@"Hello, World!");
               //1.开启10086服务
               ServerListener *server = [[ServerListener alloc] init];
               [server start];
                      
               //2.让程序不死(开启主运行循环)
               //因为是主线程不需要添加source或者timer,本来就有.子线程的话还要主动添加source和timer
               [[NSRunLoop mainRunLoop] run];
           }
           return 0;
       }
      
    4. 这就相当于,用MAC写了个MAC端服务器,而终端使用telnet就相当于客户端.
    5. 终端调用如下:

               xyjdeMacBook-Pro:~ xyj$ telnet 172.16.15.43 5288
       Trying 172.16.15.43...
       Connected to 172.16.15.43.
       Escape character is '^]'.
       欢迎来到10086在线服务,请输入下面的数字选择服务
       [0]在线充值
       [1]在线投诉
       [2]优惠信息
       [3]特殊服务
       [4]退出
       0
       系统维护中...
       1
       系统维护中...
       2
       充1W,送5千
       3
       你SB啊,真想有特殊服务呀?哈哈哈...
       4
       退出
       Connection closed by foreign host.
      
      

实例:写个转发消息服务(群聊)

  1. 需求
    1. 多个客户端连接到服务器
    2. 当一个客户端发送消息服务器时,服务器转发给其它已经连接的客户端。
    3. 相当于一个群聊的雏形
  2. 如图所示: 图1
  3. 代码(只是ServerListener.m发生了少许变化)

     #import "ServerListener.h"
     #import "GCDAsyncSocket.h"
        
     @interface ServerListener()<GCDAsyncSocketDelegate>
     //当前服务端的socket对象
     @property(strong,nonatomic)GCDAsyncSocket *serverSocket;
     //用于存储所有的客户端Socket对象(多个客户端访问该服务)
     @property(nonatomic,strong)NSMutableArray *clientSockets;
        
     @end
        
     @implementation ServerListener
        
     -(NSMutableArray *)clientSockets{
         if(!_clientSockets){
             _clientSockets = [NSMutableArray array];
         }
            
         return _clientSockets;
     }
        
     -(void)start{
         //    1.服务器绑定端口
         //1.1 socket():创建一个Socket对象(当前服务端socket对象)
         //delegateQueue: 代理方法在那个线程调用.
         self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
            
            
         //1.2 bind():绑定端口(开启一个端口),监听客户端面的连接(给这个服务器设置一个端口)
         NSError *err = nil;
         [self.serverSocket acceptOnPort:5288 error:&err];
         if(err){
             NSLog(@"端口开启失败(服务开启失败)");
         }else{
             NSLog(@"服务开启成功");
         }
     }
     #pragma mark - GCDAsyncSocketDelegate
     //    2.listen():监听客户端的连接
     -(void)socket:(GCDAsyncSocket *)serverSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket{
         //    3.accept(): 允许客户端建立连接(实际操作就是把客户端的socket强引用起来,不让他死掉)
         //3.1把客户端socket存储到一个数组
         //如果不强制引用客户端的socket的话,就会销毁,然后断开连接
         [self.clientSockets addObject:clientSocket];
         NSLog(@"有客户端连接 %zd",self.clientSockets.count);
            
            
         //3.3调用客户端的读取数据的方法,这样读取数据的代理方法才会调用
         /**
          * -1:代表不超时
          * 0:随便写,现在用不到
          */
         [clientSocket readDataWithTimeout:-1 tag:0];
     }
     //    4.读取客户端的请求数据
     #warning 要使用这个代理方法读取数据前需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
     -(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
            
         //读取数据:把data -> string
         NSString *readStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
         NSLog(@"%@",readStr);
            
     #warning 下次还想接收数据 也需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
         [clientSocket readDataWithTimeout:-1 tag:0];
            
         //转发
         for(GCDAsyncSocket *socket in self.clientSockets){
             //不能转发给自己
             if(socket != clientSocket){
                 [socket writeData:data withTimeout:-1 tag:0];
             }
         }
     }
        
     -(void)stop{
     }
     @end
    
    
  4. 操作
    1. 同时打开4个终端,然后分别都连接这个服务端,然后任意一个终端输入信息,其他3个都能收到.

socket客户端编程

  1. 情况:
    1. socket编程写一个客户端,用终端充当另外一个客户端.
    2. 运行服务端
    3. socket客户端和终端任一方发送数据,另外一方都能接收到数据
  2. socket客户端步骤
    1. 连接到服务器(IP+Port)
    2. 监听连接服务器是否成功
    3. 如果连接成功,就可发送消息给服务器
    4. 监听服务器转发过来的消息
    5. 发送时,刷新表格显示数据
    6. 接收聊天消息时,刷新表格显示数据
  3. 代码如下:

     #import "ViewController.h"
     #import "GCDAsyncSocket.h"
     @interface ViewController ()<GCDAsyncSocketDelegate>
     @property (weak, nonatomic) IBOutlet UITextField *field;
     @property(nonatomic,strong)GCDAsyncSocket *clientSocket;
     @property(nonatomic,strong)NSMutableArray *messages;
     @end
     @implementation ViewController
        
     -(NSMutableArray *)messages{
         if(!_messages){
             _messages = [NSMutableArray array];
         }
         return _messages;
     }
        
     //1.连接到服务器(IP+Port)
     - (IBAction)connectToHostAction:(id)sender {
         //1.1创建一个Socket对象
         self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
            
         //1.2连接服务器
         NSError *err = nil;
         [self.clientSocket connectToHost:@"172.16.15.43" onPort:5288 error:&err];
         //err实际上没有什么太大的作用
         if(!err){
             NSLog(@"连接请求发送成功");
         }else{
             NSLog(@"连接请求发送失败");
         }
     }
     //3.如果连接成功,就可发送消息给服务器
     - (IBAction)sendAction:(id)sender {
         //获取发送的聊天内容
         NSString *text = self.field.text;
            
         //添加换行
         text = [NSString stringWithFormat:@"%@\n",text];
            
         //发送
         [self.clientSocket writeData:[text dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
            
         //清空内容
         self.field.text = nil;
            
         //5.发送时,刷新表格显示数据
         [self.messages addObject:text];
            
         [self.tableView reloadData];
     }
        
        
     #pragma mark - GCDAsyncSocketDelegate
     //2.监听连接服务器是否成功
     -(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
            
         NSLog(@"连接服务器成功");
     #warning 调用下面的方法目的是为了读取数据的代理方法能调用
         [self.clientSocket readDataWithTimeout:-1 tag:0];
     }
        
     -(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
         NSLog(@"连接服务器失败 %@",err);
     }
        
     //4.监听服务器转发过来的消息
     -(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
         //默认读取数据的方法是不会调用
         //Data - String
         NSString *recStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            
     #warning 调用下面的方法目的是为了下次还能接收数据
         [self.clientSocket readDataWithTimeout:-1 tag:0];
            
         NSLog(@"%@",recStr);
         NSLog(@"%@",[NSThread currentThread]);
            
         //6.接收聊天消息时,刷新表格显示数据
         [self.messages addObject:recStr];
            
     #warning 此方法是在是在子线程调用的所以不能刷新UI
         [[NSOperationQueue mainQueue] addOperationWithBlock:^{
             [self.tableView reloadData];
         }];
     }
        
     #pragma mark- UITableViewDataSource
     -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
         return self.messages.count;
     }
        
     -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
            
         static NSString *ID = @"Cell";
         UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
         if(cell == nil){
             cell = [[UITableViewCell alloc] initWithStyle:0 reuseIdentifier:ID];
         }
            
         //显示内容
         cell.textLabel.text = self.messages[indexPath.row];
         return cell;
     }
     @end
    

socket层上的协议

  1. socket层上的协议就是应用层上的传输协议
  2. Socket层上的协议指的数据传输的格式
  3. HTTP协议
    1. 传输格式:假设(这是假设),实际http的格式不是这样的
    2. http1.1,content-type:multipart/form-data,content-length:188,body:username=zhangsan&password=12345
  4. XMPP协议,是一款即时通讯协议
    1. 可扩展消息处理现场协议是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线现场探测。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息
    2. 传输格式:

       <from>zhangsan<from>
       <to>lisi<to>
       <body>一起吃晚上</body>
      
  5. 即时通信为何使用XMPP协议,而不使用http协议?
    1. 即时通信数据交互特别频繁.尽量做到传输的数据最小.
    2. http有非常多的请求头,这些请求头没有用,浪费流量

TCP/UDP/HTTP/XMPP的关系

  1. 协议:定义规则
  2. TCP/UDP:数据的传输方式
  3. HTTP/XMPP:数据的传输内容格式
  4. 例子:写信
    1. 寄信的方式:EMS,顺风,韵达等.(TCP/UDP)
    2. 内容格式:英文,日语等(http/XMPP)

socket的长连接与短连接

  1. iOS的远程推送是一个长连接
  2. 即时通信也是一个长连接,每次发送消息完不能立刻断开.
  3. http是一个短连接
  4. 如下图举例: 图1
  5. 情况
    1. 微信客户端在开启状态下时,微信服务器发送消息给客户端直接通过TCP/IP通道
    2. 微信客户端在关闭状态下时,微信服务器就不能直接给微信客户端发送消息了,就需要将消息推送给苹果的APNS服务器,由APNS推送给微信客户端,因为苹果的APNS服务器一直个微信APP保持连接着.

    githubDemo地址

Table of Contents