BIO模式下实现简单的即时通讯
需求:需要实现一个客户端的消息可以发送给所有的客户端去接收。
(群聊实现)
项目功能演示项目
案例说明功能清单简单说明:
1.客户端登陆功能
可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。
2.在线人数实时更新。
客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。
3.离线人数更新
检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。
4.群聊
任意一个客户端的消息,可以推送给当前所有客户端接收。
5.私聊
可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。
6.@消息
可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能
7.消息用户和消息时间点
服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。
项目启动步骤:
1.首先需要启动服务端,点击ServerChat类直接右键启动,显示服务端启动成功!
2.其次,点击客户端类ClientChat类,在弹出的方框中输入服务端的ip和当前客户端的昵称
3.登陆进入后的聊天界面如下,即可进行相关操作。
如果直接点击发送,默认发送群聊消息
如果选中右侧在线列表某个用户,默认发送@消息
如果选中右侧在线列表某个用户,然后选择右下侧私聊按钮默,认发送私聊消息。
服务端设计
服务端接收多个客户端逻辑目标服务端需要接收多个客户端的接入。
实现步骤
1.服务端需要接收多个客户端,目前我们采取的策略是一个客户端对应一个服务端线程。
2.服务端除了要注册端口以外,还需要为每个客户端分配一个独立线程处理与之通信。
代码实现
服务端主体代码,主要进行端口注册,和接收客户端,分配线程处理该客户端请求
public class ServerChat { /** 定义一个集合存放所有在线的socket */ public static Map<Socket, String> onLineSockets = new HashMap<>(); public static void main(String[] args) { try { /** 1.注册端口 */ ServerSocket serverSocket = new ServerSocket(Constants.PORT); /** 2.循环一直等待所有可能的客户端连接 */ while(true){ Socket socket = serverSocket.accept(); /**3. 把客户端的socket管道单独配置一个线程来处理 */ new ServerReader(socket).start(); } } catch (Exception e) { e.printStackTrace(); } } }
服务端分配的独立线程类负责处理该客户端Socket的管道请求。
class ServerReader extends Thread { private Socket socket; public ServerReader(Socket socket) { this.socket = socket; } @Override public void run() { try { } catch (Exception e) { e.printStackTrace(); } } }
常量包负责做端口配置
public class Constants { /** 常量 */ public static final int PORT = 7778 ; }
这样就实现了服务端可以接收多个客户端请求。接下来,我们要实现服务端消息的接收发送,以及离线监测
服务端接收登陆消息以及监测离线
主要是在服务端处理客户端的线程的登陆消息,需要注意的是,服务端需要接收客户端的消息可能有很多种。分别是登陆消息,群聊消息,私聊消息 和@消息
这里需要约定如果客户端发送消息之前需要先发送消息的类型,类型我们使用信号值标志(1,2,3)。
1代表接收的是登陆消息
2代表群发| @消息
3代表了私聊消息
服务端的线程中有异常校验机制,一旦发现客户端下线会在异常机制中处理,然后移除当前客户端用户,把最新的用户列表发回给全部客户端进行在线人数更新。
代码实现
import com.netty.bio.demo4.util.Constants; import java.io.DataInputStream; import java.io.DataOutputStream; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Set; public class ServerReader extends Thread { private Socket socket; public ServerReader(Socket socket) { this.socket = socket; } @Override public void run() { DataInputStream dis = null; try { dis = new DataInputStream(socket.getInputStream()); /** 1.循环一直等待客户端的消息 */ while(true){ /** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */ int flag = dis.readInt(); if(flag == 1){ /** 先将当前登录的客户端socket存到在线人数的socket集合中 */ String name = dis.readUTF() ; System.out.println(name+"---->"+socket.getRemoteSocketAddress()); ServerChat.onLineSockets.put(socket, name); } writeMsg(flag,dis); } } catch (Exception e) { System.out.println("--有人下线了--"); // 从在线人数中将当前socket移出去 ServerChat.onLineSockets.remove(socket); try { // 从新更新在线人数并发给所有客户端 writeMsg(1,dis); } catch (Exception e1) { e1.printStackTrace(); } } } private void writeMsg(int flag, DataInputStream dis) throws Exception { // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); // 定义一个变量存放最终的消息形式 String msg = null ; if(flag == 1){ /** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */ StringBuilder rs = new StringBuilder(); Collection<String> onlineNames = ServerChat.onLineSockets.values(); // 判断是否存在在线人数 if(onlineNames != null && onlineNames.size() > 0){ for(String name : onlineNames){ rs.append(name+ Constants.SPILIT); } // 去掉最后的一个分隔符 msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT)); /** 将消息发送给所有的客户端 */ sendMsgToAll(flag,msg); } }else if(flag == 2 || flag == 3){ // 读到消息 群发的 或者 @消息 String newMsg = dis.readUTF() ; // 消息 // 得到发件人 String sendName = ServerChat.onLineSockets.get(socket); // 内容-- StringBuilder msgFinal = new StringBuilder(); // 时间 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE"); if(flag == 2){ msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis()*2)).append("\r\n"); msgFinal.append(" ").append(newMsg).append("\r\n"); sendMsgToAll(flag,msgFinal.toString()); }else if(flag == 3){ msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis()*2)).append("对您私发\r\n"); msgFinal.append(" ").append(newMsg).append("\r\n"); // 私发 // 得到给谁私发 String destName = dis.readUTF(); sendMsgToOne(destName,msgFinal.toString()); } } } /** * @param destName 对谁私发 * @param msg 发的消息内容 * @throws Exception */ private void sendMsgToOne(String destName, String msg) throws Exception { // 拿到所有的在线socket管道 给这些管道写出消息 Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet(); for(Socket sk : allOnLineSockets){ // 得到当前需要私发的socket // 只对这个名字对应的socket私发消息 if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){ DataOutputStream dos = new DataOutputStream(sk.getOutputStream()); dos.writeInt(2); // 消息类型 dos.writeUTF(msg); dos.flush(); } } } private void sendMsgToAll(int flag, String msg) throws Exception { // 拿到所有的在线socket管道 给这些管道写出消息 Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet(); for(Socket sk : allOnLineSockets){ DataOutputStream dos = new DataOutputStream(sk.getOutputStream()); dos.writeInt(flag); // 消息类型 dos.writeUTF(msg); dos.flush(); } } }
客户端设计
启动客户端界面,登陆,刷新在线人数列表
实现步骤
客户端界面主要是GUI设计,主体页面分为登陆界面和聊天窗口,以及在线用户列表。
GUI界面读者可以自行复制使用。
登陆输入服务端ip和用户名后,要请求与服务端的登陆,然后立即为当前客户端分配一个读线程处理客户端的读数据消息。因为客户端可能随时会接收到服务端那边转发过来的各种即时消息信息。
客户端登陆完成,服务端收到登陆的用户名后,会立即发来最新的用户列表给客户端更新。
代码实现
客户端主体代码:
import com.netty.bio.demo4.util.Constants; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.DataOutputStream; import java.net.Socket; /** * 客户端界面 */ public class ClientChat implements ActionListener { /** 1.设计界面 */ private JFrame win = new JFrame(); /** 2.消息内容框架 */ public JTextArea smsContent =new JTextArea(23 , 50); /** 3.发送消息的框 */ private JTextArea smsSend = new JTextArea(4,40); /** 4.在线人数的区域 */ /** 存放人的数据 */ /** 展示在线人数的窗口 */ public JList<String> onLineUsers = new JList<>(); // 是否私聊按钮 private JCheckBox isPrivateBn = new JCheckBox("私聊"); // 消息按钮 private JButton sendBn = new JButton("发送"); // 登录界面 private JFrame loginView; private JTextField ipEt , nameEt , idEt; private Socket socket ; public static void main(String[] args) { new ClientChat().initView(); } private void initView() { /** 初始化聊天窗口的界面 */ win.setSize(650, 600); /** 展示登录界面 */ displayLoginView(); /** 展示聊天界面 */ //displayChatView(); } private void displayChatView() { JPanel bottomPanel = new JPanel(new BorderLayout()); //----------------------------------------------- // 将消息框和按钮 添加到窗口的底端 win.add(bottomPanel, BorderLayout.SOUTH); bottomPanel.add(smsSend); JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT)); btns.add(sendBn); btns.add(isPrivateBn); bottomPanel.add(btns, BorderLayout.EAST); //----------------------------------------------- // 给发送消息按钮绑定点击事件监听器 // 将展示消息区centerPanel添加到窗口的中间 smsContent.setBackground(new Color(0xdd,0xdd,0xdd)); // 让展示消息区可以滚动。 win.add(new JScrollPane(smsContent), BorderLayout.CENTER); smsContent.setEditable(false); //----------------------------------------------- // 用户列表和是否私聊放到窗口的最右边 Box rightBox = new Box(BoxLayout.Y_AXIS); onLineUsers.setFixedCellWidth(120); onLineUsers.setVisibleRowCount(13); rightBox.add(new JScrollPane(onLineUsers)); win.add(rightBox, BorderLayout.EAST); //----------------------------------------------- // 关闭窗口退出当前程序 win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); win.pack(); // swing 加上这句 就可以拥有关闭窗口的功能 /** 设置窗口居中,显示出来 */ setWindowCenter(win,650,600,true); // 发送按钮绑定点击事件 sendBn.addActionListener(this); } private void displayLoginView(){ /** 先让用户进行登录 * 服务端ip * 用户名 * id * */ /** 显示一个qq的登录框 */ loginView = new JFrame("登录"); loginView.setLayout(new GridLayout(3, 1)); loginView.setSize(400, 230); JPanel ip = new JPanel(); JLabel label = new JLabel(" IP:"); ip.add(label); ipEt = new JTextField(20); ip.add(ipEt); loginView.add(ip); JPanel name = new JPanel(); JLabel label1 = new JLabel("姓名:"); name.add(label1); nameEt = new JTextField(20); name.add(nameEt); loginView.add(name); JPanel btnView = new JPanel(); JButton login = new JButton("登陆"); btnView.add(login); JButton cancle = new JButton("取消"); btnView.add(cancle); loginView.add(btnView); // 关闭窗口退出当前程序 loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setWindowCenter(loginView,400,260,true); /** 给登录和取消绑定点击事件 */ login.addActionListener(this); cancle.addActionListener(this); } private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) { /** 得到所在系统所在屏幕的宽高 */ Dimension ds = frame.getToolkit().getScreenSize(); /** 拿到电脑的宽 */ int width1 = ds.width; /** 高 */ int height1 = ds.height ; System.out.println(width1 +"*" + height1); /** 设置窗口的左上角坐标 */ frame.setLocation(width1/2 - width/2, height1/2 -height/2); frame.setVisible(flag); } @Override public void actionPerformed(ActionEvent e) { /** 得到点击的事件源 */ JButton btn = (JButton) e.getSource(); switch(btn.getText()){ case "登陆": String ip = ipEt.getText().toString(); String name = nameEt.getText().toString(); // 校验参数是否为空 // 错误提示 String msg = "" ; // 12.1.2.0 // \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\ if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){ msg = "请输入合法的服务端ip地址"; }else if(name==null || !name.matches("\\S{1,}")){ msg = "姓名必须1个字符以上"; } if(!msg.equals("")){ /** msg有内容说明参数有为空 */ // 参数一:弹出放到哪个窗口里面 JOptionPane.showMessageDialog(loginView, msg); }else{ try { // 参数都合法了 // 当前登录的用户,去服务端登陆 /** 先把当前用户的名称展示到界面 */ win.setTitle(name); // 去服务端登陆连接一个socket管道 socket = new Socket(ip, Constants.PORT); //为客户端的socket分配一个线程 专门负责收消息 new ClientReader(this,socket).start(); // 带上用户信息过去 DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeInt(1); // 登录消息 dos.writeUTF(name.trim()); dos.flush(); // 关系当前窗口 弹出聊天界面 loginView.dispose(); // 登录窗口销毁 displayChatView(); // 展示了聊天窗口了 } catch (Exception e1) { e1.printStackTrace(); } } break; case "取消": /** 退出系统 */ System.exit(0); break; case "发送": // 得到发送消息的内容 String msgSend = smsSend.getText().toString(); if(!msgSend.trim().equals("")){ /** 发消息给服务端 */ try { // 判断是否对谁发消息 String selectName = onLineUsers.getSelectedValue(); int flag = 2 ;// 群发 @消息 if(selectName!=null&&!selectName.equals("")){ msgSend =("@"+selectName+","+msgSend); /** 判断是否选中了私法 */ if(isPrivateBn.isSelected()){ /** 私法 */ flag = 3 ;//私发消息 } } DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeInt(flag); // 群发消息 发送给所有人 dos.writeUTF(msgSend); if(flag == 3){ // 告诉服务端我对谁私发 dos.writeUTF(selectName.trim()); } dos.flush(); } catch (Exception e1) { e1.printStackTrace(); } } smsSend.setText(null); break; } } }
客户端socket处理线程:
import com.netty.bio.demo4.util.Constants; import java.io.DataInputStream; import java.net.Socket; class ClientReader extends Thread { private Socket socket; private ClientChat clientChat ; public ClientReader(ClientChat clientChat, Socket socket) { this.clientChat = clientChat; this.socket = socket; } @Override public void run() { try { DataInputStream dis = new DataInputStream(socket.getInputStream()); /** 循环一直等待客户端的消息 */ while(true){ /** 读取当前的消息类型 :登录,群发,私聊 , @消息 */ int flag = dis.readInt(); if(flag == 1){ // 在线人数消息回来了 String nameDatas = dis.readUTF(); // 展示到在线人数的界面 String[] names = nameDatas.split(Constants.SPILIT); clientChat.onLineUsers.setListData(names); }else if(flag == 2){ //群发,私聊 , @消息 都是直接显示的。 String msg = dis.readUTF() ; clientChat.smsContent.append(msg); // 让消息界面滾動到底端 clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length()); } } } catch (Exception e) { e.printStackTrace(); } } }
此处说明了如果启动客户端界面,以及登陆功能后,服务端收到新的登陆消息后,会响应一个在线列表用户回来给客户端更新在线人数!
运行测试
先运行ServerChat 服务端,再多次运行ClientChat客户端,弹出登陆界面
点击登陆,然后出现聊天界面,当有用户上线,右侧就会更新新上线用户
有兴趣的朋友可关注公众号【一起收破烂】,回复【004】,获取源码自行测试~