【基于rabbitmq实现异步RPC框架】

|-- src
|   |-- main
|   |   |-- java
|   |   |   `-- org
|   |   |       `-- example
|   |   |           |                               `-- rpc
|   |   |           |   |                           `-- CallbackData.java
|   |   |           |   |                           `-- ConnectionUtil.java
|   |   |           |   |                           `-- IMsgCallback.java
|   |   |           |   |                           `-- RPCClient.java
|   |   |           |                               `-- RPCServer.java
|   |   |           `-- test
|   |   |               |                           `-- GameServer.java
|   |   |                                           `-- MjServer.java

  IMsgCallabck.java  // 回调函数,我们这里用字节数组

package org.example.rpc;

public interface IMsgCallback {
    void onMessage(byte[] body);
}

CallbackData.java

​
package org.example.rpc;

public class CallbackData {
    private String corrIdStr;
    private long startTime;
    private IMsgCallback msgCallback;

    public CallbackData(String corrIdStr, long startTime, IMsgCallback msgCallback) {
        this.corrIdStr = corrIdStr;
        this.startTime = startTime;
        this.msgCallback = msgCallback;
    }

    public IMsgCallback getMsgCallback() {
        return msgCallback;
    }

    public String getCorrIdStr() {
        return corrIdStr;
    }

    public void setCorrIdStr(String corrIdStr) {
        this.corrIdStr = corrIdStr;
    }

    /**
     * 是否超时
     *
     * @return
     */
    public boolean isTimeout() {
        long curTime = System.currentTimeMillis();
        if (curTime - startTime > 5000) {
            return true;
        }

        return false;
    }
}

​

ConnectionUtil.java  // 连接到RabbitMQ

package org.example.rpc;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConnectionUtil {
    public static Connection getConnection() throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/itcast");
        connectionFactory.setUsername("heima");
        connectionFactory.setPassword("heima");

        return connectionFactory.newConnection();
    }
}

 RPCClient.java // RPC客户端,用于发起http请求

package org.example.rpc;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

public class RPCClient {
    // 连接上mq,相当于一个Socket
    private Connection connection;
    private Channel channel;

    // 异步监听结果
    private Map<String, CallbackData> msgCallbackMap = new ConcurrentHashMap<>();
    // 唯一请求
    private AtomicInteger corrId = new AtomicInteger(0);

    // 用于监听回调结果的Queue名字
    private String serverId;

    public RPCClient(String serverId) throws IOException, TimeoutException {
        this.serverId = serverId;

        connection = ConnectionUtil.getConnection();
        channel = connection.createChannel();
        // durable:持久 exclusive:排他
        channel.queueDeclare(serverId, false, false, true, null);
        // 一次只处理一个请求
        channel.basicQos(1);
        // TODO 重启时清空下队列
        channel.queuePurge(serverId);

        channel.basicConsume(serverId, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String correlationId = properties.getCorrelationId();
                CallbackData callbackData = msgCallbackMap.remove(correlationId);
                if (callbackData != null) {
                    callbackData.getMsgCallback().onMessage(body);
                    System.out.println("收到回应,还剩余:" + msgCallbackMap.size());
                }
            }
        });

        // 扫描超时的请求
        new Thread(() -> {
            for (; ; ) {
                try {
                    TimeUnit.SECONDS.sleep(60);
                    msgCallbackMap.forEach((k, v) -> {
                        if (v.isTimeout()) {
                            System.err.println("超时的请求CorrIdStr:" + v.getCorrIdStr());
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        System.out.println("RPC客户端 serverId:" + serverId + " 启动成功");
    }

    /**
     * 由于请求是异步的,因此会直接关闭
     *
     * @throws Exception
     */
    public void stopServer() throws Exception {
        connection.close();
    }

    /**
     * 调用远程某个serverId上的方法
     *
     * @param reqMsgStr
     * @param remoteServerIdStr 标识这次用于请求哪个服务器
     * @param msgCallback
     * @throws Exception
     */
    public void callRemoteMethod(String reqMsgStr, String remoteServerIdStr, IMsgCallback msgCallback) throws Exception {
        String corrIdStr = String.valueOf(corrId.incrementAndGet());

        // 让服务端知道此次请求的信息
        AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
                .correlationId(corrIdStr) // TODO 标识唯一一次请求,可以用AtomicInteger代替
                .replyTo(this.serverId) // 监听返回结果的队列
                .build();

        // 发送请求数据到rpc队列
        channel.basicPublish("", remoteServerIdStr, props, reqMsgStr.getBytes("UTF-8"));

        // 记录这次请求
        msgCallbackMap.put(corrIdStr, new CallbackData(corrIdStr, System.currentTimeMillis(), msgCallback));
        System.out.println("等待反应个数:" + msgCallbackMap.size());
    }
}

RPCServer.java // rpc服务端

package org.example.rpc;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RPCServer {
    private Connection connection = ConnectionUtil.getConnection();
    private Channel channel = connection.createChannel();
    private final int CORE_SIZE = Runtime.getRuntime().availableProcessors();
    private final ExecutorService[] _esArray = new ExecutorService[CORE_SIZE];

    // 监听客户端请求的Queue
    private String serverId;

    public RPCServer(String serverId) throws Exception {
        this.serverId = serverId;
    }

    public void start() throws Exception {
        // 业务线程池
        for (int i = 0; i < CORE_SIZE; i++) {
            String threadName = "business-thread-" + i;
            _esArray[i] = Executors.newSingleThreadExecutor((newRunnable) -> {
                Thread newThread = new Thread(newRunnable);
                newThread.setName(threadName);
                return newThread;
            });
        }

        // 创建Connection和Channel
        // 客户端的rpc请求队列
        channel.queueDeclare(serverId, false, false, true, null);
        // 一次只处理一个请求
        channel.basicQos(1);
        // TODO 重启时清空下队列
        channel.queuePurge(serverId);

        // 在rpc队列等待客户端的请求
        channel.basicConsume(serverId, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(properties.getCorrelationId())
                        .build();

                // 确认下收到了请求
                channel.basicAck(envelope.getDeliveryTag(), false);

                // 由于规定每一个客户端请求队列是固定的,因此筛选出一个线程处理
                int hash = properties.getReplyTo().hashCode();
                int index = Math.abs(hash % CORE_SIZE);
                _esArray[index].execute(() -> {
                    System.out.println("业务线程名字 threadName:" + Thread.currentThread().getName());
                    System.out.println("RPC客户端请求标识 properties.getCorrelationId:" + properties.getCorrelationId()); // 客户端规定的请求唯一标识
                    System.out.println("MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:" + envelope.getDeliveryTag());
                    System.out.println();

                    String response = "";
                    try {
                        // 解析客户端的请求
                        String message = new String(body, "UTF-8");
                        int n = Integer.parseInt(message);
                        //计算
                        response += fib(n);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            // 计算完毕后,将结果扔到回调队列
                            channel.basicPublish("", properties.getReplyTo(), replyProps,
                                    response.getBytes("UTF-8")); // 将结果返回过去
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        });

        System.out.println("RPC服务端 serverId:" + serverId + " 启动成功");
    }

    private int fib(int n) {
        if (n == 0 || n == 1) {
            return n;
        }
        return fib(n - 1) + fib((n - 2));
    }
}

 GameServer.java // 游戏服务器,可以有多个玩家登陆后根据负载连接这个,然后rpc到MjServer

package org.example.test;

import org.example.rpc.IMsgCallback;
import org.example.rpc.RPCClient;

public class GameServer {
    public static void main(String[] args) throws Exception {
        for (int num = 0; num < Runtime.getRuntime().availableProcessors(); num++) {
            String name = String.valueOf(num);
            RPCClient rpcClient = new RPCClient("GameServer-" + name);
            try {
                for (int i = 0; i < 47; i++) {
                    String reqMsgStr = Integer.toString(i);
                    // 向WorldServer发出rpc请求
                    rpcClient.callRemoteMethod(reqMsgStr, "MjServer", new IMsgCallback() {
                        @Override
                        public void onMessage(byte[] body) {
                            String response = new String(body);
                            System.out.println(name + "-" + reqMsgStr + "-" + response);
                        }
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

MjServer.java // 相当于一个个业务服务器,可以有多个

package org.example.test;

import org.example.rpc.RPCServer;

public class MjServer {
    public static void main(String[] args) throws Exception {
        RPCServer rpcServer = new RPCServer("MjServer");
        rpcServer.start();
    }
}

服务端:

RPC服务端 serverId:MjServer 启动成功
业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:1
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:1

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:2
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:2

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:3
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:3

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:4
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:4

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:5
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:5

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:6
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:6

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:7
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:7

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:8
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:8

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:9
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:9

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:10
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:10

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:11
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:11

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:12
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:12

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:13
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:13

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:14
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:14

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:15
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:15

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:16
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:16

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:17
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:17

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:18
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:18

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:19
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:19

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:20
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:20

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:21
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:21

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:22
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:22

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:23
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:23

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:24
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:24

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:25
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:25

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:26
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:26

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:27
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:27

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:28
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:28

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:29
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:29

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:30
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:30

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:31
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:31

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:32
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:32

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:33
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:33

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:34
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:34

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:35
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:35

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:36
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:36

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:37
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:37

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:38
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:38

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:39
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:39

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:40
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:40

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:41
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:41

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:42
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:42

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:43
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:43

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:44
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:44

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:45
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:45

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:46
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:46

业务线程名字 threadName:business-thread-5
RPC客户端请求标识 properties.getCorrelationId:47
MQ内部用于确认收到这个消息号 envelope.getDeliveryTag:47

客户端:

1-45-1134903170
收到回应,还剩余:1
7-45-1134903170
收到回应,还剩余:1
2-45-1134903170
收到回应,还剩余:1
6-45-1134903170
收到回应,还剩余:1
0-46-1836311903
收到回应,还剩余:0
3-46-1836311903
收到回应,还剩余:0
5-46-1836311903
收到回应,还剩余:0
7-46-1836311903
收到回应,还剩余:0
4-46-1836311903
收到回应,还剩余:0
1-46-1836311903
收到回应,还剩余:0
2-46-1836311903
收到回应,还剩余:0
6-46-1836311903
收到回应,还剩余:0

自动删除队列

笔记:

在云服务上,部署到内网,因此服务之间rpc访问很快。

按照特性划分为一个个微服务,每个服务都监听在serverId这个队列上,监听其它服务发给自己的请求,然后进行处理。

=====================服务节点架构====================

多个游戏的话,用vhost做隔离。

===========实际项目中交换机种类的设计=========

多个Exchange代表多个服务通讯类型:

SINGLE_SERVER_EXCHANGE,   

SERVERS_EXCHANGE_BY_TYPE,

SERVERS_EXCHANGE_BY_TYPE_AND_BANLANCE,

ALL_SERVER_EXCHANGE;


RoutingKey: 运行着的服务节点
 

使用MQ实现rpc通信,则是多个Exchange区分不同的业务通信类型(点对点、广播...) 和 RoutingKey(设置为ServerId),从而知道路由到哪个队列。

总结:// 我们这demo还做到了这些

1.服务器启动时清空队列

2.创建的是连接断开后会自动删除队列

3.考虑rpc超时监控

4.业务端采用多线程方式处理业务,并且对单个客户端逻辑是单线程处理

思考:

1.引入了一个中间件,看着还是挺麻烦的,能否用Netty代替,这个就要取舍了。

2.还有更轻量级的Redisson的发布订阅也是可以实现服务间通信,而且一个消息可以被消费多次,看起来更加符合游戏服务器的特点。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值