基於 GCDAsyncSocket,簡單實現相似《你猜我畫》的 socket 數據傳輸

1、前言

  • Socket
    • Socket 是對 TCP/IP 協議的封裝,其中IP協議對應爲網絡層,TCP 協議對應爲傳輸層,而咱們經常使用的HTTP協議,是位於應用層,在七層模型中HTTP協議是基於 TCP/IP 的,咱們想要使用 TCP/IP 協議,則要經過 Socket
  • Socket 編程用途(其餘待補充)
    • 長鏈接
    • 端到端的即時通信
  • Socket 和 Http(來源網絡)
    • socket 通常用於比較即時的通訊和實時性較高的狀況,好比推送,聊天,保持心跳長鏈接等,http 通常用於實時性要求不那麼高的狀況,好比信息反饋,圖片上傳,獲取新聞信息等。

2、相似《你猜我畫》簡易效果說明

  • 效果(分別是模擬器和手機截圖)

    git

  • 工做中碰到相似需求,但沒找到相似的成熟的第三方框架,只有先看看原理性的東西了。其實也就基於 socket 即時傳輸圖片數據、筆畫數據,還有聊天文字,也能夠拓展作其餘的指令控制
  • 沒有作註冊登陸,沒有作用戶管理,只是簡單原理性的探討
  • 基於 GCDAsyncSocket 框架進行,關於 GCDAsyncSocket 的介紹可自行了解github

3、服務端部分代碼

  • 直接用 mac 程序做爲服務端
    • Server 類
/*!
 @method  開啓服務
 @abstract 開啓服務器 TCP 鏈接服務
 */
- (void)startServer {

    self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self
                                                    delegateQueue:dispatch_get_main_queue()];
                                                    NSError *error = nil;
    [self.serverSocket acceptOnPort:5555
                              error:&error];
    if (error) {
        NSLog(@"服務開啓失敗");
    } else {
        NSLog(@"服務開啓成功");
    }

}
#pragma mark - GCDAsyncSocketDelegate
/*!
 @method  收到socket端鏈接回調
 @abstract 服務器收到socket端鏈接回調
 */
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
    [self.clientSocketArray addObject:newSocket];
    [newSocket readDataWithTimeout:-1
                               tag:self.clientSocketArray.count];
}
/*!
 @method  收到socket端數據的回調
 @abstract 服務器收到socket端數據的回調
 */
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    // 直接進行轉發數據
    for (GCDAsyncSocket *clientSocket in self.clientSocketArray) {
        if (sock != clientSocket) {

            [clientSocket writeData:data
                withTimeout:-1
                        tag:0];
        }
    }
    [sock readDataWithTimeout:-1
                          tag:0];

}
  • main 中
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Server *chatServer = [[Server alloc]init];
        [chatServer startServer];
        // 開啓主運行循環
        [[NSRunLoop mainRunLoop] run];
    }
    return 0;
}

4、移動端部分代碼

  • 基於 GCDAsyncSocket 的接受數據代理方法及發送數據方法
  • 圖片數據的發送
// 回調 發送圖片
    __weak typeof(self) weakSelf = self;
    bgImgView.block = ^(UIImage *img) {

        weakSelf.drawView.drawImg = img;
        // image
        NSData *imgData = UIImageJPEGRepresentation(weakSelf.drawView.drawImg, 0.2);
        NSMutableData *dat = [NSMutableData data];
        [dat appendData:imgData];
        // 拼接二進制數據流的結束符
        NSData *endData = [@"$" dataUsingEncoding:NSUTF8StringEncoding];
        [dat appendData:endData];
        // 發送數據
        [weakSelf.clientSocket writeData:dat
                         withTimeout:-1
                                 tag:111111];

    };
  • 圖片二進制數據的傳輸是基於流的,一段一段的,避免斷包缺包等問題,須要拼接結束符,圖片數據結束
  • 圖片數據的接受接受
// 拼接數據 轉成圖片

       [self.socketReadData appendData:data];

       NSData *endData = [data subdataWithRange:NSMakeRange(data.length -1, 1)];

       NSString *end= [[NSString alloc] initWithData:endData
                                            encoding:NSUTF8StringEncoding];

       if ([end isEqualToString:@"$"]) {

           UIImage *tmpImg = [UIImage imageWithData:self.socketReadData];

           self.drawView.drawImg = tmpImg;

           [self.drawView setNeedsDisplay];

           [self.clientSocket readDataWithTimeout:-1
                                              tag:111111];
           // 拼完圖片 恢復默認
           self.socketReadData = nil;

       }
  • 畫布筆畫數據的傳輸
    • 由於傳輸的是二進制數據,因此採起將貝塞爾曲線轉換成 CGPoint 座標數組,再加上線寬和線的顏色,最後組成一個字典,轉換爲二進制進行傳輸
    • 考慮到座標點在不一樣屏幕上須要適配,所以須要把當前手機端的屏幕高寬一塊兒傳輸
