zeroMQ通讯库--iOS <一>

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的操作文件ZMQContextZMQSocket,里面是没有提供超时设置的接口。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

你可能感兴趣的:(zeroMQ通讯库--iOS <一>)