|
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的发布订阅也是可以实现服务间通信,而且一个消息可以被消费多次,看起来更加符合游戏服务器的特点。