最近有同学秋招面试被问到聊天室项目,于是想自己去总结一下,这里只记录设计思路。
首先有一个通信内容协议类Request,它约定了通信双方遵守的规则,包括发送消息的类型,发送者,接收者等。parameters属性是一个Map
在网络中通信,当客户端和服务器不在同一台主机时,直接用object流传输消息不便于转换和恢复对象的状态信息和做持久化操作,同时Object流通常会比字符串更大,因为它包含了更多的元数据和状态信息,这会增加数据传输的大小和网络带宽的消耗,降低传输效率,所以还提供fromString和toString方法实现Request和String之间的转换。字符串格式key1=val1,key2=val2...。常量VK_S_01和VK_S_02是在转换成字符串前把发送内容中的 “=” 和 “,” 临时用特殊符号代替。
/** * 请求 通信的内容协议 */ public class Request { private static final String VK_01 = ","; private static final String VK_S_01 = "#vk01#"; private static final String VK_02 = "="; private static final String VK_S_02 = "#vk02#"; /** * 群聊的类型 */ public static final String MSG_TYPE_ALL = "all"; /** * 私聊的类型 */ public static final String MSG_TYPE_ONE = "one"; /** * 抖动的类型 */ public static final String MSG_TYPE_SHAKE = "shake"; /** * 登陆 */ public static final String MSG_TYPE_LOGIN = "login"; /** * 刷新列表 */ public static final String MSG_TYPE_REFRESH = "refresh"; private Mapparameters = new HashMap (); public String getParameter(String key) { return parameters.get(key); } public void setParameter(String key,String value) { parameters.put(key, value); } public String toString() { Set keys = parameters.keySet(); StringBuffer buffer = new StringBuffer(); for (String s : keys) { String key = s.replaceAll(VK_01, VK_S_01) .replaceAll(VK_02, VK_S_02); String value = parameters.get(s) .replaceAll(VK_01, VK_S_01) .replaceAll(VK_02, VK_S_02); buffer.append(key + "=" + value+","); } buffer.deleteCharAt(buffer.lastIndexOf(",")); return buffer.toString(); } public static Request fromString(String value) { Request r = new Request(); String[] values = value.split(","); for (String s : values) { String[] arr = s.split("="); String key = arr[0].replaceAll(VK_S_01, VK_01) .replaceAll(VK_S_02, VK_02); String val = arr[1].replaceAll(VK_S_01, VK_01) .replaceAll(VK_S_02, VK_02); r.setParameter(key, val); } return r; } }
其次再有一个客户端类,用于获取包括发送消息的类型,发送者,接收者等,通过Request对象设置parameters的参数,在通过socket对象把消息发出去。
public class ChatClient { private String nickName ; private Socket s ; private BufferedReader reader; private PrintWriter pw; private void registHandler() { // 获取要发送的消息的内容 // 获取类型 // Request r = new Request(); // r.setParameter("type", Request.MSG_TYPE_ONE); // r.setParameter("toWho", ""); // r.setParameter("fromWho", ""); //点击事件 写数据 } public void connect() { // 创建socket对象,连接到服务器 //初始化 reader和writer new Thread(){ public void run() { //不停的读数据,读到数据 //1.如果是userList:开头 // 刷新在线用户列表 //2 如果消息是shake 窗口的抖动 //3 否则将消息放入到msgArea中 } }.start(); } public static void main(String[] args) { ChatClient c = new ChatClient(); c.connect(); } }
/** *服务器类 */
public class ChatServer { public void start() { try { System.out.println("开启服务器"); ServerSocket ss = new ServerSocket(9999); System.out.println("正在监听9999端口"); while(true) { Socket s = ss.accept(); new ServerThread(s).start(); } } catch (Exception e) { System.out.println("服务器出现异常," + e.getMessage()); } } }
服务器类ChatServer也就是服务器主线程,负责读取客户端发送消息内容,变成Request对象,再根据类型做不同的事情。服务器要连多个客户端需要一直处于监听状态,对socket的监听写在while(true)中,为了避免先连接进来的客户端输入慢导致阻塞状态影响后面需要连接的客户端,设计一个服务器线程类,专门处理socket连接进来以后的逻辑。服务器类只做监听socket这一件事。
/** * 服务器线程 * <一句话功能简述> */
public class ServerThread extends Thread { private MessageQueue queue; private Socket s; public ServerThread(Socket s) { this.s = s; } @Override public void run() { //读取客户端的内容 BufferedReader reader = null; try { reader = new BufferedReader( new InputStreamReader( s.getInputStream() ) ); while(true) { synchronized (queue){ if(queue.isFull()){ try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { try { String msg = reader.readLine(); // 客户端 一定是创建一个Request对象 // 调用该对象的toString方法 Request r = Request.fromString(msg); // 登陆功能 String type = r.getParameter("type"); if(Request.MSG_TYPE_LOGIN.equals(type)) { //将用户昵称放入到fromWho中 String name = r.getParameter("fromWho"); Application.add(name, s); // 登陆成功,应该给所有的客户端发送消息 // 让他们刷新在线用户列表 r = new Request(); r.setParameter("type", Request.MSG_TYPE_REFRESH); } // 客户端一定要通过setParameter放一个key为type的东西 // 这个东西的value一定是常量之一 // 这里应该是线程中的生产者 消费者 模式 queue.add(r); queue.notifyAll(); } catch (Exception e) { e.printStackTrace(); } } } } } catch (Exception e) { e.printStackTrace(); } //变成Request对象 //根据类型做不同的事情 } }
其中,当获取到 String type = r.getParameter("type")以后,如果在这里去处理不同消息的类型,是不合适的,假设需要做一个群发客户端的功能,在线量很大的情况下需要发送大量消息,这个操作会非常耗时,而在消息发送期间,如果又下一条消息过来,服务器线程就读不到了。所以,应该还有另外一个线程去处理这件事。
在此之前先定义一个消息队列类,这样做的好处主要有两种:1.解耦 消息队列可以将系统内部的不同组件解耦,使它们之间通过消息进行通信,而不是直接调用。这样一来,系统的各个组件可以独立演化,不会直接依赖彼此的状态和实现细节,从而提高系统的可维护性和灵活性。2.异步通信 使用消息队列可以实现异步通信,即发送方不需要等待接收方的即时响应。
/** * 消息队列 */
public class MessageQueue { private Queuemsgs = new ArrayBlockingQueue (100); public void add(Request request) { msgs.offer(request); } public Request get() { return msgs.poll(); } public boolean isEmpty() { return msgs.isEmpty(); } public boolean isFull() { return msgs.size()==100; } }
再定义一个线程类读队列消息并处理这些消息。
/** * 消息的发送队列 */
public class SendMsgThread extends Thread { private MessageQueue queue; private void sendMsg(String msg,Socket s) { PrintWriter pw = null; try { pw = new PrintWriter(s.getOutputStream()); pw.println(msg); pw.flush(); } catch (Exception e) { e.printStackTrace(); } } @Override public void run() { while(true) { synchronized (queue){ if(!queue.isEmpty()) { Request r = queue.get(); queue.notifyAll(); String msgType = r.getParameter("type"); String fromWho = r.getParameter("fromWho"); String toWho = r.getParameter("toWho"); String msg = r.getParameter("msg"); if(Request.MSG_TYPE_ALL.equals(msgType)) { //群发给所有的客户端 Listall = Application.getAll(); for (int i = 0; i < all.size(); i++) { sendMsg(msg, all.get(i)); } } if(Request.MSG_TYPE_ONE.equals(msgType)) { // 发给某个客户端 // 客户端在创建Request对象的时候, // 要使用setParameter方法设置fromWho以及toWho Socket s = Application.get(fromWho); Socket s1 = Application.get(toWho); sendMsg(msg, s1); sendMsg(msg, s); } if(Request.MSG_TYPE_SHAKE.equals(msgType)) { //抖动所有客户端的窗口 //msg一定是一个特殊的字符串 List all = Application.getAll(); for (int i = 0; i < all.size(); i++) { sendMsg(msg, all.get(i)); } } if(Request.MSG_TYPE_REFRESH.equals(msgType)) { List all = Application.getAll(); msg = "userList:" + Application.getAllOnLineUserNames(); for (int i = 0; i < all.size(); i++) { sendMsg(msg, all.get(i)); } } }else { try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
这里是典型的多生产者+单消费者+共享池模式。ServerThread 是生产者,SendMsgThread是消费者,MessageQueue做共享池。
SendMsgThread中有两个问题:1.如果我要做一个群发功能,需要获取所有的socket对象;2.如果我做私聊,需要获取某个特定的socket,这些socket对象相对于整个服务器应该是全局的。因此需要把所有的socket保存起来,用一个map去保存,name做key。
定义一个应用类去做这件事
/** * 应用程序类 * <保存所有的socket> */ public class Application { private static Mapsockets = new HashMap (); public static void add(String name,Socket s) { sockets.put(name, s); } public static Socket get(String name) { return sockets.get(name); } public static List getAll() { List all = new ArrayList (); all.addAll(sockets.values()); return all; } public static String getAllOnLineUserNames() { StringBuffer buffer = new StringBuffer(); for(String k : sockets.keySet()) { buffer.append(k+","); } buffer.deleteCharAt(buffer.lastIndexOf(",")); return buffer.toString(); } } ServerThread 拿到Request后去做登录功能,保存name String name = r.getParameter("fromWho"); Application.add(name, s); 这样SendMsgThread在处理不同类型消息时就可以直接从Application就可以获取需要的socket。