「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
iOS进程通信
说到iOS进程之间的通信,大家能够想到有哪些方式呢?
在这里,我总结了以下几种常用的方式:
- APP Group
- UIPasteBoard
- URL scheme
- LocalSocket
- Keychain
看到这里,是不是觉得有些方式很熟悉,比方说:UIPasteBoard
、URL scheme
常常做一些跨应用之前的处理(从WX粘贴分享文案到某宝、某多、某东等),APP Group
是苹果用来解决应用之间数据传输(在前面屏幕共享这篇文章有讲过),而LocalSocket
这种方式基本上很少用到,那么本篇文章我们就讲讲怎么使用LocalSocket。
LocalSocket
首先我们需要知道客户端、服务端的区别;
- 服务端:需要在本地端口进行TCP的绑定、监听
- 客户端:获取服务端同一个端口,进行连接
当两端建立连接后,接着就可以去发送消息了;
这里还是以Broadcast Upload Extension
为例;(使用OC三方库GCDAsyncSocket
)
我们需要将系统屏幕共享的数据回传给主应用,所以在主程序中,我们是以服务端去开启socket,在扩展程序中,我们是以客户端去连接socket,并向主程序发出通信。
MainApp 中 socket 的设置
在主程序中,设置socket的端口号、地址。
开启socket服务
// 开启socket服务
- (void)startSocketService {
if (self.serverSocket != nil) {
return;
}
self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
self.serverSocket.autoDisconnectOnClosedReadStream = YES;
__autoreleasing NSError *error = nil;
BOOL result = [self.serverSocket acceptOnInterface:@"localhost" port:8080 error:&error];
if(error || !result){
NSLog(@"server socket result:%@ error:%@", error, @(result));
}else {
NSLog(@"server socket:%@ listen", self.serverSocket);
}
}
停止socket服务
// 停止socket服务
- (void)stopSocketService {
if (self.serverSocket != nil) {
[self.serverSocket disconnect];
}
self.serverSocket = nil;
}
Extension 中 socket 的设置
在扩展程序中连接socket,并发送数据包。
连接socket
// 连接socket
- (void)socketConnect {
if (self.videoSocket != nil) {
self.videoSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:[self.class SEND_VIDEO_SERIAL_QUEUE]];
}
if (!self.videoSocket.isConnected) {
NSError *error;
[self.videoSocket connectToHost:@"localHost" onPort:8080 error:&error];
[self.videoSocket readDataWithTimeout:-1 tag:10086];
}
}
断开socket
// 断开socket
- (void)socketDisConnect {
[self.videoSocket disconnect];
self.videoSocket = nil;
}
socket写入数据
// 发送数据
- (void)sendVideoSampleBuffer:(nonnull CMSampleBufferRef)sampleBuffer {
CFTimeInterval currentTime = CACurrentMediaTime();
if (currentTime - self.lastTimeInterval < self.frameInterval) {
return;
}
self.lastTimeInterval = currentTime;
@autoreleasepool {
NSNumber *orientation = nil;
if (@available(iOS 11.0, *)) {
CFStringRef orientationKey = (__bridge CFStringRef)RPVideoSampleOrientationKey;
orientation = (NSNumber *)CMGetAttachment(sampleBuffer,orientationKey,NULL);
}
size_t length = 0;
void *sendData = [TScreenShareBroadcasterTool sampleBufferToData:sampleBuffer targetSize:self.options.targetFrameSize orientation:orientation length:&length];
dispatch_async([[self class] SEND_VIDEO_SERIAL_QUEUE], ^{
NSData *data = [NSData dataWithBytes:sendData length:length];
[self.videoSocket writeData:data withTimeout:5 tag:10086];
free(sendData);
});
}
}
socket回调
#pragma mark - GCDAsyncSocketDelegate
//已经连接到服务器
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(nonnull NSString *)host port:(uint16_t)port{
NSLog(@"连接成功 : %@---%d",host,port);
//连接成功或者收到消息,必须开始read,否则将无法收到消息,
//不read的话,缓存区将会被关闭
// -1 表示无限时长 ,永久不失效
[sock readDataWithTimeout:-1 tag:10086];
}
// 连接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"断开 socket连接 原因:%@",err);
[self.videoSocket disconnect];
self.videoSocket = nil;
[self socketConnect];
}
//已经接收服务器返回来的数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"接收到tag = %ld : %ld 长度的数据",tag,data.length);
//连接成功或者收到消息,必须开始read,否则将无法收到消息
//不read的话,缓存区将会被关闭
// -1 表示无限时长 , tag
[sock readDataWithTimeout:-1 tag:10086];
}
//消息发送成功 代理函数 向服务器 发送消息
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"%ld 发送数据成功",tag);
}
使用socket一般都需要处理粘包的情况。目前粘包解决方案基本上都是通用的。
粘包解决方案
一般解决方案就是将数据包分为两部分:Header
+ Data
;
`Header`: 包含`Data`的length,有自己固定的length;
`Data`: 实际的数据包;
Header
中包含Data
长度,这样接收到数据后,通过读取Header
的长度字段,便知道每一个Data
的实际长度,然后根据实际的长度去读取对应的长度的数据,这样便可以获取正确的数据;具体做法如下:
- 封包:给每个包添加包头,包头包含数据的长度
- 拆包:将数据包塞入环形缓冲区,比较缓存区的长度是否大于总长度,大于则说明
Data
完整,此时取出Header
获取数据的长度,并根据长度取出正确的数据,处理完后将处理后的数据移出缓冲区;
写在最后
我们应该都知道socket
是实时的传输数据,虽然有可能存在丢包的情况,但是如果我们需要使用到实时性较高的场景,使用socket
无疑是一种不错的选择。