ZeroMQ(java)Socket之Dealer

本文详细介绍了ZeroMQ中Dealer类型的Socket实现原理。重点讲解了FQ类和LB类的作用及其实现方式,以及它们如何帮助Dealer Socket进行消息的公平排队和负载均衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

好像也有一段时间没有写博客了。。。

ZeroMQ到现在,其实基本的原理也都了解的还算差不多了,剩下的就是一些细节的了。。。

我们知道ZeroMQ中定义了很多类型的Socket,其实这个里面最为简单的就是Dealer类型的socket看了,它基本没有做太多额外的处理。。。

在看具体的Dealer之前,先来看看两个类型,其实是两个工具类,用于维护在pipe上面执行的读写操作。。。

首先是FQ类型,它用于维护Socket与Session之间的pipe,socket在pipe上的读操作:

//用于维护pipe上面的读取操作
public class FQ {

    //  Inbound pipes.
    private final List<Pipe> pipes;  //所有绑定的pipe
    
    private int active;  //激活的pipe的数量,所有激活的pipe都放在最前面
    private int current;  //下一个可以读取的pipe的下标
    private boolean more;  //如果是ture的话,表示只是接受了一部分,还有别的需要接受
    
    //构造函数
    public FQ () {
        active = 0;
        current = 0;
        more = false;
        
        pipes = new ArrayList<Pipe>();   //用于保存所有的关联的pipe
    }
    
    //这里其实很简单,直接保存pipe就好了,并且好更新pipe的数量,这里加进来的pipe一开始是需要放到活动的pipe部分的
    public void attach (Pipe pipe_) {
        pipes.add (pipe_);
        Utils.swap (pipes, active, pipes.size () - 1);  
        active++;
    }
    
    //终止一个pipe,这里还是比较的简单吧,直接去除就好了,不过这里需要更新一些标志位
    public void terminated (Pipe pipe_) {
        final int index = pipes.indexOf (pipe_);
        //当前pipe居然还在活动的部分中,需要现将其放到活动的pipe的最后去,
        if (index < active) {
            active--;
            Utils.swap (pipes, index, active);
            if (current == active)
                current = 0;  //更新current下标
        }
        pipes.remove (pipe_);  //移除
    }

    //激活一个pipe,其实这里是将当前的pipe放到活动的pipe的那里去,这里active数量还要加1
    public void activated (Pipe pipe_) {
        //  Move the pipe to the list of active pipes.
        Utils.swap(pipes, pipes.indexOf (pipe_), active);
        active++;
    }

    //这里从pipe里面读取数据
    public Msg recv(ValueReference<Integer> errno) {
        return recvpipe(errno, null);
    }

    public Msg recvpipe(ValueReference<Integer> errno, ValueReference<Pipe> pipe_) {

        //  Round-robin over the pipes to get the next message.
        while (active > 0) {  //有活动的pipe

            //  Try to fetch new message. If we've already read part of the message
            //  subsequent part should be immediately available.
            Msg msg_ = pipes.get(current).read();  //从当前的current下标读取
            boolean fetched = msg_ != null;  //判断是否有msg读取出来

            //  Note that when message is not fetched, current pipe is deactivated
            //  and replaced by another active pipe. Thus we don't have to increase
            //  the 'current' pointer.
            if (fetched) {
                if (pipe_ != null) {
                    pipe_.set(pipes.get(current));
                }
                more = msg_.has_more();  //判断当前的msg是否还有急需要读的
                if (!more) {  //如果没有more了,那么需要更新current下标
                    current = (current + 1) % active;  //更新current下标
                }
                return msg_;  //返回msg
            }
            
            //到这里了,说明没有读取到数据
            //  Check the atomicity of the message.
            //  If we've already received the first part of the message
            //  we should get the remaining parts without blocking.
            assert (!more);
            
            active--;  //表示当前的活动的pipe数量已经少了一个,
            Utils.swap (pipes, current, active);  //这里其实是将刚刚读取的pipe从活动部分去除
            if (current == active)  //有可能需要更新current下标
                current = 0;
        }

        //  No message is available. Initialise the output parameter
        //  to be a 0-byte message.
        errno.set(ZError.EAGAIN);  //到这里了,说明确实没得数据可以读取了
        return null;
    }

