这片文章主要包括两个技术点:
- 服务端和客户端示例(GCDAsyncSocket应用)
-
Socket自定义数据格式传输协议
利用GCDAsyncSocket写一个服务端的小案例
1.把GCDAsyncSocket.h/GCDAsyncSocket.m两个文件导入到项目中就可以了
2.案例分析流程
- 1.服务器绑定端口
- 2.监听客户端的连接
- 3.允许客户端建立连接
- 4.读取客户端的请求数据
- 5.处理客户端的请求数据
- 6.响应客户端的请求数据
3.服务是有开启和关闭的两个状态
4.接收客户端的请求数据时的注意点
- 接收客户端的请求数据的代理方法是
-(void)socket: didReadData:withTag: - 要使用这个代理方法读取数据前,需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
- 下次还想接收数据 ,也需要调用一个方法 客户端的 readDataWithTimeout:tag:方法
#import "ServerListener.h"
#import "GCDAsyncSocket.h"
@interface ServerListener ()<GCDAsyncSocketDelegate>
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;
@property (strong, nonatomic) NSMutableArray *socketMarray;
@end
@implementation ServerListener
- (NSMutableArray *)socketMarray{
if (_socketMarray == nil) {
_socketMarray = [NSMutableArray array];
}
return _socketMarray;
}
- (void)start{
// 1.服务器绑定端口
//1>创建服务对象,绑定端口
self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
NSError *error = nil;
[self.serverSocket acceptOnPort:5288 error:&error];
if (error) {
NSLog(@"开启失败");
}else{
NSLog(@"开启成功");
}
}
//2.监听客户端的连接
//代理方法
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
NSLog(@"有链接");
//3.允许客户端建立连接(防止释放,创建持久连接)
[self.socketMarray addObject:newSocket];
/*
* -1代表没有超时约束 0用不到
*/
[newSocket readDataWithTimeout:-1 tag:0];
}
//3.允许客户端建立连接
//代理方法
#warning 使用这个代理之前必须调用一个方法(客户端方法)
//sock readDataWithTimeout:<#(NSTimeInterval)#> tag:<#(long)#>
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
// 4.读取客户端的请求数据
NSString *dataStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
#warning 想接受多次还要调用这方法
[sock readDataWithTimeout:-1 tag:0];
//5.处理客户端的请求数据
//6.响应客户端的请求数据
NSString *huida = @"我是回答\n";
[sock writeData:[huida dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
if ([dataStr isEqualToString:@"esc\r\n"]) {
[self.socketMarray removeObject:sock];
}
}
- (void)stop{
}
-----------------------------------------------
利用GCDAsyncSocket写一个客户端的小案例
Socket的客户端编程
- 1.连接到服务器(IP+Port)
- 2.监听连接服务器是否成功
- 3.如果连接成功,就可发送消息给服务器
- 4.监听服务器转发过来的消息
(其中在接受服务端发来数据刷新的时候需要在主线程刷新,因为接收是在子线程里接收)
#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
//1.连接到服务器(IP+Port)
//2.监听连接服务器是否成功
//3.如果连接成功,就可发送消息给服务器
//4.监听服务器转发过来的消息
//5.发送时,刷新表格显示数据
//6.接收聊天消息时,刷新表格显示数据
@implementation ViewController
-(NSMutableArray *)messages{
if(!_messages){
_messages = [NSMutableArray array];
}
return _messages;
}
//1.连接到服务器(IP+Port)
- (IBAction)connectToHostAction:(id)sender {
//创建一个Socket对象
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
//连接服务器
NSError *err = nil;
[self.clientSocket connectToHost:@"192.168.8.111" onPort:5288 error:&err];
//err实际上没有什么太大的作用
if(!err){
NSLog(@"连接请求发送成功");
}else{
NSLog(@"连接请求发送失败");
}
}
//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);
}
//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];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
//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 表格数据源方法
-(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通信报文要保留前8个字节 是前四位指定文件总大小,后四位是文件传输格式,除了定义请求头,还要定义响应!!!!
其实主要思路就是根据下图所示,传输的时候一块一块向下扒,在三方监听方法里监听data
第一次扒文件大小
第二层扒文件格式
-
第三次扒文件主体
// 把图片转在NSData
UIImage *img = [UIImage imageNamed:@"IMG_2427.JPG"];
NSData *imgData = UIImagePNGRepresentation(img);
//定义数据格式传输协议
//请求头和请求体 / 响应头和响应体
NSMutableData *totalDataM = [NSMutableData data];
// 1.拼接长度(0~3:长度)
unsigned int totalSize = 4 + 4 + (int)imgData.length;
NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
[totalDataM appendData:totalSizeData];
// 2.拼接指令类型(4~7:指令) 这里就是请求头,用于判断你的床底数据类型
// 0x00000001 = 图片
// 0x00000002 = 文字
// 0x00000003 = 位置
// 0x00000004 = 语音
unsigned int commandId = 0x00000001;
NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
[totalDataM appendData:commandIdData];
// 3.拼接图片(8~N) 图片数据
[totalDataM appendData:imgData];
// 发数据
[self.clientSocket writeData:totalDataM withTimeout:-1 tag:0];
完整代码
#import "ViewController.h"
#import "GCDAsyncSocket.h"
@interface ViewController ()<GCDAsyncSocketDelegate>
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
/** 客户端的Socket */
@property (nonatomic ,strong) GCDAsyncSocket *clientSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
//与服务器连接
- (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:@"127.0.0.1" onPort:5288 error:&error];
if (error) {
NSLog(@"%@",error);
}
}
// 与服务器连接成功
-(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];
}
// 与服务器断开
- (IBAction)closeToHost:(id)sender {
[self.clientSocket disconnect];
}
// 发图片
- (IBAction)sendImag:(id)sender {
// 把图片转在NSData
UIImage *img = [UIImage imageNamed:@"IMG_2427.JPG"];
NSData *imgData = UIImagePNGRepresentation(img);
//定义数据格式传输协议
//请求头和请求体 / 响应头和响应体
NSMutableData *totalDataM = [NSMutableData data];
// 1.拼接长度(0~3:长度)
unsigned int totalSize = 4 + 4 + (int)imgData.length;
NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
[totalDataM appendData:totalSizeData];
// 2.拼接指令类型(4~7:指令)
// 0x00000001 = 图片
// 0x00000002 = 文字
// 0x00000003 = 位置
// 0x00000004 = 语音
unsigned int commandId = 0x00000001;
NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
[totalDataM appendData:commandIdData];
// 3.拼接图片(8~N) 图片数据
[totalDataM appendData:imgData];
NSLog(@"图片的字节大小:%ld",imgData.length);
NSLog(@"发送数据的总字节大小:%ld",totalDataM.length);
// 发数据
[self.clientSocket writeData:totalDataM withTimeout:-1 tag:0];
}
- (IBAction)sendText:(id)sender {
NSString *text = @"Hello,自定义协议";
NSData *textData = [text dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *totalDataM = [NSMutableData data];
// 1.拼接长度(0~3:长度)
unsigned int totalSize = 4 + 4 + (int)textData.length;
NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
[totalDataM appendData:totalSizeData];
// 2.拼接指令类型(4~7:指令)
// 0x00000001 = 图片
// 0x00000002 = 文字
// 0x00000003 = 位置
// 0x00000004 = 语音
unsigned int commandId = 0x00000002;
NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
[totalDataM appendData:commandIdData];
// 3.拼接(8~N) 文字数据
[totalDataM appendData:textData];
//发送
[self.clientSocket writeData:totalDataM withTimeout:-1 tag:0];
}
// 接收服务器响应的数据
-(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
/**
* 解析服务器返回的数据
*/
// 获取总的数据包大小
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
NSLog(@"响应总数据的大小 %u",totalSize);
// 获取指令类型
NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
unsigned int commandId = 0;
[commandIdData getBytes:&commandId length:4];
// 结果
NSData *resultData = [data subdataWithRange:NSMakeRange(8, 4)];
unsigned int result = 0;
[resultData getBytes:&result length:4];
NSMutableString *str = [NSMutableString string];
if (commandId == 0x00000001) {//图片
[str appendString:@"图片 "];
}else if(commandId == 0x00000002){//文字
[str appendString:@"文字 "];
}
if(result == 1){
[str appendString:@"上传成功"];
}else{
[str appendString:@"上传失败"];
}
NSLog(@"%@",str);
#warning 可以接收下一次数据
[clientSocket readDataWithTimeout:-1 tag:0];
}
自定义协议服务端
#import "ServerListener.h"
#import "GCDAsyncSocket.h"
#define kCommandId_Img 0x00000001// = 图片
#define kCommandId_Txt 0x00000002// = 文字
#define kCommandId_Location 0x00000003// = 位置
#define kCommandId_Voice 0x00000004// = 语音
@interface ServerListener()<GCDAsyncSocketDelegate>
/** 服务端的Socket对象 */
@property (nonatomic ,strong) GCDAsyncSocket *serverSocket;
/** 客户端的Socket数组 */
@property (nonatomic ,strong) NSMutableArray *clientSockets;
/** <#注释#> */
@property (nonatomic ,strong) NSMutableData *dataM;
/** 总的数据包大小*/
@property(nonatomic ,assign)unsigned int totalSize;
/**当前的指令类型*/
@property(nonatomic ,assign)unsigned int currentCommandId;
@end
@implementation ServerListener
-(NSMutableData *)dataM{
if (!_dataM) {
_dataM = [NSMutableData data];
}
return _dataM;
}
-(NSMutableArray *)clientSockets{
if (!_clientSockets) {
_clientSockets = [NSMutableArray array];
}
return _clientSockets;
}
-(void)start{
// 1.创建一个服务端的Socket对象
self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
// 2.绑定端口监听
NSError *error = nil;
#warning 失败原因:端口号被占用
BOOL success = [self.serverSocket acceptOnPort:5288 error:&error];
if (success == YES) {
NSLog(@"服务开启成功");
}else{
NSLog(@"服务开启失败");
}
}
// 新的socket客户端连接
-(void)socket:(GCDAsyncSocket *)serverSocket didAcceptNewSocket:(GCDAsyncSocket *)clientSocket{
NSLog(@"当前客户端的IP:%@ 端口号%d",clientSocket.connectedHost,clientSocket.connectedPort);
// 1.存储客户端面的Socket对象
[self.clientSockets addObject:clientSocket];
NSLog(@"当前有%ld个客户端连接",self.clientSockets.count);
// 2.客户连接建立后,设置可以读取数据
[clientSocket readDataWithTimeout:-1 tag:0];
}
// 读取客户端面提交数据
-(void)socket:(GCDAsyncSocket *)clientSocket didReadData:(NSData *)data withTag:(long)tag{
// 1.第一次接收数据
if(self.dataM.length == 0){
// 获取总的数据包大小
NSData *totalSizeData = [data subdataWithRange:NSMakeRange(0, 4)];
unsigned int totalSize = 0;
[totalSizeData getBytes:&totalSize length:4];
NSLog(@"接收总数据的大小 %u",totalSize);
self.totalSize = totalSize;
// 获取指令类型
NSData *commandIdData = [data subdataWithRange:NSMakeRange(4, 4)];
unsigned int commandId = 0;
[commandIdData getBytes:&commandId length:4];
self.currentCommandId = commandId;
switch (commandId) {
case kCommandId_Img:
NSLog(@"此次请求是上传图片 ");
break;
case kCommandId_Txt:
NSLog(@"此次请求是上传文字");
break;
default:
break;
}
}
// 2.拼接二进度
[self.dataM appendData:data];
// 3.图片数据已经接收完成
// 判断当前是否是图片数据的最后一段?
NSLog(@"此次接收的数据包大小 %ld",data.length);
if (self.dataM.length == self.totalSize) {
NSLog(@"数据已经接收完成");
if (self.currentCommandId == kCommandId_Img) {//图片
[self saveImage];
}else if(self.currentCommandId == kCommandId_Txt){//文字
[self handleText];
}
// 响应客户端
[self responseToClient:clientSocket];
}
#warning 接收下一次的数据
[clientSocket readDataWithTimeout:-1 tag:0];
}
-(void)responseToClient:(GCDAsyncSocket *)clientSocket{
//1.总的字节长度
unsigned int totalSize = 4 + 4 + 4;
NSData *totalSizeData = [NSData dataWithBytes:&totalSize length:4];
//2.响应指令类型
unsigned int commandId = self.currentCommandId;
NSData *commandIdData = [NSData dataWithBytes:&commandId length:4];
//3.上传的结果 //1:上传成功 0://上传失败
unsigned int result = 0;
NSData *resultData = [NSData dataWithBytes:&result length:4];
NSMutableData *totalData = [NSMutableData data];
[totalData appendData:totalSizeData];
[totalData appendData:commandIdData];
[totalData appendData:resultData];
// 写入
[clientSocket writeData:totalData withTimeout:-1 tag:0];
}
-(void)handleText{
// 1.截取文字NSData
NSData *txtData = [self.dataM subdataWithRange:NSMakeRange(8, self.dataM .length - 8)];
// 2.转化字符串
NSString *receviceStr = [[NSString alloc] initWithData:txtData encoding:NSUTF8StringEncoding];
NSLog(@"%@",receviceStr);
// 重新赋值
self.dataM = [NSMutableData data];
}
-(void)saveImage{
// 4.把图片写入一个文件
// 4.1获取图片的NSData
NSData *imgData = [self.dataM subdataWithRange:NSMakeRange(8, self.dataM.length - 8)];
// 4.2生成图片路径
NSString *imgPath = @"/Users/a1/Desktop/img/xxxx.png";
// 写入文件
[imgData writeToFile:imgPath atomically:YES];
// 清除数据
self.dataM = [NSMutableData data];
}