基於 GCDAsyncSocket,簡單實現類似《你猜我畫》的 socket 數據傳輸 ...
一、前言
- Socket
- Socket 是對 TCP/IP 協議的封裝,其中IP協議對應為網路層,TCP 協議對應為傳輸層,而我們常用的HTTP協議,是位於應用層,在七層模型中HTTP協議是基於 TCP/IP 的,我們想要使用 TCP/IP 協議,則要通過 Socket
- Socket 編程用途(其他待補充)
- 長連接
- 端到端的即時通訊
- Socket 和 Http(來源網路)
- socket 一般用於比較即時的通信和實時性較高的情況,比如推送,聊天,保持心跳長連接等,http 一般用於實時性要求不那麼高的情況,比如信息反饋,圖片上傳,獲取新聞信息等。
二、類似《你猜我畫》簡易效果說明
效果(分別是模擬器和手機截圖)
- 工作中碰到類似需求,但沒找到類似的成熟的第三方框架,只有先看看原理性的東西了。其實也就基於 socket 即時傳輸圖片數據、筆畫數據,還有聊天文字,也可以拓展做其他的指令控制
- 沒有做註冊登錄,沒有做用戶管理,只是簡單原理性的探討
基於 GCDAsyncSocket 框架進行,關於 GCDAsyncSocket 的介紹可自行瞭解
三、服務端部分代碼
- 直接用 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; }
四、移動端部分代碼
- 基於 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];
}
- 筆畫數據的接受
- 需要轉換坐標,解析自定義傳輸的數據格式
// 1、接受坐標點
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];
五、小demo地址
https://github.com/HOWIE-CH/-You-guess-I-painted-_socket.git
六、問題
- 定義了圖片文件二進位數據、筆畫路徑二進位數據、聊天字元串二進位數據,三種格式的二進位數據,在 GCDAsyncSocket 接受數據的代理方法,需要判斷接受的二進位文件的類型再進行解析,如果有更好的方式可留言。
- 只是簡單的功能的嘗試,有時存在畫的一條線過長就傳輸不過去的情況,存在圖片偶爾傳輸不完整的情況
- 不清楚是否有相關成熟的框架,如果有,請留言。