zeroMQ是号称史上最好的通讯库,基于C语言开发的,实时流处理sorm的task之间的通信就是用的zeroMQ。zeroMQ在使用模式上支持多种,有req-reply,publish-subscribe,pipe。我在项目中使用的是req-replay, publish-subscribe两种方式,pipe方式暂时还没用过,或许后期研究下。zmq框架封装好了底层实现,只抛给我们一个socket使用,所以我对socket进行了二次封装,以满足我们的业务需求。
先上一个小菜,我个人对req-reply模式的使用和理解。req-replay是请求响应机制,类是http的响应请求,每请求一次服务器就获取一次数据,这种是应答请求模式。
下面是zmq的应答请求创建
ZMQContext *context = [[ZMQContext alloc] initWithIOThreads:1];
ZMQSocket *socket = [ctx socketWithType:ZMQ_REP]; // ZMQ_REP是请求响应模式
NSString *endpoint = @"tcp://localhost:5555"; // 需要绑定的IP地址
BOOL bind = [socket connectToEndpoint:endpoint];
if (!bind) {
NSLog(@"*** Failed to bind to endpoint [%@].", endpoint);
}
if (![socket sendData:json withFlags:1]) {
NSLog(@"发送失败");
} else {
NSData *reply = [socket receiveDataWithFlags:0]; // 阻塞当前线程,直到有数据返回
}
请求完成销毁socket和上下文
[context terminate];
这是zmq示例的用法,这个用法是不能满足在实际项目的使用。这里发送数据使用的是非阻塞式,接收数据是使用阻塞式,当然你也可以使用非阻塞式的,就是withflags:这个来选择的,使用非阻塞式就要轮询socket,获取服务器的返回的数据。我用的是阻塞式,这个方式比较符合我们的业务需求。
使用阻塞式首先要解决一个超时的问题,zmq提供给我们两个oc的操作文件ZMQContext和ZMQSocket,里面是没有提供超时设置的接口。zmq在iOS端应用比较少,网上可以查阅的资料也很少,最后看了PHP的示例代码,发现可以在socket层设置,然后我就在socket层设置了,并封装到ZMQSocket,对外提供了接口loadingtime。
阻塞式是肯定不能使用在主线程的,我们要另开一个线程来处理这种应答请求,为了考虑性能,我们就要面对两个问题了。第一个是socket的重用,不能每次请求都创建socket,请求完成就销毁socket,第二个是线程的重用。
zmq的应答请求,原则上一请求,一响应。可是真实的情况是有时候网络不好,请求的响应速度变慢了,然后你重用socket进行请求并发,socket就会出现发送失败,还有超时的socket重用也是发送失败的。超时的socket要销毁,重新开启socket来处理请求。
线程的重用是使用RunLoop来实现的,每个子线程都有RunLoop,只是默认不激活,我们要激活RunLoop,让线程在发送请求时工作,没有请求时进入休眠状态。
直接上代码,多说无益了-
// 创建子线程
- (NSThread *)thread {
if (_thread == nil) {
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundThread) object:nil];
_thread.name = @"zmqREQ";
[_thread start];
}
return _thread;
}
// 启动子线程,并激活RunLoop
- (void)backgroundThread {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
BOOL isCancelled = [currentThread isCancelled];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
// 开启runloop
[currentRunLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
isCancelled = [currentThread isCancelled];
}
}
}
// 空任务,唤醒线程
- (void)closeThread {}
// 提供外部的接口,把参数传进来,使用block回调
- (void)startRequest:(NSDictionary *)params success:(void (^)(id))success failure:(void (^)(NSError *))failure {
// 先保存block
NSString *key = [params description];
if (success != nil) {
[self.successDict setObject:success forKey:key];
}
if (failure != nil) {
[self.failureDict setObject:failure forKey:key];
}
// 异步请求
[self performSelector:@selector(requestInThread:) onThread:self.thread withObject:params waitUntilDone:NO];
}
// 让子线程执行发送的请求
- (void)requestInThread:(NSDictionary *)params {
NSThread *currentThread = [NSThread currentThread];
// 判断线程是否已经取消
if (currentThread.isCancelled) { return; }
// 获取缓存数组中的socket
ZMQSocket *socket = self.sockets.lastObject;
[self.sockets removeLastObject];
// 获取block
NSString *key = [params description];
successType success = self.successDict[key];
[self.successDict removeObjectForKey:key];
failureType failure = self.failureDict[key];
[self.failureDict removeObjectForKey:key];
if (socket == nil) {
socket = [self.context socketWithType:ZMQ_REQ];
socket.loadingtime = 5000;
NSString *endpoint = @"tcp://:41204"; // 服务器IP地址
if (![socket connectToEndpoint:endpoint]) {
NSLog(@"监听失败");
[socket close];
socket = nil;
}
NSLog(@"创建socket");
}
NSData *json = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
if (![socket sendData:json withFlags:1]) {
NSLog(@"发送失败");
dispatch_async(dispatch_get_main_queue(), ^{
if (failure) {
failure(nil);
}
});
}else{
if (socket == nil) return ;
NSData *reply = [socket receiveDataWithFlags:0]; // 阻塞当前线程,直到有数据返回
// 判断线程是否已经取消
if (currentThread.isCancelled) { return; }
id data = nil;
if (reply) {
data = [NSJSONSerialization JSONObjectWithData:reply options:0 error:nil];
[self.sockets addObject:socket];
} else {
[socket close];
socket = nil;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (success) {
success(data);
}
});
}
}
要激活RunLoop必须要有事件源和时钟,我这里用的是事件源,设置端口,让子线程接收其他线程的事件信号。这里要注意,使用 [currentRunLoop run] 方法,RunLoop就停不下来了,使用runMode:beforeDate:可以控制RunLoop的生命周期。子线程的代码运行到while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])就会进入循环并休眠,当子线程接收到任务信号时就会被唤醒并执行任务,执行完任务就执行while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])这行代码,并判断是否达到运行的期限,如果没有则继续休眠,反之就退出RunLoop结束子线程。这里是用了线程的取消标记来控制,如果线程已经取消了,就让RunLoop不执行[currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture],让RunLoop退出循环,不设置时间期限了。因而,我创建一个空任务,当要关闭子线程时,给子线程一个空任务达到唤醒子线程的目的,然后子线程进入判断,并退出RunLoop。
我这里使用NSMutableArray来存储socket和NSMutableDictionary存储回调的block,这两个是线程不安全的。sockets只在子线程操作,这不会产生数据争夺。回调block是在两个线程操作,但是利用dictionary的特性,我这样操作是没有影响的,我做过大量的测试,如果你们在使用中出现线程问题,可以加锁。
这篇博客是讲述zmq的应答模式,我封装的代码和改过的zeroMQ文件都放在 github 。
下一篇写对zmq订阅模式的使用和理解,欢迎关注。
参考
zeroMQ使用指导 http://zguide.zeromq.org/page:all
zeroMQ的示例程序 https://github.com/imatix/zguide.git