java 单机版消息通讯模拟,加深消息通讯的理解
1.目标:通过单程序多线程模拟消息通讯过程,服务端负责客户端发送的消息存储及收到客户端消息后进行消息推送,客户端可进行上线,离线,向离线/在线用户发送消息,注册监听器等功能
2.交互过程:由于是单机版,消息的发送只能存储到服务端的容器中,通过服务端静态方法提供数据存储功能,替代传统的nio,bio消息事件机制
3.代码实现
3.1.Message.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) public class Message { Object body; String from; String to; boolean read; boolean send; public Message() { super(); } @Override public int hashCode() { return super.hashCode(); } @Override public boolean equals(Object obj) { return super.equals(obj); } @Override public String toString() { return "Message{" + "body=" + body + ", from='" + from + '\'' + ", to='" + to + '\'' + '}'; } }
3.2.MessageClient.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; import lombok.Data; import java.util.HashSet; import java.util.List; import java.util.Scanner; import java.util.Set; @Data public class MessageClient implements Runnable { String id; private Set<Message> postBox; private Set<Message> reciveBox; private MessageRecivedListener messageRecivedListener; private MessagePostedListener messagePostedListener; private boolean sender; private boolean stop; private boolean serverBroadCastMode = true; public MessageClient(String id){ this.id = id; this.postBox = new HashSet<>(); this.reciveBox = new HashSet<>(); MessageServer.connect(this); } public void registerMessageRecivedListener(MessageRecivedListener messageRecivedListener){ this.messageRecivedListener = messageRecivedListener; } public void registerMessagePostedListener(MessagePostedListener messagePostedListener){ this.messagePostedListener = messagePostedListener; } int getRecivedMessageCount(){ return (int) reciveBox.stream().filter(x->!x.read).count(); } int getPostedMessageCount(){ return (int) postBox.stream().filter(x->!x.send).count(); } void sendMessage(Message message) throws InterruptedException { postBox.add(message); System.out.println(this.id+":发件箱:"+postBox.size()+"个文件待发送!!!"); MessageServer.reciveFromClient(message); if(messagePostedListener != null) { messagePostedListener.postMessageSuccess(this); } postBox.remove(message); } void reciveMessage(List<Message> message){ if((message == null || message.size() == 0)) { return; } this.reciveBox.addAll(message); if (messageRecivedListener!= null) { messageRecivedListener.reciveMessage(this); } } void displayMessage(){ this.reciveBox.stream().filter(x->!x.read).forEach(x->{ x.setRead(true); System.out.println(this.id+"收到消息:"+x); }); } @Override public void run() { while(!stop) { if(sender) { System.out.println(this.id+"->请输入收件人和收件内容,用\";\"隔开:[$id上线,#id下线]"); Scanner scanner = new Scanner(System.in); String[] body = scanner.nextLine().split(";"); if(body.length == 1) { if(body[0].startsWith("$")) { MessageClient messageClient = new MessageClient(body[0].replace("$", "")); messageClient.setMessageRecivedListener(new MessageRecivedListenerImpl()); new Thread(messageClient).start(); continue; } else if(body[0].startsWith("#")) { MessageServer.disConnect(body[0].replace("#","")); continue; } else { continue; } } Message sender = new Message().setTo(body[0]).setFrom(this.id).setBody(body[1]); try { this.sendMessage(sender); } catch(InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(1000); } catch(InterruptedException e) { e.printStackTrace(); } } } public void pullMessage() { reciveBox.addAll(MessageServer.getMessageListByReciver(this.id)); this.displayMessage(); } public static void main(String[] args) throws InterruptedException { MessageClient zhangsanClient = new MessageClient("zhangsan"); zhangsanClient.setSender(true); zhangsanClient.registerMessagePostedListener(new MessagePostedListenerImpl()); MessageClient lisiClient = new MessageClient("lisi"); new Thread(zhangsanClient).start(); lisiClient.registerMessageRecivedListener(new MessageRecivedListenerImpl()); new Thread(lisiClient).start(); } }
3.3.MessagePostedListener.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; public interface MessagePostedListener { public void postMessageSuccess(MessageClient client); }
3.4.MessagePostedListenerImpl.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; public class MessagePostedListenerImpl implements MessagePostedListener { @Override public void postMessageSuccess(MessageClient client) { System.out.println(client.getId()+"发送回执监听器检测到消息发送成功,数量:"+client.getPostedMessageCount()); } }
3.5.MessageRecivedListener.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; public interface MessageRecivedListener { public void reciveMessage(MessageClient client); }
3.6.MessageRecivedListenerImpl.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; public class MessageRecivedListenerImpl implements MessageRecivedListener { @Override public void reciveMessage(MessageClient client) { System.out.println(client.getId()+":message listener called!!!,detect msg size:"+client.getRecivedMessageCount()); client.displayMessage(); } }
3.7.MessageServer.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.example.demo.test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.springframework.util.Assert; import org.springframework.util.StringUtils; public class MessageServer { private volatile static List<Message> messageList; private volatile static List<MessageClient> clientList; private static boolean broadCastExecute = true; static { messageList = new ArrayList<>(); clientList = new ArrayList<>(); } public static void connect(MessageClient client) { clientList.add(client); System.out.println(client.getId() + "连接到服务器..."); client.pullMessage(); } public static List<Message> getMessageListBySender(String from) { Assert.isTrue(StringUtils.hasLength(from), () -> { throw new RuntimeException("from must not empty!!!"); }); return messageList.stream().filter(x -> x.from.equals(from)).collect(Collectors.toList()); } public static List<Message> getMessageListByReciver(String to) { Assert.isTrue(StringUtils.hasLength(to), () -> { throw new RuntimeException("to must not empty!!!"); }); return messageList.stream().filter(x -> x.to.equals(to)).collect(Collectors.toList()); } public static void reciveFromClient(Message message) { messageList.add(message); System.out.println("server get message from " + message.from + " to " + message.to); BroadCastToRecivers();//服务端接收到客户端消息,开始推送一轮消息 } private static void BroadCastToRecivers() { List<String> onlineClientIds = clientList.stream().map(MessageClient::getId).collect(Collectors.toList()); Map<String, List<Message>> collect = messageList.stream() .filter(x -> onlineClientIds.contains(x.to)) .collect(Collectors.groupingBy(Message::getTo)); Iterator<Map.Entry<String, List<Message>>> iterator = collect.entrySet().iterator(); while(iterator.hasNext()) { Map.Entry<String, List<Message>> next = iterator.next(); Optional<MessageClient> any = clientList.stream().filter(x -> next.getKey().equals(x.getId())).findAny(); if(any.isPresent()) { MessageClient messageClient = any.get(); List<Message> value = next.getValue(); messageClient.reciveMessage(value); messageList.removeAll(value); } else { System.err.println(next.getKey() + ":客户端已断开连接..."); } } } public static void disConnect(String clientId) { Optional<MessageClient> any = clientList.stream().filter(x -> x.getId().equals(clientId)).findAny(); if(any.isPresent()) { MessageClient messageClient = any.get(); clientList.remove(messageClient); messageClient.setStop(true); System.out.println(clientId+":离线了..."); } } }
4.总结:对于消息如何到达客户端,是通过服务端推模式还是客户端的拉模式?在实践过程中走了很多弯路,先是尝试在服务端通过开启线程轮询检测消息,然后尝试发送给接收者,对于离线的用户采用方法回调的方式每隔一秒推送一次,直到客户端上限再推送成功,采取这种方式的弊端是,一旦有离线用户,消息发给了他,就会阻塞别的消息发送,这很不理想。考虑到用户上线时可以主动去服务端拉取消息,服务端接收到消息后筛选出接收者在线的消息,直接推送,这样就能解决客户端离线时无用的轮询,导致服务端压力大,也可在客户端上线期间,有消息也能由服务端主动推到客户端,推拉结合实现消息的可靠投递。