实现即时通讯之BIO编程

简介: 实现即时通讯之BIO编程

BIO模式下实现简单的即时通讯

需求:需要实现一个客户端的消息可以发送给所有的客户端去接收。

(群聊实现)

20200401134307494.png

项目功能演示项目

案例说明功能清单简单说明:


1.客户端登陆功能

可以启动客户端进行登录,客户端登陆只需要输入用户名和服务端ip地址即可。


2.在线人数实时更新。

客户端用户户登陆以后,需要同步更新所有客户端的联系人信息栏。


3.离线人数更新

检测到有客户端下线后,需要同步更新所有客户端的联系人信息栏。


4.群聊

任意一个客户端的消息,可以推送给当前所有客户端接收。


5.私聊

可以选择某个员工,点击私聊按钮,然后发出的消息可以被该客户端单独接收。


6.@消息

可以选择某个员工,然后发出的消息可以@该用户,但是其他所有人都能


7.消息用户和消息时间点

服务端可以实时记录该用户的消息时间点,然后进行消息的多路转发或者选择。


项目启动步骤:

1.首先需要启动服务端,点击ServerChat类直接右键启动,显示服务端启动成功!


2.其次,点击客户端类ClientChat类,在弹出的方框中输入服务端的ip和当前客户端的昵称

20200401134307494.png

3.登陆进入后的聊天界面如下,即可进行相关操作。

20200401134307494.png

如果直接点击发送,默认发送群聊消息

如果选中右侧在线列表某个用户,默认发送@消息

如果选中右侧在线列表某个用户,然后选择右下侧私聊按钮默,认发送私聊消息。

服务端设计

服务端接收多个客户端逻辑目标服务端需要接收多个客户端的接入。


实现步骤


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客户端,弹出登陆界面

20200401134307494.png

点击登陆,然后出现聊天界面,当有用户上线,右侧就会更新新上线用户

20200401134307494.png

有兴趣的朋友可关注公众号【一起收破烂】,回复【004】,获取源码自行测试~

相关文章
|
7月前
|
Java 数据挖掘 开发者
Java网络编程进阶:Socket通信的高级特性与应用
【6月更文挑战第21天】Java Socket通信是分布式应用的基础,涉及高级特性如多路复用(Selector)和零拷贝,提升效率与响应速度。结合NIO和AIO,适用于高并发场景如游戏服务器和实时数据分析。示例展示了基于NIO的多路复用服务器实现。随着技术发展,WebSockets、HTTP/2、QUIC等新协议正变革网络通信,掌握Socket高级特性为应对未来挑战准备。
60 1
|
5月前
|
编解码 NoSQL Redis
(十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
113 3
|
6月前
|
Java API 开发者
Java网络编程基础与Socket通信实战
Java网络编程基础与Socket通信实战
|
8月前
|
前端开发 网络协议 Java
Netty入门指南:从零开始的异步网络通信
Netty入门指南:从零开始的异步网络通信
192 0
|
8月前
Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力
Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力
85 0
|
存储 网络协议 Linux
解密异步IO:使用C++进行高效的网络编程
解密异步IO:使用C++进行高效的网络编程
|
存储 消息中间件 缓存
Netty入门到超神系列-零拷贝技术
内存主要作用是在计算机运行时为操作系统和各种程序提供临时储存,操作系统的进程和进程之间是共享CPU和内存资源的。为了防止内存泄露需要一套完善且高效的内存管理机制。因此现代操作系提供了一种基于主内存抽象出来的概念:虚拟内存(Virtual Memory)。 虚拟内存 虚拟内存是计算机系统内存管理的一种技术,主要为每个进程提供私有的地址空间,让每个进程拥有一片连续完整的内存空间。而实际上,虚拟内存通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换,加载到物理内存中来 物理内存 物理内存指通过内存条而获得的内存空间,而虚拟内存则是指将硬盘的一块区域划分来
219 0
|
JSON 网络协议 关系型数据库
计网 - 网络 I/O 模型:BIO、NIO 和 AIO 有什么区别?
计网 - 网络 I/O 模型:BIO、NIO 和 AIO 有什么区别?
120 0
|
域名解析 设计模式 缓存
【Netty】网络协议与BIO/NIO编程
做为Netty系列第一篇文章,简要介绍学习Netty需要掌握的计算机网络知识,面试和学习中的重点TCP和UDP两个协议,并实现BIO编程对不同协议(TCP/UDP)的开发方法,同时也详细介绍了NIO编程的开发步骤和开发方法供读者参考
【Netty】网络协议与BIO/NIO编程
|
前端开发 网络协议 JavaScript
Go语言实现即时通讯-上(前端)
之前学习 websocket 基本上都是后端,开几个黑窗口来模拟消息的收发。等到自己真正实战的时候,会发现无从下手,本文会将前后端串联起来,从实战出发,分为上下文,此篇先介绍下前端的 websocket 实现及思路。
210 0
Go语言实现即时通讯-上(前端)