    //用于判断当前的底层的pipe是否有可以读取的数据
    public boolean has_in ()
    {
        //  There are subsequent parts of the partly-read message available.
        if (more)  //如果有标志位的话,那么肯定可以了
            return true;

        //  Note that messing with current doesn't break the fairness of fair
        //  queueing algorithm. If there are no messages available current will
        //  get back to its original value. Otherwise it'll point to the first
        //  pipe holding messages, skipping only pipes with no messages available.
        //在当前的活动pipe部分去找,直到找到有数据可以读取的
        while (active > 0) {
            if (pipes.get(current).check_read ())
                return true;

            //  Deactivate the pipe.
            active--;   //表示当前的这个没有数据可以读取,那么需要将其从活动的pipe部分移除
            Utils.swap (pipes, current, active);
            if (current == active)  //如果已经到了尾部,那么讲current下标设置为最开始
                current = 0;
        }

        return false;
    }

}

还蛮简单的吧,基本上注释都已经说的比较清楚了。。。首先有一个list,用于维护当前所有的pipe,然后有一个active下标,然后还有一个current下标:


这里在Active前面的pipe就是当前活动的pipe,也就是可能有数据可以读取的pipe,current就是下一个要读取的pipe的下标。。。应该够简单的吧,这里还通过current的移动,实现在pipe上面数据的循环读取。。。防止只在某一个pipe上读取数据。。。、

然后还有另外一个工具类,LB,它与LQ相反,它是用于维护写操作的,不知道为啥取这两个奇怪的名字,囧:

//用于维护pipe上的写操作
public class LB {

    //  List of outbound pipes.
    private final List<Pipe> pipes;  //用于保存所有关联的pipe
    
    private int active;  //当前所有的活动pipe的下标,前面的都是活动的
    private int current;  //current下标
    private boolean more;  //是否还有数据要写
    private boolean dropping;  //如果是true的话,那么丢弃当前的数据
    
    public LB() {
        active = 0;
        current = 0;
        more = false;
        dropping = false;
        
        pipes = new ArrayList<Pipe>();  //创建arraylist
    }

    public void attach (Pipe pipe_)  {
        pipes.add (pipe_);  //将当前的pipe放到列表
        activated (pipe_);  //将其移动到活动的pipe部分,也就是active前面
    }

    //终止一个pipe
    public void terminated(Pipe pipe_) {
        int index = pipes.indexOf (pipe_);  //获取这个pipe的下标

        //  If we are in the middle of multipart message and current pipe
        //  have disconnected, we have to drop the remainder of the message.
        if (index == current && more)  //如果就是当前的current,而且还有数据,那么将dropping标志位设置为true
            dropping = true;

        //  Remove the pipe from the list; adjust number of active pipes
        //  accordingly.
        if (index < active) {  //如果在active部分,那么这里还要更新一下
            active--;
            Utils.swap (pipes, index, active);
            if (current == active)
                current = 0;
        }
        pipes.remove (pipe_);

    }
    //将这个pipe移动到active部分,而且增加active计数
    public void activated(Pipe pipe_) {
        //  Move the pipe to the list of active pipes.
        Utils.swap (pipes, pipes.indexOf (pipe_), active);
        active++;
    }

    //用于向pipe写数据
    public boolean send(Msg msg_, ValueReference<Integer> errno) {
        //  Drop the message if required. If we are at the end of the message
        //  switch back to non-dropping mode.
        if (dropping) {  //如果有dropping标志位的话,那么直接丢弃就好了

            more = msg_.has_more();
            dropping = more;

            msg_.close();
            return true;
        }

        while (active > 0) {  //如果有底层的pipe可以写
            if (pipes.get(current).write (msg_))  //想当前的current的pipe写
                break;  //写进去了,那么直接break就好了

            assert (!more);
            active--;  //这里表示这个pipe已经满了,那么需要将其从active部分移除
            if (current < active)
                Utils.swap (pipes, current, active);
            else
                current = 0;
        }

        //  If there are no pipes we cannot send the message.
        if (active == 0) {  //实在没有pipe可以写了
            errno.set(ZError.EAGAIN);
            return false;
        }

        //  If it's final part of the message we can fluch it downstream and
        //  continue round-robinning (load balance).
        more = msg_.has_more();  //判断当前数据是否还有接下来要写的
        if (!more) {//如果没有的话,那么flush,而且要更新current
            pipes.get(current).flush ();
            if (active > 1)
                current = (current + 1) % active;
        }

        return true;
    }

    //用于看底层的pipe有没有哪一个可以写
    public boolean has_out() {
        //  If one part of the message was already written we can definitely
        //  write the rest of the message.
        if (more)
            return true;

        //遍历,直到找到可以写的pipe,这里还要将无法写的pipe从队列里面移除以及更新current下标
        while (active > 0) {

            //  Check whether a pipe has room for another message.
            if (pipes.get(current).check_write ())
                return true;

            //  Deactivate the pipe.
            active--;
            Utils.swap (pipes, current, active);
            if (current == active)
                current = 0;
        }

        return false;
    }
}

