网络篇N+1:Socket自定义协议

车联网

过程如下:

图1

  1. 手机能够控制汽车的条件是手机和汽车二者都必须联网,才能通信
  2. 汽车如和联网呢?
    1. 汽车有一个设备交OBD
    2. 只要将这个设备插入到汽车中,就能监测(采集)到汽车的一些信息
    3. OBD中可以插入SIM卡,这个卡就相当于电话卡,就可以联网了
  3. 汽车发送信息给APP:
    1. OBD采集到数据后,首先通过互联网发送到的是服务器,保存,然后由服务器通过互联网发送给手机app
  4. App发出指令开汽车空调
    1. App发送指令给服务器,服务器再发送给汽车
  5. 可以看到手机跟汽车之间有一个数据的传输,因此,手机跟汽车之间必须拟定一个传输协议,告诉汽车什么是开门/开空调等等指令.
  6. 这时通信一般不是使用http协议,因此需要自定义通讯协议.

自定义协议

  1. 发送一条数据基本要素
    1. 如何判断(图片)数据有没有发送完成?(数据很大时,是一帧一帧的发送的)
      1. 获取数据包的总大小
      2. 如果拼接的data等于数据包的大小,就算是接收完了
    2. 还要给出该数据的类型:图片/文字/语音…
    3. 即要发送(图片)数据,又要发送过来总数据包的大小/数据类型,怎么办呢?
  2. 客户端与服务器定义的协议如下:
    1. 数据传输 图1
  3. 代码展示:
    1. 将协议封装到一个工具类DefineScoketProtocol中:

       //这个类主要是用来定制客户单与服务器之间的协议
      
       #import <Foundation/Foundation.h>
              
       typedef enum : NSUInteger {
           PDataTypeText = 0x00000001,
           PDataTypeAudio = 0x00000002,
           PDataTypePic = 0x00000003,
           PDataTypeLocation = 0x00000004
       } PDataType;
       @interface DefineScoketProtocol : NSObject
              
       /*******客户端部分*********/
       /**
        客户端协议: 4字节总数据大小 + 4字节数据类型 + N字节传输数据大小
              
        @param data 要发送的数据
        @param dataType 数据类型
        @return 协议格式化后的数据
        */
       +(NSData *)useRequestProtocolWriteData:(NSData *)data andType:(PDataType)dataType;
              
       /**
        客户端解析服务器发送的数据
               
        @param data 服务器发送的数据
        @param result 解析后的结果
        */
       +(void)responseProtocolAnalyWithData:(NSData *)data andResult:(void(^)(NSUInteger totalSize,PDataType dataType,NSUInteger response))result;
              
       /*******服务器部分*********/
       /**
        服务器解析客户端发送的数据
              
        @param data 客户端发送的数据
        @param result 解析后的结果
        */
       +(void)requestProtocolAnalyWithData:(NSData *)data andResult:(void(^)(NSUInteger totalSize,PDataType dataType,NSData *inputData))result;
       /**
        服务端协议: 4字节总数据大小 + 4字节数据类型 + 4字节响应数据大小
               
        @param dataType 数据类型
        @return 协议格式化后的数据
        */
       +(NSData *)useResponseProtocolWithDataType:(PDataType)dataType;
          
       @end
              
        #import "DefineScoketProtocol.h"
       /**
        当客户端发送的数据非常大时,数据是一帧一帧发送过来的,因此要保存起来.
        */
       static NSMutableData *_dataM;
       /** 总的数据包大小*/
       static NSUInteger _totalSize;
       /**当前的指令类型*/
       static NSUInteger _commandId;
              
       @implementation DefineScoketProtocol
              
       +(void)load{
           _dataM = [NSMutableData data];
       }
              
       /**
        客户端协议: 4字节总数据大小 + 4字节数据类型 + N字节传输数据大小
               
        @param data 要发送的数据
        @param dataType 数据类型
        @return 协议格式化后的数据
        */
       +(NSData *)useRequestProtocolWriteData:(NSData *)data andType:(PDataType)dataType{
           NSMutableData *totalData = [NSMutableData data];
           //1. 拼接长度(0-3个字节代表长度)
           //总大小 = 4个字节(表示总大小) + 4个字节(表示数据类型) + N个字节(图片数据所占字节数)
           NSUInteger totalSize = 4+4+data.length;
           //将NSUInteger转化为NSData,并且占据4个字节
           NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
           //拼接
           [totalData appendData:totalSizeData];
                  
           //2. 拼接指令种类(4-7字节代表指令类型)
           // 0x00000001 = 图片
           // 0x00000002 = 文字
           // 0x00000003 = 位置
           // 0x00000004 = 语音
           // 用16进制表示
           NSUInteger commandId = dataType;
           //将NSUInteger转化为NSData,并且占据4个字节
           NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
           //拼接
           [totalData appendData:commandIdData];
                  
           //3. 拼接图片数据(8-N代表数据)
           //拼接
           [totalData appendData:data];
           NSLog(@"客户端发送数据: 大小为:%ld==数据类型:%ld==有效数据大小:%ld",totalSize,commandId,data.length);
           return  totalData;
       }
              
       /**
        服务端协议: 4字节总数据大小 + 4字节数据类型 + 4字节响应数据大小
               
        @param dataType 数据类型
        @return 协议格式化后的数据
        */
       +(NSData *)useResponseProtocolWithDataType:(PDataType)dataType{
           NSMutableData *totalDataM = [NSMutableData data];
           // 1.返回数据总字节长度(0~3:长度)
           NSUInteger totalSize = 4 + 4 + 4;
           NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
           [totalDataM appendData:totalSizeData];
           // 2.响应指令类型
           NSUInteger commandId = dataType;
           NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
           [totalDataM appendData:commandIdData];
                  
           //3.上传的结果 //1:上传成功 0://上传失败
           NSUInteger result = 1;
           NSData *resultData = [NSData dataWithBytes:&result length:4];
           [totalDataM appendData:resultData];
           NSLog(@"服务器向客户端发出响应: 大小为:%ld==相应类型:%ld==响应状态:%ld",totalSize,commandId,result);
           return totalDataM;
       }
              
       /**
        服务器解析客户端发送的数据
               
        @param data 客户端发送的数据
        @param result 解析后的结果
        */
       +(void)requestProtocolAnalyWithData:(NSData *)data andResult:(void (^)(NSUInteger, PDataType, NSData *))result{
           //1. 处理解析客户端发送过来的数据
           //第一次接收数据
           if (!_dataM.length) {
               //1.1 获取总数据包的大小
               //截取前4个字节
               NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
               NSUInteger totalSize = 0;
               //之前是NSUInteger转化为NSData -> 现在将NSData转化为NSUInteger
               [totalSizeData getBytes:&totalSize length:4];
               _totalSize =  totalSize;
                      
               //1.2 获取类型指令
               // 获取指令类型
               NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
               NSUInteger commandId = 0;
               [commandIdData getBytes:&commandId length:4];
               _commandId = commandId;
                      
           }
                  
           //1.3 拼接二进制
           [_dataM appendData:data];
                  
           //1.4 数据已经接收完成,进行处理
           if (_dataM.length == _totalSize) {
               //1.5 将处理结果进一步处理
               NSLog(@"解析客户端: 总大小为:%ld==数据类型:%ld==有效数据大小:%ld",_totalSize,_commandId,_dataM.length-8);
                      
               result(_totalSize,_commandId,_dataM);
               //1.6 数据解析完成要清空
               _dataM = [NSMutableData data];
           }
       }
              
       /**
        客户端解析服务器发送的数据
               
        @param data 服务器发送的数据
        @param result 解析后的结果
        */
       //注意: 数据比较小,一帧就能发完
       +(void)responseProtocolAnalyWithData:(NSData *)data andResult:(void(^)(NSUInteger totalSize,PDataType dataType,NSUInteger response))result{
           // 1. 获取总的数据包大小
           NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
           NSUInteger totalSize = 0;
           [totalSizeData getBytes:&totalSize length:4];
                  
           //2. 获取指令类型
           NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
           NSUInteger commandId = 0;
           [commandIdData getBytes:&commandId length:4];
                  
           //3. 结果
           NSData *resultData = [data subdataWithRange:NSMakeRange(8, 4)];
           NSUInteger resultx = 0;
           [resultData getBytes:&resultx length:4];
           NSLog(@"服务器响应: 大小为:%ld==响应类型:%ld==响应状态:%ld",totalSize,commandId,resultx);
           //4. 返回结果值
           result(totalSize,commandId,resultx);
       }
       @end
      
      
    2. 客户端代码:

              
       #import "ViewController.h"
       #import "GCDAsyncSocket.h"
       #import "DefineScoketProtocol.h"
       @interface ViewController ()<GCDAsyncSocketDelegate>
       @property (weak, nonatomic) IBOutlet UILabel *statusLabel;
       /** 客户端的Socket */
       @property (nonatomic ,strong) GCDAsyncSocket *clientSocket;
       @end
              
       @implementation ViewController
              
              
       - (IBAction)connectToHost:(id)sender {
           // 1.创建一个socket对象
           if (self.clientSocket == nil) {
               self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
           }
                  
           // 2.连接服务器
           NSError *error = nil;
           [self.clientSocket connectToHost:@"172.16.15.43" onPort:5288 error:&error];
           if (error) {
               NSLog(@"%@",error);
           }
       }
       //断开连接
       - (IBAction)closeToHost:(id)sender {
           [self.clientSocket disconnect];
       }
              
       //发送文本信息
       - (IBAction)sendText:(id)sender {
           //1. 拿到文本data
           NSString *text = @"Hello,自定义协议";
           NSData *textData = [text dataUsingEncoding:NSUTF8StringEncoding];
                  
           //2. 根据自定义协议,组装数据格式
           NSData *totalDataM = [DefineScoketProtocol useRequestProtocolWriteData:textData andType:PDataTypeText];
                  
           //3. 将自定义协议格式的数据发送给服务器
           [self.clientSocket writeData:totalDataM withTimeout:-1 tag:0];
       }
              
       //发送图片
       - (IBAction)sendImag:(id)sender {
                  
           //1.把图片转化为NSData
           UIImage *image = [UIImage imageNamed:@"IMG_2427.JPG"];
           NSData *imgdata = UIImagePNGRepresentation(image);
                 
           //2. 根据自定义协议,组装数据格式
           NSData *totalData = [DefineScoketProtocol useRequestProtocolWriteData:imgdata andType:PDataTypePic];
           //imgdata.length:拿到的就是图片数据所占的字节数;
           NSLog(@"图片的字节大小:%ld",imgdata.length);
           NSLog(@"发送数据的总字节大小:%ld",totalData.length);
           //3. 将自定义协议格式的数据发送给服务器
           [self.clientSocket writeData:totalData withTimeout:-1 tag:0];
                  
       }
              
       #pragma mark - GCDAsyncSocketDelegate
       // 与服务器连接成功
       -(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
                  
           self.statusLabel.text = @"连接中..";
           self.statusLabel.backgroundColor = [UIColor greenColor];
                  
       #warning 读取数据
           [sock readDataWithTimeout:-1 tag:0];
       }
              
       //与服务器断开连接
       -(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
           NSLog(@"%@",err);
           self.statusLabel.text = @"断开..";
           self.statusLabel.backgroundColor = [UIColor redColor];
       }
       /**
        *  解析服务器返回的数据
        */
       // 接收服务器响应的数据
       -(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
                  
           //1. 解析服务器的响应
           [DefineScoketProtocol responseProtocolAnalyWithData:data andResult:^(NSUInteger totalSize, PDataType dataType, NSUInteger response) {
               NSMutableString *str = [NSMutableString string];
               if (dataType == PDataTypePic) {//图片
                   [str appendString:@"图片 "];
               }else if(dataType == PDataTypeText){//文字
                   [str appendString:@"文字 "];
               }
                      
               if(response == 1){
                   [str appendString:@"上传成功"];
               }else{
                   [str appendString:@"上传失败"];
               }
                      
               NSLog(@"%@",str);
           }];
                  
       #warning 可以接收下一次数据
           [clientSocket readDataWithTimeout:-1 tag:0];
                  
       }
       @end
      
    3. 服务端代码

       #import <Foundation/Foundation.h>
      
       @interface ServerListener : NSObject
       -(void)start;
       @end
      
       #import "ServerListener.h"
       #import "GCDAsyncSocket.h"
       #import "DefineScoketProtocol.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对象
           self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
                  
           //1.2 绑定端口
           NSError *err = nil;
           [self.serverSocket acceptOnPort:5288 error:&err];
           if(err){
               NSLog(@"端口开启失败(服务开启失败)");
           }else{
               NSLog(@"服务开启成功");
           }
       }
       #pragma mark - GCDAsyncSocketDelegate
       //    2.监听客户端的连接
       -(void)socket:(GCDAsyncSocket *)serverSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket{
           NSLog(@"有客户端请求连接");
           //3.允许客户端建立连接
           [self.clientSockets addObject:clientSocket];
           //3.1调用客户端的读取数据的方法,这样读取数据的代理方法才会调用
           [clientSocket readDataWithTimeout:-1 tag:0];
       }
       // 4.读取客户端的请求数据
       -(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
           //1.解析客户端发送过来的数据
           [DefineScoketProtocol requestProtocolAnalyWithData:data andResult:^(NSUInteger totalSize, PDataType dataType, NSData *inputData) {
               NSLog(@"数据已经接收完成");
               switch (dataType) {
                   case PDataTypePic:
                       NSLog(@"此次请求是上传图片 ");
                       [self saveImageWithData:data];
                       break;
                   case PDataTypeText:
                       NSLog(@"此次请求是上传文字");
                       [self handleTextWithData:data];
                       break;
                   default:
                       break;
               }
               //3. 处理完成,给客户端发出响应数据
               [self responseToClient:clientSocket andDataType:dataType];
           }];
                  
          //2. 下次还想接收数据 ,也需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
           [clientSocket readDataWithTimeout:-1 tag:0];
       }
              
       #pragma mark - defineDealWithData
       //处理文字
       -(void)handleTextWithData:(NSData *)data{
           // 1.转化字符串
           NSString *receviceStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
           NSLog(@"客户端发来的文本信息为: %@",receviceStr);
       }
       //处理图片
       -(void)saveImageWithData:(NSData *)data{
           // 1.生成图片路径
           NSString *imgPath = @"/Users/xyj/Desktop/img/xxxx.png";
           //2. 写入文件
           [data writeToFile:imgPath atomically:YES];
       }
              
       //服务端发送完成,要响应:  服务端自定义响应协议 4个字节总大小+4个字节的类型+ 4个字节的返回内容
       -(void)responseToClient:(GCDAsyncSocket *)clientSocket andDataType:(PDataType)dataType{
                  
           //1. 根据自定义的响应协议组装数据
         NSData *totalDataM  = [DefineScoketProtocol useResponseProtocolWithDataType:dataType];
           //2.服务端响应数据
           [clientSocket writeData:totalDataM withTimeout:-1 tag:0];
       }
       @end
              
       //main.m
       #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;
       }
      
      
Table of Contents