本期涉及到的库为 GCDAsyncSocket

以下就以我实际接触到的项目为例写出相关的代码 可能与其他案例有地方不相同

本文比较重要和容易踩坑的地方都会加粗 如果遇到问题 请多看几遍

serverSocket服务端

1.首先继承下GCDAsync的代理

GCDAsyncSocketDelegate

2.我们这里需要监听本地的一个随机端口 而不是固定端口 所以用了 self.serverSocket.localPort 当服务端的socket启动后

@property (nonatomic, strong) GCDAsyncSocket *serverSocket;

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("socket_delegate_queue", DISPATCH_QUEUE_SERIAL);
    self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue];
    NSError *error;
    // 监听自己服务端的端口
    if ([self.serverSocket acceptOnPort:self.serverSocket.localPort error:&error]) {
        NSLog(@"允许接收本地端口");
    } else {
        NSLog(@"连接失败");
        return;
    }

    NSLog(@"%d",self.serverSocket.localPort);
    [self.serverSocket readDataWithTimeout:-1 tag:0];
    NSLog(@"socket开始读取数据");

    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 60)];
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button]; 
}

当有客户端连接上服务端之后 可以打印出客户端的ip及服务端(生产模式注意删除掉) 我最开始一直失败 后面发现 需要用一个全局的对象去持有这个newSocket 防止它被提前释放 从而导致没办法维持连接状态 self.serverSocket = newSocket;

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {

  NSLog(@"收到链接:%@: %d, 本端的接口 %@ %d", newSocket.connectedHost, newSocket.connectedPort,newSocket.localHost,newSocket.localPort);

  self.serverSocket = newSocket;
  [self.serverSocket readDataWithTimeout:-1 tag:0];
  // 回复客户端
  NSString *responseString = @"我是服务端.你是谁啊";
  NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
  [self.serverSocket writeData:responseData withTimeout:-1 tag:0];
  // 解决数据黏包问题 防止读取不到正确的数据

//  [self.serverSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 maxLength:0 tag:0];

}

如果成功连接上 会打印类似如下信息,这是我测试时候的ip:端口号 具体还是以实际为准

收到链接:192.168.0.104: 56015, 本端的接口 192.168.0.101 54339

如果未进行对象持有 连接成功后会马上断开 且客户端会出现如下打印信息

2023-11-17 14:58:51.840617+0800 UDP测试[510:57465] 连接错误:(null)
2023-11-17 14:58:52.652426+0800 UDP测试[510:57493] [connection] nw_connection_copy_connected_path [C1] Client called nw_connection_copy_connected_path on unconnected nw_connection
2023-11-17 14:58:52.652724+0800 UDP测试[510:57493] [] tcp_connection_is_cellular No connected path
2023-11-17 14:58:52.653153+0800 UDP测试[510:57488] Client disconnected. Socket closed by remote peer

3.接下来就是消息的接收和发送了 都可以在这个代理方法里面实现

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"收到客户端内容: %@ 长度:%zd", message, message.length);

    // 判断收到的数据
    if (message == nil) {
        NSLog(@"接收的数据为空 不处理");
        return;
    }
    // 回复客户端
    NSString *responseString = @"我是服务端.";
    NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
    [self.serverSocket writeData:responseData withTimeout:-1 tag:0];
    [sock readDataWithTimeout:-1 tag:0];
}

4.连接断开之后会调用下面的方法

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err {
    NSLog(@"%@; error: %@", sock.connectedHost, err.localizedDescription);
}

如果数据需要分片发送 这里需要调用以后方法 否则可能出现数据黏包 无法正确读取需要的指定数据 当前另外也可以双端商量 对一块数据进行包装 用特征值包裹起来 其实跟下面这个方法实现原理差不多

// 解决数据黏包问题 防止读取不到正确的数据
[self.serverSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 maxLength:0 tag:0];

clientSocket客户端

@property (nonatomic, strong) GCDAsyncSocket *clientSocket;

连接的ip及端口都是服务端的 至于获取方式 可以通过其他连接方式或者接口获取

连接之后立马执行一下这个方法 才会触发权限弹窗 否则 第一次可能连接不成功

[self.clientSocket readDataWithTimeout:-1 tag:0]; 

1.开始初始化数据

- (void)startServer {
    NSError *error = nil;
    dispatch_queue_t queue = dispatch_queue_create("socket_delegate_queue", DISPATCH_QUEUE_SERIAL);
    self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:queue];
    [self.clientSocket connectToHost:@"192.168.0.101" onPort:51060 error:&error];
    [self.clientSocket readDataWithTimeout:-1 tag:0];

    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 60)];
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

2.发送消息的方法

- (void)click {
    // 回复客户端
    NSString *responseString = @"我是客户端";
    NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
    [self.clientSocket writeData:responseData withTimeout:-1 tag:0];

	// 继续读取下一条数据
    [self.clientSocket readDataWithTimeout:-1 tag:0];

}

3.客户端接收消息和发送消息其实和客户端基本是一致的

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *receivedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"收到服务端的消息 %@", receivedString);
    // 继续读取下一条数据
    [sock readDataWithTimeout:-1 tag:0];
    // 处理接收到的数据

    // 回复客户端
    NSString *responseString = @"我是客户端";
    NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
    [sock writeData:responseData withTimeout:-1 tag:0];
}

此方法是写入数据 默认超时时间为-1 表示不会超时 如果有需求 可以改成30S 或者其他的 tag是消息标识 可以在服务端数据接收方法里面获取 做对应场景的识别

[self.clientSocket writeData:responseData withTimeout:-1 tag:0];

此方法是读取消息标识 主要作用是告诉对方 我还会继续接收消息 所以在每次发完消息之后 都需要调一下这个方法 否则后面可能会出现 无法继续接收或者发送消息的问题

 [self.clientSocket readDataWithTimeout:-1 tag:0];

出现以下提供 标识客户端连接服务端超时 需要检查ip及服务端端口是否正确

Client disconnected. Operation timed out

本项目涉及到的权限已截图 部分权限可能不需求 可自行进行测试删减

image-20231117114619988.png

如果连接不上 请在两台测试机上 运行一次客户端的版本 触发一下弹窗的权限 否则服务端这边的测试机 无法正确开启局域网的权限 导致客户端连接不上,总而言之,测试的时候需要两边都要触发下本地网络权限的权限并进行权限确认。