其实实现跟FQ基本一样,也就是这个是写操作。。。。。


好了,接下来来看看具体的Dealer类型的实现吧:

//这个应该最简单的socket类型了
public class Dealer extends SocketBase {
     
    public static class DealerSession extends SessionBase {  //这里其实对Session也没有扩展,就是直接用SessionBase
        public DealerSession(IOThread io_thread_, boolean connect_,
            SocketBase socket_, final Options options_,
            final Address addr_) {
            super(io_thread_, connect_, socket_, options_, addr_);
        }
    }
    
    //  Messages are fair-queued from inbound pipes. And load-balanced to
    //  the outbound pipes.
    private final FQ fq;  //用于维护读取操作
    private final LB lb;   //用于维护写操作

    //  Have we prefetched a message.
    private boolean prefetched;   //是否有预读取的msg
      
    private Msg prefetched_msg;   //指向预读取的数据 

    //构造函数,第一个参数是其所属的CTX,第二个参数是tid,这里其实并没有生存在某个IO线程中,所以这个ID并没有与任何IO线程关联,因为生存在用户线程
    public Dealer(Ctx parent_, int tid_, int sid_) {
        super(parent_, tid_, sid_);
        
        prefetched = false;
        options.type = ZMQ.ZMQ_DEALER;  //当前socket的类型
        
        fq = new FQ();
        lb = new LB();
        //  TODO: Uncomment the following line when DEALER will become true DEALER
        //  rather than generic dealer socket.
        //  If the socket is closing we can drop all the outbound requests. There'll
        //  be noone to receive the replies anyway.
        //  options.delay_on_close = false;
            
        options.recv_identity = true;   
    }


    @Override
    protected void xattach_pipe(Pipe pipe_, boolean icanhasall_) {
        assert (pipe_ != null);
        fq.attach (pipe_);  //放到维护读取的
        lb.attach (pipe_);  //放到维护写的
    }
    
    @Override
    protected boolean xsend(Msg msg_)
    {
        return lb.send(msg_, errno);  //直接调用lb来发送 msg,这里会遍历的底层活动的pipe,直接直到将msg写到pipe为止
    }

    @Override
    protected Msg xrecv() {
        return xxrecv();
    }

    private Msg xxrecv()
    {
        Msg msg_ = null;
        //  If there is a prefetched message, return it.
        if (prefetched) {  //如果有预接收的msg,那么直接返回它就行了
            msg_ = prefetched_msg;
            prefetched = false;
            prefetched_msg = null;
            return msg_;
        }

        //  DEALER socket doesn't use identities. We can safely drop it and 
        while (true) {  //这里不断的从底层的pipe里面读取数据
            msg_ = fq.recv(errno);  //调用fq来接收,其实是从底层的活动的pipe里面去接收数据,它会遍历底层所有活动的pipe,直到接收到数据为止
            if (msg_ == null)  //确实没有数据
                return null;
            if ((msg_.flags() & Msg.identity) == 0)  //dealer发送的msg没有标志位
                break;
        }
        return msg_;
    }

    @Override
    //用于判断底层的pipe是否有可以读取的msg
    protected boolean xhas_in ()
    {
        //  We may already have a message pre-fetched.
        if (prefetched)  //已经有预接收的数据了
            return true;

        //  Try to read the next message to the pre-fetch buffer.
        prefetched_msg = xxrecv();  //这里接收到预接收
        if (prefetched_msg == null)
            return false;
        prefetched = true;
        return true;
    }

    @Override
    //检查是否有pipe可以写数据
    protected boolean xhas_out () {
        return lb.has_out ();
    }

    @Override
    //当底层的pipe有数据可以读取的时候,这个其实将其放到的pipe列表的活动pipe部分,这个方法会在socketBase里面调用,当pipe接收到session传上来的数据的时候
    protected void xread_activated (Pipe pipe_) {
        fq.activated (pipe_);
    }

    @Override
    //将这个pipe放到活动部分,也就是可以向这个pipe里面写数据了
    protected void xwrite_activated (Pipe pipe_) {
        lb.activated (pipe_);
    }


    @Override
    //用于终止一个关联的pipe
    protected void xterminated(Pipe pipe_) {
        fq.terminated (pipe_);
        lb.terminated (pipe_);
    }

}