/*!
 @method  發送路徑
 @abstract 經過socket 發送路徑信息
 */
- (void)sendPath {
    // path 座標點及 轉換
    NSArray *points = [(UIBezierPath *)self.dataModel.path points];
    NSMutableArray *tmp = [NSMutableArray array];
    for (id value in points) {
        CGPoint point = [value CGPointValue];
        NSDictionary *dic = @{@"x" : @(point.x), @"y": @(point.y)};
        [tmp addObject:dic];
    }

    // 顏色類別
    NSInteger colorNum = 0;

    if (CGColorEqualToColor(self.drawView.color.CGColor, [UIColor redColor].CGColor)) {
        colorNum = 1;
    }
    else  if (CGColorEqualToColor(self.drawView.color.CGColor, [UIColor blueColor].CGColor)  ){

        colorNum = 2;
    } else if (CGColorEqualToColor(self.drawView.color.CGColor, [UIColor greenColor].CGColor)  ) {
        colorNum = 3;
    }


    // 傳遞數據格式
    NSDictionary *pathDataDict = @{
                                   @"path" : tmp,
                                   @"width" : @(self.drawView.width),
                                   @"color" : @(colorNum),
                                   @"screenW": @([UIScreen mainScreen].bounds.size.width),
                                   @"screenH": @([UIScreen mainScreen].bounds.size.height)
                                   };

    NSData *pathData = [NSJSONSerialization
                        dataWithJSONObject:pathDataDict
                        options:NSJSONWritingPrettyPrinted
                        error:nil];


    [self.clientSocket writeData:pathData
                     withTimeout:-1
                             tag:111111];
}
  • 筆畫數據的接受
    • 須要轉換座標,解析自定義傳輸的數據格式
// 一、接受座標點
       NSInteger w = [tmpDict[@"screenW"] integerValue];
       NSInteger h = [tmpDict[@"screenH"] integerValue];
       CGFloat scaleW = [UIScreen mainScreen].bounds.size.width / w;
       CGFloat scaleH = [UIScreen mainScreen].bounds.size.height / h;
       // 處理點
       NSArray *pointDict = tmpDict[@"path"];
       DIYBezierPath *path = [[DIYBezierPath alloc]init];
       for (NSDictionary *tmpDict in pointDict) {
           CGPoint point = CGPointMake([tmpDict[@"x"] floatValue] * scaleW, [tmpDict[@"y"] floatValue] * scaleH);
           NSInteger index = [pointDict indexOfObject:tmpDict];
           if (index == 0) {
               [path moveToPoint:point];
           } else {
               [path addLineToPoint:point];
           }

       }
       switch ([tmpDict[@"color"] integerValue]) {
           case 0:
               self.drawView.color = [UIColor blackColor];
               break;
           case 1:
               self.drawView.color = [UIColor redColor];
               break;
           case 2:
               self.drawView.color = [UIColor blueColor];
               break;
           case 3:
               self.drawView.color = [UIColor greenColor];
               break;

           default:
               break;
       }
       self.drawView.width = [tmpDict[@"width"] floatValue];
       self.drawView.currentPath = path;
       self.drawView.currentPath.pathColor = self.drawView.color;
       self.drawView.currentPath.lineWidth = self.drawView.width;
       [self.drawView.pathArray addObject:path];
       [self.drawView setNeedsDisplay];

5、小demo地址

https://github.com/HOWIE-CH/-You-guess-I-painted-_socket.gitweb

6、問題

  • 定義了圖片文件二進制數據、筆畫路徑二進制數據、聊天字符串二進制數據,三種格式的二進制數據,在 GCDAsyncSocket 接受數據的代理方法,須要判斷接受的二進制文件的類型再進行解析,若是有更好的方式可留言。
  • 只是簡單的功能的嘗試,有時存在畫的一條線過長就傳輸不過去的狀況,存在圖片偶爾傳輸不完整的狀況
  • 不清楚是否有相關成熟的框架,若是有,請留言。
  • 最近試過服務端是 NodeJs 用 socket.io 的話,iOS 用 GCDAsyncSocket,感受這樣是通信不了的。像這樣要實現 Android、iOS 跨平臺 socket 傳輸數據,那 socket 選擇什麼框架呢,服務端選擇什麼 socket 框架? 以前即時通信都是 XMPP ,如今貌似是 webSocket socket.io 了。
相關文章
相關標籤/搜索