![2e2dd0ec7e2a068254b14ca05e8553e6.png](https://img-blog.csdnimg.cn/img_convert/2e2dd0ec7e2a068254b14ca05e8553e6.png)
想要源码可以访问我的github(记得帮我点个小星星 ):
Anonymoushhh/MyMQgithub.com![152560715b6686384d2ac742808beb4c.png](https://img-blog.csdnimg.cn/img_convert/152560715b6686384d2ac742808beb4c.png)
MyMQ简介
MyMQ是一个简单版的消息队列,它的架构主要分为三部分:Producer,Broker和Consumer。
![df2d3135ca478933b083155f690d4049.png](https://img-blog.csdnimg.cn/img_convert/df2d3135ca478933b083155f690d4049.png)
生产者支持同步发送消息和发送单向消息,生产者发送消息时需先通过一个消息主题向Broker申请队列,Broker根据自己的负载情况返回给生产者可用队列号,生产者将队列号添加到topic中,并用该消息主题发送消息;
Broker中有许多队列,每个队列中消息顺序一定,队列对消息主题Topic可以是多对多,一对多,多对一的关系,具体如何使用由使用者决定。Broker支持负载均衡和消息过滤功能,对消费者提供Push和Pull两种模式。Broker还实现了主从同步(Slave节点)和队列持久化存储与恢复来保证消息的可靠性。若消息由于网络原因发送失败时会重试,默认为16次,发送成功(返回ACK)或返回失败消息后才会发送下一条消息,以此来保证消息的有序性;
消费者可以同步获取消息,延时获取消息,支持Push和Pull两种模式。
Producer,Broker和Consumer三者支持单机和分布式环境,通过NIO的Socket通信。
Producer,Broker和Consumer三者均支持横向扩展,增加新的机器对旧的服务没有任何影响,保证了高可用性。
MyMQ架构
Broker
- Broker.java
- BrokerResponseProcessor.java
- Filter.java
- LoadBalancer.java
- MyQueue.java
- Slave.java
- SlaveResponseProcessor.java
- Synchronizer.java
Broker包的作用主要是创建Broker实例对象,以及提供主从同步,负载均衡,消息过滤服务。
Common
- IpNode.java
- Message.java
- MessageType.java
- PullMessage.java
- RegisterMessage.java
- Topic.java
Common包定义了一些通用的类,如消息类,地址类等。
Consumer
- ConsumerFactory.java
- ConsumerResponseProcessor.java
消费者包定义了消费者工厂,可通过工厂方法添加消费者。
Producer
- DelaySyscProducerFactory.java
- SyscProducerFactory.java
- UnidirectionalProducerFactory.java
生产者包定义了生产者工厂,支持同步生产者工厂,延时生产者工厂和单向生产者工厂。
Test
- ConsumerTest.java
- DaoTest.java
- BrokerTest.java
- ProducerTest.java
- PressTest.java
测试包,里面包含了MyMQ的基本使用方法。
Utils
- Client.java
- DefaultRequestProcessor.java
- DefaultResponseProcessor.java
- JsonFormatUtil.java
- PersistenceUtil.java
- MessageUtil.java
- RequestProcessor.java
- ResponseProcessor.java
- SequenceUtil.java
- SerializeUtil.java
- Server.java
工具包,定义了一些通用的工具类。
MyMQ使用指南
Broker.Broker
Broker为消息队列服务器节点,提供的服务有:消息存储,消息分发(Push模式与Pull模式),失败重试机制,消息过滤,负载均衡,死信队列,主从备份,持久化存储(同步或异步刷盘)与冗机恢复,横向扩展等。
![eb17dee02e24ade7b22c7639cbd90906.png](https://img-blog.csdnimg.cn/img_convert/eb17dee02e24ade7b22c7639cbd90906.png)
![09b07ee047959f7a7fe7992d9ea7d4f5.png](https://img-blog.csdnimg.cn/img_convert/09b07ee047959f7a7fe7992d9ea7d4f5.png)
Broker.BrokerResponseProcessor
该类实现了ResponseProcessor接口,为Broker制定了特殊的消息响应机制。
![815e1587c4587ee521736538edf56a50.png](https://img-blog.csdnimg.cn/img_convert/815e1587c4587ee521736538edf56a50.png)
Broker.Filter
消息过滤器,将消息按照消费者地址分类。
![0ff28d9d029315652188cae61b7268a4.png](https://img-blog.csdnimg.cn/img_convert/0ff28d9d029315652188cae61b7268a4.png)
Broker.LoadBalancer
负载均衡器,用于为生产者选择一个合适的消息队列。
![b7793803b9be54705b8eab56a09a79c2.png](https://img-blog.csdnimg.cn/img_convert/b7793803b9be54705b8eab56a09a79c2.png)
Broker.MyQueue
消息队列类,保证了消息的顺序性。
![6f0063c4aa9d8c4c7179e54b170742d0.png](https://img-blog.csdnimg.cn/img_convert/6f0063c4aa9d8c4c7179e54b170742d0.png)
Broker.Slave
备份节点类,用于Slave的同步或异步备份。
![cc2e92f7b85631d2512591531ce66fc6.png](https://img-blog.csdnimg.cn/img_convert/cc2e92f7b85631d2512591531ce66fc6.png)
Broker.SlaveResponseProcessor
用于指定备份节点的特殊消息响应机制。
![dc1f0eff98a5f6142a7a85136558af55.png](https://img-blog.csdnimg.cn/img_convert/dc1f0eff98a5f6142a7a85136558af55.png)
Broker.Synchronizer
同步器,用于Broker主从节点的同步。
![537c8a5f93c4314408b0130f1265296e.png](https://img-blog.csdnimg.cn/img_convert/537c8a5f93c4314408b0130f1265296e.png)
Common.IpNode
定义一个网络地址。
![95bd578589eec1084f6cff22794a8886.png](https://img-blog.csdnimg.cn/img_convert/95bd578589eec1084f6cff22794a8886.png)
Common.Message
定义了传输的消息结构。
![35a2337b89c053f819e7462c02b12366.png](https://img-blog.csdnimg.cn/img_convert/35a2337b89c053f819e7462c02b12366.png)
Common.MessageType
定义了消息类型。
![a28e1b9457901aa31fe755c05d78413f.png](https://img-blog.csdnimg.cn/img_convert/a28e1b9457901aa31fe755c05d78413f.png)
Common.PullMessage
一种特殊的消息,用于消费者向Broker拉取消息。
![b526b631621418ab37c75e0cfb3a2af9.png](https://img-blog.csdnimg.cn/img_convert/b526b631621418ab37c75e0cfb3a2af9.png)
Common.RegisterMessage
一种特殊的消息,用与消费者向Broker注册。
![36087de0a6c72512e9938c67b5f849e6.png](https://img-blog.csdnimg.cn/img_convert/36087de0a6c72512e9938c67b5f849e6.png)
Common.Topic
消息主题。
![c657e3eca80f5bc94900bbaad8a56d89.png](https://img-blog.csdnimg.cn/img_convert/c657e3eca80f5bc94900bbaad8a56d89.png)
Consumer.ConsumerFactory
消费者工厂类,用于创建消费者。
![926bcf2d3efb3bd55e1ff48049ae364e.png](https://img-blog.csdnimg.cn/img_convert/926bcf2d3efb3bd55e1ff48049ae364e.png)
Consumer.ConsumerResponeProcessor
为消费者指定特殊的消息响应机制。
![eda66be67ad57620e7632e43875f01f6.png](https://img-blog.csdnimg.cn/img_convert/eda66be67ad57620e7632e43875f01f6.png)
Producer.SyscProducerFactory
同步生产者工厂。
![5ee968a11a0ace1b9d3f1c0c8d049c1b.png](https://img-blog.csdnimg.cn/img_convert/5ee968a11a0ace1b9d3f1c0c8d049c1b.png)
Producer.DelaySyscProducerFactory
延时生产者工厂。
![15e43203776b67dcb7cd4711dd205220.png](https://img-blog.csdnimg.cn/img_convert/15e43203776b67dcb7cd4711dd205220.png)
Producer.UndirectionalProducerFactory
单向消息生产者工厂。 API同SyscProducerFactory。
Utils.Client
NIO通信模型客户端类,用于发送消息和接受回复。
![182fdc6114330606e3e3a207a371eb8e.png](https://img-blog.csdnimg.cn/img_convert/182fdc6114330606e3e3a207a371eb8e.png)
Utils.DefaultRequestProcessor
默认的请求接收响应类。
![4fe51d4cea0579b98a11ab61e7a2887a.png](https://img-blog.csdnimg.cn/img_convert/4fe51d4cea0579b98a11ab61e7a2887a.png)
Utils.DefaultResponeProcessor
默认的请求回复响应类。
![a77fcd0d6ea7d08ad89fd128349d0e25.png](https://img-blog.csdnimg.cn/img_convert/a77fcd0d6ea7d08ad89fd128349d0e25.png)
Utils.RequestProcessor接口
请求接收响应接口。
![f301b8406de138aed47e7f7d9a2d4a37.png](https://img-blog.csdnimg.cn/img_convert/f301b8406de138aed47e7f7d9a2d4a37.png)
Utils.ResponseProcessor接口
请求回复响应接口。
![60acf14b47a1d28e2b97a4541bd7e435.png](https://img-blog.csdnimg.cn/img_convert/60acf14b47a1d28e2b97a4541bd7e435.png)
Utils.SequenceUtil
生成唯一序列号的工具类(单机唯一)。
![4ed574e9bc236e75b40344c6b1d1aecb.png](https://img-blog.csdnimg.cn/img_convert/4ed574e9bc236e75b40344c6b1d1aecb.png)
Utils.SerializeUtil
序列化工具类。
![e9521a68993216b7a2cb257ca7698245.png](https://img-blog.csdnimg.cn/img_convert/e9521a68993216b7a2cb257ca7698245.png)
Utils.Server
NIO通信模型服务器类,在某个端口上监听消息。
![f063b5233de2cadb2d9c3a895b0e16b2.png](https://img-blog.csdnimg.cn/img_convert/f063b5233de2cadb2d9c3a895b0e16b2.png)
使用示例
Producer
SequenceUtil
Broker
//创建Broker(主从复制,push模式)
Consumer
//创建Consumer(Push模式)
IpNode ipNode1 = new IpNode("127.0.0.1", 81);
IpNode ipNode2 = new IpNode("127.0.0.1", 8888);//消费者地址
try {
ConsumerFactory.createConsumer(ipNode1, ipNode2);
} catch (IOException e1) {
System.out.println("Broker未上线!");
}
while(true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message m1 = ConsumerFactory.getMessage(8888);
if(m1!=null)
System.out.println("消费者"+ipNode2.getIp()+ipNode2.getPort()+"收到消息:"+m1.getMessage());
}
//创建Consumer(Pull模式)
IpNode ipNode3 = new IpNode("127.0.0.1", 81);
IpNode ipNode4 = new IpNode("127.0.0.1", 8888);
try {
ConsumerFactory.createConsumer(ipNode3, ipNode4);
} catch (IOException e) {
System.out.println("Broker未上线!");
}
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ConsumerFactory.Pull(ipNode3, ipNode4);
}
主要架构与功能实现详解
消息结构
public class Message implements Serializable{
private static final long serialVersionUID = 1L;
private int num;//消息序号
private String message;//消息
private int type;//消息类型
private Topic topic;//消息主题
...
}
Message类实现了序列化接口,每个Message有消息序号(该序号是否具有唯一性由使用者决定),消息内容,消息类型和消息主题。消息内容由使用者自己定义,可以是某个手机号(用于给该手机号发送短信)或订单信息(用于更新数据库)等等。消息类型定义了5种:
public static final int ONE_WAY = 0;//单向消息
public static final int REPLY_EXPECTED = 1;//需要得到回复的消息
public static final int REQUEST_QUEUE = 2;//请求包,用户生产者向Broker申请队列
public static final int REGISTER = 3;//用于消费者向Broker注册
public static final int PULL = 4;//用于消费者向Broker注册
消息主题类Topic定义如下:
private HashSet<Integer> queueId;//该Topic在Broker中对应的queueId
private HashSet<IpNode> consumer_address;//该Topic对应的cunsumer
String topic_name;//主题名称
int queueNum;//请求队列数
该类同样实现了序列化接口,主要用于记录消息主题名称,请求队列数,请求队列号和消费者地址。当用户首次定义一个Topic时,需要向Broker申请分配可用的消息队列号,之后将可用的队列号存储进Topic中,以后使用该Topic时就无需申请队列。
消息存储
public class MyQueue implements Serializable{
private static final long serialVersionUID = 1L;
private ConcurrentLinkedDeque<Message> queue;
}
MyQueue定义了消息存储队列,它的实现是一个同步的双向队列,一个Broker中可以同时存在一个或多个队列。
消息过滤
public HashMap<IpNode, List<Message>> filter(List<Message> list) {
//将Message按照分发地址分类
HashMap<IpNode, List<Message>> map = new HashMap<IpNode, List<Message>>();
//初始化
for(IpNode address:index) {
if(map.get(address)==null) {
map.put(address, new ArrayList<Message>());
}
}
//遍历消息,将每条message分类
Iterator<Message> iterator = list.iterator();
while(iterator.hasNext()) {
Message message = iterator.next();
//每个message可能有很多消费者
List<IpNode> consumer_address = message.getTopic().getConsumer();
Iterator<IpNode> it = consumer_address.iterator();
while(it.hasNext()) {
IpNode address = it.next();
List<Message> l = map.get(address);
if(l!=null)
l.add(message);
}
}
return map;
}
过滤器的主要作用就是将要发送的消息按照消费者地址分类,一个消息可能有一个或多个消费者。
消息分发(Push模式与Pull模式)
//为消费者推送消息
private void pushMessage() {
HashMap<IpNode, List<Message>> map = filter(index,poll(1));
for(IpNode ip:map.keySet())
{
List<Message> message = map.get(ip);
for(Message m:message) {
Client client = clients.get(ip);
if(client!=null) {
int i=0;
for(i=0;i<reTry_Time;i++) {//失败重试三次
String ack=null;
try {
ack = client.SyscSend(m);
} catch (IOException e) {
System.out.println("发送失败!正在重试...");
}
if(ack!=null)
break;
}
if(i>=reTry_Time) {
//todo 进入死信队列
}
}else {
System.out.println("消费者不存在");
//todo 进入死信队列
}
}
}
}
//push模式
public void push() {
new Thread(){
public void run() {
while(true) {
try {
Thread.sleep(push_Time);
} catch (InterruptedException e) {
e.printStackTrace();
}
pushMessage();
}
};
}.start();
}
push模式启动一个线程,每次push过程是所有队列出队一个元素,使用过滤器将所有消息分类,发送给相应的消费者,如果发送失败则重试一定次数(默认16次),次数达到上限后依然失败的话会进入死信队列,并告知相应的生产者。
负载均衡
public static List<Integer> balance(ConcurrentHashMap<String,MyQueue> queueList,int queueNum){
//此时queueList的size一定大于queueNum
List<Integer> list = new ArrayList<>();
for(int i=0;i<queueNum;i++) {
int index = 0;
int min = Integer.MAX_VALUE;
for(java.util.Map.Entry<String, MyQueue> entry:queueList.entrySet()) {
if(entry.getValue().size()<min&&!list.contains(Integer.valueOf(entry.getKey()))) {
min = entry.getValue().size();
index = Integer.valueOf(entry.getKey());
}
}
list.add(index);
}
return list;
}
负载均衡器提供一个负载均衡的方法,遍历队列找到前queueNum小的队列号。
主从备份
//slave同步
new Thread(){
public void run() {
while(true) {
if(hasSlave) {
try {
Thread.sleep(sync_Time);
} catch (InterruptedException e) {
e.printStackTrace();
}
Synchronizer sync = new Synchronizer(queueList, index);
try {
String s = SerializeUtil.serialize(sync);
for(IpNode ip:slave) {
Client client = new Client(ip.getIp(), ip.getPort());
client.Send(s);
}
} catch (IOException e) {
System.out.println("Slave未上线!");
}
}
}
};
}.start();
Broker会在init方法中创建一个线程。如果创建带Slave节点备份的消息队列的话,该线程会不停的向Slave节点同步消息,同步不可保证强一致性。
持久化存储(同步或异步刷盘)与冗机恢复
//持久化
new Thread(){
public void run() {
while(true) {
if(startPersistence) {
try {
Thread.sleep(store_Time);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
String path = PersistenceUtil.class.getResource("").getPath().toString().substring(1);
File file = new File(path);
String newPath1 = file.getParentFile().getParent()+"QueueList.json";
String newPath2 = file.getParentFile().getParent()+"ConsumerAddress.json";
PersistenceUtil.Export(PersistenceUtil.persistenceQueue(broker.queueList),newPath1);
PersistenceUtil.Export(PersistenceUtil.persistenceConsumer(broker.index),newPath2);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
}.start();
Broker在init方法中创建一个线程。如果用户开启持久化功能,该线程会每隔一段时间将队列内容写入磁盘,存储格式为2个json,一个存队列内容,一个存消费者地址。 若不幸冗机,用户可根据recover方法来恢复Broker。
//恢复Broker
public void recover() {
String path = PersistenceUtil.class.getResource("").getPath().toString().substring(1);
File file = new File(path);
String newPath1 = file.getParentFile().getParent()+"QueueList.json";
String newPath2 = file.getParentFile().getParent()+"ConsumerAddress.json";
ConcurrentHashMap<String,MyQueue> queueList = PersistenceUtil.Extraction(PersistenceUtil.Import(newPath1));
this.setQueueList(queueList);
List<IpNode> address= PersistenceUtil.ExtractionConsumer(PersistenceUtil.Import(newPath2));
for(IpNode ipNode:address)
addConsumer(ipNode);
}
死信队列
死信队列机制因为楼主时间有限,没有在代码中实现。主要思路就是借鉴了TCP的捎带确认机制设计的捎带回复机制:当消息队列服务器Broker 向消费者发送消息时,可能由于一系列原因导致发送失败(例如消费者不在线或网络延时较高),此时若达到了系统设置的重试次数上限,则向定时线程池ScheduledThreadPoolExecutor提交任务,该任务为5秒后向该消息的生产者发送消息失败信息。若5秒内该生产者再次发来非单向消息,则捎带回复失败消息的信息。
生产者工厂(这里以延时同步工厂为例)
private static ConcurrentHashMap<IpNode, Boolean> requestMap= new ConcurrentHashMap<IpNode, Boolean>();
private static int reTry_Time = 16;
private static int Delay_Time = 2000;//延时时间默认2s
requestMap用于记录该消费者地址是否已向Broker注册,reTry_Time定义发送失败重试的次数,Delay_Time定义了延时发送时间。 生产者需先向Broker申请队列:
public static Topic RequestQueue(Topic topic,String ip,int port){//输入为一个topic,里面包含请求的队列个数
System.out.println("请求向Broker申请队列...");
Topic t = topic;
Message m = new Message("RequestQueue",MessageType.REQUEST_QUEUE,t, -1);
String queue = DelaySyscProducerFactory.SendQueueRegister(m, ip, port);
String[] l = queue.substring(7).split(" ");
for(String i:l)
topic.addQueueId(Integer.parseInt(i));
IpNode ipNode = new IpNode(ip, port);
requestMap.put(ipNode, true);
return t;
}
申请队列时向Broker发送一个MessageType.REQUEST_QUEUE类型的消息:
private static String SendQueueRegister(Message msg,String ip,int port) {//未申请队列返回null
Client client;
if(msg.getType()!=MessageType.REPLY_EXPECTED&&msg.getType()!=MessageType.REQUEST_QUEUE)
msg.setType(MessageType.REPLY_EXPECTED);
try {
client = new Client(ip, port);
//失败重复,reTry_Time次放弃
for(int i=0;i<reTry_Time;i++) {
String result = client.SyscSend(msg);
if(result!=null) {
System.out.println("队列申请成功!");
return result;
}
if("".equals(result))
return null;
}
} catch (IOException e) {
System.out.println("Broker未上线!");
}
return null;
}
Broker收到该消息后会返回可用的消息队列序号,生产者工厂将这些消息序号添加到topic中,之后就可用该topic发送消息了:
//发送成功返回值为消息号+ACK
//发送失败返回值为null
public static String Send(Message msg,String ip,int port) {//未申请队列返回null
IpNode ipNode = new IpNode(ip, port);
if(requestMap.get(ipNode)==null) {
System.out.println("未向Broker申请队列!");
return null;
}
//等待Delay_Time秒
try {
Thread.sleep(Delay_Time);
} catch (InterruptedException e1) {
e1.printStackTrace();
return null;
}
Client client;
if(msg.getType()!=MessageType.REPLY_EXPECTED&&msg.getType()!=MessageType.REQUEST_QUEUE)
msg.setType(MessageType.REPLY_EXPECTED);
//失败重复,reTry_Time次放弃
for(int i=0;i<reTry_Time;i++) {
try {
client = new Client(ip, port);
String result = client.SyscSend(msg);
if(result!=null)
return result;
if("".equals(result))
return null;
} catch (IOException e) {
System.out.println("生产者消息发送失败,正在重试第"+(i+1)+"次...");
}
}
return null;
}
若发送成功返回值为消息号+空格+ACK,发送失败返回值为null。
消费者工厂
private static ConcurrentHashMap<Integer, ConcurrentLinkedQueue<Message>> map = new ConcurrentHashMap<Integer,ConcurrentLinkedQueue<Message>>();
这个map用于缓存Broker发来的消息,键为本地端口号,值为该消费者的消息缓冲区。 消费者工厂调用createConsumer向Broker注册消费者:
public static void createConsumer(IpNode ipNode1/*Broker地址*/,IpNode ipNode2/*本地地址*/) throws IOException {
if(map.containsKey(ipNode2.getPort())) {
System.out.println("端口已被占用!");
return;
}
ConsumerFactory.register(ipNode1,ipNode2);
ConsumerFactory.waiting(ipNode2.getPort());
map.put(ipNode2.getPort(), new ConcurrentLinkedQueue<Message>());
}
register方法向Broker发送注册消息:
//向Broker注册
private static void register(IpNode ipNode1/*目的地址*/,IpNode ipNode2/*本地地址*/){
System.out.println("正在注册Consumer...");
Client client;
try {
client = new Client(ipNode1.getIp(), ipNode1.getPort());
RegisterMessage msg = new RegisterMessage(ipNode2, "register", 1);
if(client.SyscSend(msg)!=null)
System.out.println("注册成功!");
else
System.out.println("注册失败!");
} catch (IOException e) {
System.out.println("Connection Refuse.");
}
}
waiting方法的作用是在某个端口监听,接受消息队列发送来的消息。
//在某个端口监听
private static void waiting(int port) throws IOException {
DefaultRequestProcessor defaultRequestProcessor = new DefaultRequestProcessor();
ConsumerResponeProcessor consumerResponeProcessor = new ConsumerResponeProcessor();
new Thread(){
public void run() {
System.out.println("Consumer在本地端口"+port+"监听...");
try {
new Server(port,defaultRequestProcessor,consumerResponeProcessor);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}.start();
}
性能测试
我写了个简单的测试类来模拟高并发,主要用了CountDownLatch类:
public class PressTest {
public static void main(String[] args) {
int time = 100000;//并发线程数
final CountDownLatch send = new CountDownLatch(time);
final CountDownLatch timing = new CountDownLatch(time);
//记录消息是否被成功发送
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
for(int i=0;i<time;i++){
map.put(i+"", 0);
}
//创建Producer
SequenceUtil Sequence = new SequenceUtil();
Topic topic = SyscProducerFactory.RequestQueue(new Topic("topic",1), "127.0.0.1", 81);
topic.addConsumer(new IpNode("127.0.0.1", 8888));
long startTime=System.currentTimeMillis(); //获取开始时间
for(int i=0;i<time;i++) {
new Thread(){
public void run() {
int num = Sequence.getSequence();
Message msg = new Message("message"+num,topic, num);
SyscProducerFactory.setReTry_Time(16);//设置发送失败重试次数
try {
send.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
String string = SyscProducerFactory.Send(msg, "127.0.0.1", 81);//同步发送
if(string!=null) {
String[] a = string.split(" ");
map.put(a[0], map.get(a[0])+1);
}
System.out.println(string);
timing.countDown();
}
}.start();
send.countDown();
}
try {
timing.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
//打印未发送成功的消息序号
for(Entry<String, Integer> entry:map.entrySet())
if(entry.getValue()==0)
System.out.print(entry.getKey()+" ");
}
![8eb5f41f98ca193c7be3b8c7a3eb0e76.png](https://img-blog.csdnimg.cn/img_convert/8eb5f41f98ca193c7be3b8c7a3eb0e76.png)
最终测试结果还不错,不过由于楼主只是在本地不同进行测试,所以测试数据成立的前提是忽略网络延时。
尚有不足
本项目中还有很多功能没有实现,希望有兴趣的读者可以在github上贡献你们的代码。
- “心跳机制”
- 统一的注册中心
- 生产者消费者连接池
- ......