好吧,dealer的实现真的是太简单了,它继承自SocketBase,也就扩展了几个读写操作,其实具体的还是上面的两个工具类型对象完成的。。。

这里好像直接跳过SocketBase的实现直接来看具体的Socket类型的实现有点不太好,。。。不过其实也都蛮简单的。。。就不细说了。。。

这里有一点需要提醒的就是,在一般情况下对象都会关联一个IO线程,用于执行命令啥的,不过Socket并没有关联某个具体的IO线程,当然不是说它不需要执行命令,而是它直接依赖用户线程。。。

<think>首先,用户的问题是:“我想获取ZeroMQDEALER/ROUTER模式的代码示例 请问ZeroMQ ROUTER模式和DEALER模式代码示例”。这明确要求提供DEALER/ROUTER模式的代码示例。 系统级指令要求: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 - 回答结构清晰,帮助用户逐步解决问题,保证回答真实可靠。 用户可见层要求: - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 参考站内引用: - 引用[1]: 关于ZeroMQ通信模式与多帧消息,包括PUB/SUB、REQ/REP、DEALER/ROUTER等。 - 引用[2]: ZeroMQ(java)之Router/Dealer模式的代码示例。 - 引用[3]: 关于消息队列——ZeroMQ的router/dealer的PHP代码示例。 我需要基于这些引用提供代码示例。引用[2]提供了Java代码示例,引用[3]提供了PHP代码示例。用户没有指定语言,所以我应该覆盖常见的语言,比如Python、Java、PHP等,或者询问用户语言偏好。但为了全面,我可以提供多个语言的示例。 结构化回答: 1. **解释DEALER/ROUTER模式**:简要说明什么是DEALER和ROUTER模式,以及它们如何一起工作。 2. **提供代码示例**:包括Python、Java、PHP等语言的示例代码。 3. **添加引用标识**:自然地添加引用标识。 4. **生成相关问题**:在最后添加相关问题部分。 代码示例来源: - 从引用[2]中提取Java代码。 - 从引用[3]中提取PHP代码。 - 对于Python,我可以基于ZeroMQ文档或常见示例添加,因为引用中没有Python代码。确保真实可靠。 DEALER/ROUTER模式概述: - ROUTER套接字:处理多个客户端连接,添加身份标识。 - DEALER套接字:负载均衡,将消息分发到多个worker。 - 通常用于构建异步请求-响应模式。 代码结构: - 服务器端:使用ROUTER绑定到客户端。 - 工作端:使用DEALER连接到ROUTER。 - 代理:在ROUTER和DEALER之间设置代理。 从引用[2]: - Java代码:ROUTER绑定,DEALER绑定,并启动代理。 - 工作线程使用REP套接字。 从引用[3]: - PHP客户端代码:使用REQ套接字连接到服务器。 对于完整示例,我需要提供服务器、代理和客户端代码。 我的回答应包括: - Python示例(常见,ZeroMQ常用语言)。 - Java示例(从引用[2])。 - PHP示例(从引用[3])。 确保回答清晰: 1. 介绍模式。 2. 分语言提供代码。 3. 解释关键点。 4. 添加引用。 5. 生成相关问题。 最后,添加相关问题部分。</think>### ZeroMQ DEALER/ROUTER模式代码示例 ZeroMQDEALER/ROUTER模式常用于构建异步请求-响应系统,支持负载均衡和多路复用。ROUTER套接字负责处理客户端连接并添加身份标识,而DEALER套接字将消息分发到多个worker线程。这种模式适用于高并发场景,如微服务通信[^1]。下面我将提供Python、Java和PHP的完整代码示例,基于ZeroMQ官方文档和最佳实践,确保真实可靠。 #### 1. **模式概述** - **ROUTER套接字**:绑定到客户端,接收请求时附加客户端ID(如IP地址),用于路由响应。 - **DEALER套接字**:连接到worker线程,负载均衡分发请求。 - **工作流程**:客户端发送请求 → ROUTER接收并添加ID → DEALER分发到worker → worker处理并返回 → ROUTER通过ID路由响应回客户端。 - 优势:支持异步、非阻塞通信,避免单点瓶颈[^1]。 #### 2. **Python代码示例(推荐语言)** Python是ZeroMQ的常用语言,代码简洁易读。示例包括客户端、服务器和worker代理。 **客户端代码(使用DEALER套接字发送请求)** ```python import zmq context = zmq.Context() socket = context.socket(zmq.DEALER) # 使用DEALER套接字 socket.connect("tcp://localhost:5555") # 连接到ROUTER服务器 for i in range(5): socket.send_string(f"请求 {i}") # 发送消息 response = socket.recv_string() # 接收响应 print(f"收到响应: {response}") ``` **服务器和代理代码(ROUTER绑定,并代理到DEALER worker)** ```python import zmq import threading def worker(): context = zmq.Context() socket = context.socket(zmq.DEALER) # worker使用DEALER socket.connect("inproc://workers") # 连接到内部代理 while True: message = socket.recv_multipart() # 接收多帧消息(含ID) print(f"Worker处理: {message[1].decode()}") socket.send_multipart([message[0], b"响应"]) # 返回ID和响应 context = zmq.Context() router = context.socket(zmq.ROUTER) # ROUTER绑定客户端 router.bind("tcp://*:5555") dealer = context.socket(zmq.DEALER) # DEALER分发到worker dealer.bind("inproc://workers") # 启动worker线程 for _ in range(3): # 3个worker线程 threading.Thread(target=worker, daemon=True).start() # 代理:转发ROUTER和DEALER之间的消息 zmq.proxy(router, dealer) ``` **运行说明**: - 启动服务器代理:运行服务器代码。 - 启动客户端:发送请求,worker异步处理并返回响应。 - 输出示例:客户端显示`收到响应: 响应`,服务器显示`Worker处理: 请求 0`等。 #### 3. **Java代码示例(基于引用[2])** Java示例使用ROUTER和DEALER设置代理,worker线程处理请求[^2]。 ```java import org.zeromq.SocketType; import org.zeromq.ZMQ; import org.zeromq.ZContext; public class DealerRouterExample { public static void main(String[] args) { try (ZContext context = new ZContext()) { // ROUTER绑定客户端 ZMQ.Socket router = context.createSocket(SocketType.ROUTER); router.bind("tcp://*:5555"); // DEALER绑定worker ZMQ.Socket dealer = context.createSocket(SocketType.DEALER); dealer.bind("inproc://workers"); // 启动worker线程 for (int i = 0; i < 3; i++) { new Thread(() -> { try (ZMQ.Socket worker = context.createSocket(SocketType.DEALER)) { worker.connect("inproc://workers"); while (!Thread.currentThread().isInterrupted()) { byte[] id = worker.recv(); // 接收ID帧 worker.recv(); // 跳过空帧 String request = new String(worker.recv(), ZMQ.CHARSET); System.out.println("Worker处理: " + request); worker.send(id, ZMQ.SNDMORE); // 返回ID worker.send("响应".getBytes(ZMQ.CHARSET)); } } }).start(); } // 代理转发 ZMQ.proxy(router, dealer, null); } } } ``` **关键点**: - 使用`ZMQ.proxy`实现ROUTER和DEALER的代理。 - worker线程通过`inproc://workers`连接到DEALER,确保线程安全[^2]。 #### 4. **PHP代码示例(基于引用[3])** PHP示例展示客户端使用DEALER发送请求,服务器使用ROUTER处理[^3]。 **客户端代码** ```php <?php $context = new ZMQContext(); $socket = $context->getSocket(ZMQ::SOCKET_DEALER); // 使用DEALER套接字 $socket->connect("tcp://localhost:5555"); for ($i = 0; $i < 5; $i++) { $socket->send("请求 $i"); $response = $socket->recv(); echo "收到响应: $response\n"; } ?> ``` **服务器代码** ```php <?php $context = new ZMQContext(); $router = $context->getSocket(ZMQ::SOCKET_ROUTER); // ROUTER绑定 $router->bind("tcp://*:5555"); while (true) { $id = $router->recv(); // 接收客户端ID $router->recv(); // 跳过空帧 $request = $router->recv(); echo "服务器处理: $request\n"; $router->send($id, ZMQ::MODE_SNDMORE); // 返回ID $router->send("响应"); } ?> ``` **运行说明**: - 先启动PHP服务器:监听端口5555。 - 运行客户端:发送请求并打印响应。 - 注意:PHP需安装ZeroMQ扩展(例如通过PECL)。 ### 最佳实践与注意事项 - **多帧消息处理**:ROUTER/DEALER模式使用多帧消息(第一帧为ID),确保在代码中正确处理`recv_multipart`或`SNDMORE`[^1]。 - **错误处理**:添加超时机制(如`ZMQ::SOCKOPT_RCVTIMEO`)避免阻塞。 - **性能优化**:增加worker线程数以提高并发;使用`inproc`或`ipc`协议减少网络延迟[^1]。 - **语言选择**:Python适合快速原型,Java适合企业级应用,PHP适合Web集成。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值