socket作为通讯的基础,在很多场景中经常会被用到,通过本篇文章,我们把把客户端、服务端一一进行搭建。(本文基于cocoasyncsocket)
首先搭建服务端,服务端代码封装如下,注释已经完善,文章不再单独针对代码进行解析:
#import "FELIXSocketServer.h"
@interface FELIXSocketServer()
//clients用来保存连接到服务端的socket
@property (nonatomic,strong) NSMutableArray *clients;
//用来缓存socket传输的数据
@property (nonatomic,strong) NSMutableArray *clientsData;
//创建一个提供客户端进行连接的socket
@property (nonatomic,strong) GCDAsyncSocket *socket;
//这个是ReactiveObjc的一个类,用来进行各种状态的报告
//顺便强烈推荐一下ReactiveObjc,越用越优雅
//下面的代码我把这个涉及到RACSubject的代码删掉了
@property (nonatomic,strong) RACSubject *noticeSubject;
@end
@implementation FELIXSocketServer
//创建服务端的单例
+ (instancetype)sharedServer
{
static dispatch_once_t onceToken;
static FELIXSocketServer *server;
dispatch_once(&onceToken, ^{
server = [[FELIXSocketServer alloc] init];
//初始化服务器
[server initServer];
});
return server;
}
- (void)initServer
{
//创建socket对象
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
//初始化clients
_clients = [NSMutableArray array];
_clientsData = [NSMutableArray array];
}
//调用startServer用来启动socket服务器
- (void)startServer
{
NSError *err = nil;
//这里的kPort是一个宏定义,写的时候可以根据自己的需要定义一个端口用来监听
[_socket acceptOnPort:kPort error:&err];
if (err)
{
NSLog(@"服务器启动失败%@",err);
}else{
NSLog(@"服务器启动成功,正在监听%d端口",kPort);
}
}
- (void)sendData:(NSData *)data toClient:(GCDAsyncSocket *)sock
{
//为了解决粘包的问题,为data添加4个字节的头,值为data的长度
unsigned long length = data.length;
//给每一个要发送的数据添加4个字节的头,标明要发送数据的长度
NSMutableData *packageData = [NSMutableData dataWithBytes:&length length:sizeof(unsigned long)];
[packageData appendData:data];
[sock writeData:packageData withTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
//这里要注意,对于连接到服务器的客户端socket必须进行持有,否则客户端会出现连接上就断开的情况
[self.clients addObject:newSocket];
//为每一个客户端创建数据缓存池
[self.clientsData addObject:[NSMutableData data]];
[newSocket readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
//客户端断开连接后清除对应的数据缓存池
[self.clientsData removeObjectAtIndex:[self.clients indexOfObject:sock]];
//客户端断开连接后清除服务端保存的socket
[self.clients removeObject:sock];
NSLog(@"%@--%@",sock,self.clients);
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
//获取对应socket的数据缓存池
NSMutableData *dataPool = self.clientsData[[self.clients indexOfObject:sock]];
//向缓存池里面添加数据
[dataPool appendData:data];
//查看缓存池里面的数据是否大于约定的数据头的长度
if (dataPool.length >= sizeof(unsigned long))
{
//从包头读取待处理数据的长度
int length;
[dataPool getBytes:&length range:NSMakeRange(0, sizeof(unsigned long))];
//判断待处理的数据长度是否已经全部缓存到缓存池中
if (length <= dataPool.length - sizeof(unsigned long))
{
NSData *dataToBeProcess = [dataPool subdataWithRange:NSMakeRange(sizeof(unsigned long), length)];
//在这里处理自己的数据
/**
处理自己的数据
*/
}
//清空已经收到的数据,释放内存
dataPool = [NSMutableData dataWithData:[dataPool subdataWithRange:NSMakeRange(sizeof(unsigned long) + length, dataPool.length - sizeof(unsigned long) - length)]];
//注意,接下来需要再次判断数据缓存池中的数据是否有多余的,如果有,一定要反复处理完,这里没有进行接下来的处理
}
}
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"服务端发送消息成功");
}
这里针对上面代码中的数据包装分析一下,因为网络状况的不稳定性,数据包经常会发生粘包的情况,所
以上面把每个数据包的包头增加了4个字节,内容为数据的总长度,以便客户端收到数据后,采用与服务端
同样的组包方式进行拆包,解决粘包的问题,同样,服务端收到数据后也采用这种方式进行拆包;
服务端的代码就是上面的,接下来看客户端的代码:
注:下面的所有RACSubject相关的东西,可以使用代理或者block进行回传给需要数据的页面。这里的RACSubject并没有使用对应页面的逻辑进行初始化(一般需要在对应的页面做初始化),而是直接在这里进行初始化。
先上.h文件的代码
#import
//定义通知的类型
typedef NS_ENUM(int,NOTICETYPE){
NOTICETYPE_CONNECT_SUCCESS = 1,
NOTICETYPE_CONNECT_ERROR,
NOTICETYPE_DISCONNECT,
};
@interface FELIXSocketClient : NSObject
//收到消息的subject,用于对外传递收到的消息
@property (nonatomic,strong) RACSubject *msgSubject;
+ (instancetype)sharedClient;
- (void)sendData:(NSData *)data;
- (void)connect;
@end
下面是.m文件的
#import "FELIXSocketClient.h"
@interface FELIXSocketClient()
//客户端的socket
@property (nonatomic,strong) GCDAsyncSocket *socket;
//客户端的连接状态
@property (nonatomic,assign) BOOL connected;
//这个是ReactiveObjc的一个类,用来进行各种状态的报告
//顺便强烈推荐一下ReactiveObjc,越用越优雅
//下面的代码我把这个涉及到RACSubject的代码删掉了
@property (nonatomic,strong) RACSubject *noticeSubject;
//客户端的数据缓存池
@property (nonatomic,strong) NSMutableData *dataPool;
@end
@implementation FELIXSocketClient
+ (instancetype)sharedClient
{
static dispatch_once_t onceToken;
static FELIXSocketClient *client;
dispatch_once(&onceToken, ^{
client = [[FELIXSocketClient alloc] init];
[client initClient];
});
return client;
}
- (void)initClient
{
//初始化socket
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
//初始化数据缓存池
self.dataPool = [NSMutableData data];
}
//采用单例的形式,这样在使用的时候直接调用单例然后发送数据就行了,不用再去进行连接什么的
- (void)sendData:(NSData *)data
{
//为data添加4个字节的头,值为data的长度,解决粘包的问题
unsigned long length = data.length;
NSMutableData *packageData = [NSMutableData dataWithBytes:&length length:sizeof(unsigned long)];
[packageData appendData:data];
//如果处于连接状态,直接发送数据
if (_connected)
{
[_socket writeData:packageData withTimeout:-1 tag:0];
}else{
//如果不处于连接状态,先进行连接,连接成功之后把数据发送出去
[self connect];
@weakify(self)//防止循环引用,先弱引用一下
[self.noticeSubject subscribeNext:^(id _Nullable x) {
@strongify(self)//与上面的weakify对应
//这里是连接成功的话就把刚才的数据发送出去
[x intValue] == NOTICETYPE_CONNECT_SUCCESS ? [self.socket writeData:packageData withTimeout:-1 tag:0] : nil;
}];
}
}
//建立连接
- (void)connect
{
NSError *err = nil;
//这里的kHost是服务端的ip,kPort是服务端的监听端口
[_socket connectToHost:kHost onPort:kPort error:&err];
if (!err)
{
NSLog(@"客户端初始化成功");
}else{
NSLog(@"客户端初始化失败");
}
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"发送数据成功");
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
[_dataPool appendData:data];
if (_dataPool.length >= sizeof(unsigned long))
{
int length;
[_dataPool getBytes:&length range:NSMakeRange(0, sizeof(unsigned long))];
if (length <= _dataPool.length - sizeof(unsigned long))
{
//传递消息给订阅信号的类,用来做具体的业务逻辑
[self.msgSubject sendNext:[_dataPool subdataWithRange:NSMakeRange(sizeof(unsigned long), length)]];
_dataPool = [NSMutableData dataWithData:[_dataPool subdataWithRange:NSMakeRange(sizeof(unsigned long) + length, _dataPool.length - sizeof(unsigned long) - length)]];
//注意,接下来需要再次判断数据缓存池中的数据是否有多余的,如果有,一定要反复处理完,这里没有进行接下来的处理
}
}
[self.socket readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
//连接成功之后设置连接状态,同时发送连接成功的信号
_connected = YES;
[self.noticeSubject sendNext:@(NOTICETYPE_CONNECT_SUCCESS)];
[self.socket readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
//连接成功之后设置连接状态,同时发送连接断开的信号
_connected = NO;
[self.noticeSubject sendNext:@(NOTICETYPE_DISCONNECT)];
}
//懒加载
- (RACSubject *)noticeSubject
{
if (!_noticeSubject)
{
_noticeSubject = [RACSubject subject];
}
return _noticeSubject;
}
//懒加载
- (RACSubject *)msgSubject
{
if (!_msgSubject)
{
_msgSubject = [RACSubject subject];
}
return _msgSubject;
}
对于调用来讲,服务端直接使用[[FELIXSocketServer sharedServer] startServer]启动即可,对于客户端,直接在需要发送消息的地方[[FELIXSocketClient sharedClient] sendData:data]即可,不用再去考虑什么时候连接什么的,而对于客户端如果要在页面收取消息,直接订阅msgSubject即可,类似于:
[[FELIXSocketClient sharedClient].msgSubject subscribeNext:^(id _Nullable x) {
//x就是你在客户端[self.msgSubject sendNext:data]中data的内容
}];
由于这个是临时的demo,所以很多问题并没有考虑完善,仅仅是能跑通的程度,大家在使用的过程中还有很多地方需要注意和修改的地方。最后提醒一句ReactiveObjc的好用程度远远超出你用过的其他框架,但是
务必注意循环引用
务必注意循环引用
务必注意循环引用
ReactiveObjc各种隐式的强引用非常容易在你察觉不到的情况下出现各种循环引用,造成内存泄漏,即便如此,它仍然值得你为它所带来的麻烦买单。