Java实现Socket网络聊天室

孤街醉人 提交于 2019-12-10 08:11:59

  最近在学Socket编程,为了巩固知识,简单实现了一个网络聊天室;目前只实现了个群聊功能,有时间继续更新和完善,下面附上代码截图,代码上都有详细的注释,如果有看不懂的地方,欢迎留言或私信我。

  一、源代码地址:https://github.com/aa792978017/ChatRoom

  二、本地多客户端调试效果图:(为了方便本地调试区分不同客户端,这里把用户名都设置为了“路人xxxx”,可以调整为用户名)

  三、项目结构:

              

四、类代码分析:

  1、ChatProtocol类:存放了一些公共的变量和方法。

/*
 * Copyright 2019-2022 the original author or authors.
 */

public class ChatProtocol {

  /** 服务端口号 */
  public static final int PORT_NUM = 8080;

  /** 消息类型为登录 */
  public static final char CMD_LOGIN = 'A';

  /** 消息类型为私发信息,暂未用上 */
  public static final char CMD_MESG = 'B';

  /** 消息类型为登出 */
  public static final char CMD_QUIT = 'C';

  /** 消息类型为广播(目前所有消息都为广播) */
  public static final char CMD_BCAST = 'D';

  /** 分隔符,用于分隔消息里的不同部分,识别各种信息*/
  public static final int SEPARATOR = '|';

  /**
   * 判断消息体里面是否含有登录名
   * @param message 消息
   * @return 是否含有登录名
   */
  public static boolean isValidLoginName(String message) {
   return message != null && message.length() != 0;
  }
}

  2、ChatServer类:聊天室服务端实现类。

/*
 * Copyright 2019-2022 the original author or authors.
 */

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Java聊天服务器
 */
public class ChatServer {

  /**
   *
   */
  protected final static String CHATMASTER_ID = "Server";

  /**
   * 任何handle和消息之间分隔串
   */
  protected final static String SEP = ": ";

  /**
   * 服务器套接字
   */
  protected ServerSocket serverSocket;

  /**
   * 连接到服务器的客户端列表
   */
  protected List<ChatHandler> clients;

  /**
   * 调试状态,调整是否以调试的形式启动
   */
  private static boolean DEBUG = false;

  /**
   * main方法,仅构造一个ChatServer,永远不返回
   */
  public static void main(String[] args) throws IOException {
    System.out.println("java.ChatServer 0.1 starting...");
    // 启动时传入-debug,则以debug模式启动,会打印调试信息
    if (args.length == 1 && args[0].equals("-debug")) {
      DEBUG = true;
    }
    // 服务器启动后不会终止
    ChatServer chatServer = new ChatServer();
    chatServer.runServer();
    // 如果终止了,说明出现了异常,程序停止了
    System.out.println("**Error* java.ChatServer 0.1 quitting");
  }

  /**
   * 构造并运行一个聊天服务
   * @throws IOException
   */
  public ChatServer() throws IOException {
    clients = new ArrayList<>();
    serverSocket = new ServerSocket(ChatProtocol.PORT_NUM);
    System.out.println("Chat Server Listening on port " + ChatProtocol.PORT_NUM);
  }

  /**
   * 运行服务器
   */
  public void runServer() {
    try {
      // 死循环持续接收所有访问的socket
      while (true) {
        // 开启监听
        Socket userSocket = serverSocket.accept();
        // 输入连接到服务器的客户端主机名
        String hostName = userSocket.getInetAddress().getHostName();
        System.out.println("Accepted from " + hostName);
        // 每个客户端的连接都开启一个线程来负责通信
        ChatHandler client = new ChatHandler(userSocket, hostName);
        // 给客户端返回登录消息
        String welcomeMessage;
        synchronized (clients) {
          // 把处理用户连接信息的线程引用保存起来
          clients.add(client);
          // 构建欢迎信息
          if (clients.size() == 1) {
            welcomeMessage = "Welcome! you're the first one here";
          } else {
            welcomeMessage = "Welcome! you're the latest of " + clients.size() + " users.";
          }
        }
        // 启动客户端线程来处理通信
        client.start();
        client.send(CHATMASTER_ID, welcomeMessage);
      }
    } catch (IOException ex) {
      // 当前客户端处理报错,输出错误信息,但不抛出异常,服务器需要继续运行,服务其他客户端
      log("IO Exception in runServer:  " + ex.toString());
    }
  }

  /**
   * 日志打印
   * @param logMessage 需要打印的信息
   */
  protected void log(String logMessage) {
    System.out.println(logMessage);
  }




  /**
   * 每个线程处理一个用户对话
   */
  protected class ChatHandler extends Thread {

    /** 客户端套接字 */
    protected Socket clientSocket;

    /** 从套接字读取数据 */
    protected BufferedReader is;

    /** 从套接字上发送行数据 */
    protected PrintWriter pw;

    /** 客户端的IP */
    protected String clientIp;

    /** 用户句柄(名称)*/
    protected String login;

    /** 构造一个聊天程序 */
    public ChatHandler(Socket clientSocket, String clientIp) throws IOException {
      this.clientSocket = clientSocket;
      this.clientIp = clientIp;
      // TODO 正式使用时删掉下面这一行,这里为了本地运行多个客户端时,可以区分用户
      this.clientIp = "路人"+ UUID.randomUUID().toString().substring(0,8);
      is = new BufferedReader(
          new InputStreamReader(clientSocket.getInputStream(),"utf-8"));
      pw = new PrintWriter(
          new OutputStreamWriter(clientSocket.getOutputStream(),"utf-8"), true);
    }


    @Override
    public void run() {
      String line;
      try {
        /**
         * 只要客户端保持连接,我们就应该一直处于这个循环
         * 当循环结束时候,我们断开这个连接
         */
        while ((line = is.readLine()) != null) {
          // 消息的第一个字符是消息类型
          char messageType = line.charAt(0);
          line = line.substring(1);
          switch (messageType) {
            case ChatProtocol.CMD_LOGIN:  // 登录消息类型:A + login(登录名)
              // 登录信息内不包含登录名
              if (!ChatProtocol.isValidLoginName(line)) {
                // 回复登录消息,登录信息不合法
                send(CHATMASTER_ID, "LOGIN " + line + " invalid");
                // 日志记录
                log("LOGIN INVALID from " + clientIp);
                continue;
              }
              // 包含登录名
              login = line;
              broadcast(CHATMASTER_ID, login + " joins us, for a total of " +
                  clients.size() + " users");
              break;
            case ChatProtocol.CMD_MESG:  // 私人消息类型:B + 接受用户名 + :+ message(消息内容)ps:私法消息在客户端上还没有实现
              // 未登录,无法发送消息
              if (login == null) {
                send(CHATMASTER_ID, "please login first");
                continue;
              }
              // 解析出接收信息的用户名,消息内容
              int where = line.indexOf(ChatProtocol.SEPARATOR);
              String recip = line.substring(0, where);
              String message = line.substring(where + 1);
              log("MESG: " + login + "-->" + recip + ": " + message);
              // 查找接收消息的用户线程
              ChatHandler client = lookup(recip);
              if (client == null) {
                // 找不到后,发送该用户未登陆
                psend(CHATMASTER_ID, recip + "not logged in");
              } else {
                // 找到用户,把信息私人发送过去
                client.psend(login, message);
              }
              break;
            case ChatProtocol.CMD_QUIT: // 离线消息类型: C
              broadcast(CHATMASTER_ID, "Goodbye to " + login + "@" + clientIp);
              close();
              return; // 这个时候,该ChatHandler线程结束
            case ChatProtocol.CMD_BCAST: // 广播消息类型: D + message(消息内容)
              if (login != null) {
                // this.send(login + "@" + clientIp , line);
                login = clientIp; // TODO 正式使用的时候去除这一行,用于本地多客户端调试
                broadcast(login, line);
              } else {
                // 记录谁广播了消息,消息内容是什么
                log("B<L FROM " + clientIp);
              }
              break;
            default: // 消息类型无法识别
              log("Unknown cmd " + messageType + " from" + login + "@" + clientIp);
          }
        }
      } catch (IOException ex) {
        log("IO Exception: " + ex.toString());
      } finally {
        // 客户端套接字结束(客户端断开连接,用户下线)
        System.out.println(login + SEP + "All Done");
        String message = "This should never appear";
        synchronized (clients) {
          // 移除离线的用户
          clients.remove(this);
          if (clients.size() == 0) {
            System.out.println(CHATMASTER_ID + SEP + "I'm so lonely I could cry...");
          } else if (clients.size() == 1) {
            message = "Hey, you're talking to yourself again";
          } else {
            message = "There are now " + clients.size() + " users";
          }
        }
        // 广播目前的聊天室状态
        broadcast(CHATMASTER_ID, message);
      }
    }

    /**
     * 断开客户端连接
     */
    protected void close() {
      // 客户端socket本来为null
      if (clientSocket == null) {
        log("close when not open");
        return;
      }

      try {
        // 关闭连接的客户端socket
        clientSocket.close();
        clientSocket = null;
      } catch (IOException ex) {
        log("Failure during close to " + clientIp);
      }
    }

    /**
     * 某个用户发送消息
     * @param sender 发送消息的用户
     * @param message 消息内容
     */
    public void send(String sender, String message) {
      pw.println(sender + SEP + message);
    }

    /**
     * 发送私人消息
     * @param sender 接受消息的用户
     * @param message 消息内容
     */
    public void psend(String sender, String message) {
      send("<*" + sender + "*>", message);
    }

    /**
     * 向所有用户发送一条消息
     * @param sender 发送者
     * @param message 消息内容
     */
    public void broadcast(String sender, String message) {
      System.out.println("Boradcasting " + sender + SEP + message);
      // 对client遍历,调用其send方法,进行广播
      clients.forEach(client -> {
        if (DEBUG) {
          // 日志打印向某用户发送消息
          System.out.println("Sending to " + client);
        }
        client.send(sender, message);


      });
      // 打印日志,完成了广播
      if (DEBUG) {
        System.out.println("Done broadcast");
      }
    }

    /**
     * 通过用户昵称,查找某用户
     * @param nick 用户昵称
     * @return 返回用户的处理线程
     */
    protected ChatHandler lookup(String nick) {
      // 同步,遍历查找
      synchronized (clients) {
        for (ChatHandler client: clients) {
          if (client.login.equals(nick)) {
            return client;
          }
        }
      }
      // 找不到返回null
      return null;
    }

    /** ChatHandler的字符串形式 */
    public String toString() {
      return "ChatHandler[" + login +"]";
    }
  }
}

  3、ChatClient类:聊天室客户端实现类。

/*
 * Copyright 2019-2022 the original author or authors.
 */

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Font;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.UUID;
import javax.swing.JFrame;

/**
 * 聊天客户端
 */
public class ChatClient extends JFrame {

  private static final long serialVersionUID = -2270001423738681797L;

  /** 这里获取系统用户名,如果获取失败,则通过UUID随机生成,取前八位 */
  private static final String userName =
      System.getProperty("user.name",
          "路人"+ UUID.randomUUID().toString().substring(0,8));

  /** logged-in-ness的状态 */
  protected boolean loggedIn;

  /** 界面主框架*/
  protected JFrame windows;

  /** 默认端口号*/
  protected static final int PORT_NUM = ChatProtocol.PORT_NUM;

  /** 实际端口号*/
  protected int port;

  /** 网络客户端套接字*/
  protected Socket sock;

  /** 用于从套接字读取数据(读取其他聊友发送的消息) */
  protected BufferedReader is;

  /** 用于在套接字上发送行数据(即用户发送消息到聊天室) */
  protected PrintWriter pw;

  /** 用于输入TextField tf */
  protected TextField input;

  /** 用于显示对话的TextArea(消息展示的界面) */
  protected TextArea messageView;

  /** 登陆按钮 */
  protected Button loginButton;

  /** 注销按钮 */
  protected Button logoutButton;

  /** 应用程序的标题 */
  protected static String TITLE = "Chat Room Client";

  /** 这里设置服务器的地址,默认为本地 */
  protected String serverHost = "localhost";

  /**
   * 设置GUI
   */
  public ChatClient() {
    windows = this;
    windows.setTitle(TITLE);
    windows.setLayout(new BorderLayout());
    port = PORT_NUM;

    // GUI,消息展示界面样式
    messageView = new TextArea(30, 80);
    messageView.setEditable(false);
    messageView.setFont(new Font("Monospaced", Font.PLAIN, 15));
    windows.add(BorderLayout.NORTH, messageView);

    // 创建一个板块
    Panel panel = new Panel();

    // 在板块上添加登陆按钮
    panel.add(loginButton = new Button("Login"));
    loginButton.setEnabled(true);
    loginButton.requestFocus();
    loginButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        login();
        loginButton.setEnabled(false);
        logoutButton.setEnabled(true);
        input.requestFocus();
      }
    });

    // 在板块上添加注销按钮
    panel.add(logoutButton = new Button("Logout"));
    logoutButton.setEnabled(false);
    logoutButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        logout();
        loginButton.setEnabled(true);
        logoutButton.setEnabled(false);
        loginButton.requestFocus();
      }
    });

    //  消息输入框
    panel.add(new Label("Message here..."));
    input = new TextField(40);
    input.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // 判断有无登录,登录后才能发送消息
        if (loggedIn) {
          // 以广播的方式发送出去,所有人可见
          pw.println(ChatProtocol.CMD_BCAST + input.getText());
          // 发送后,发消息输入框置空
          input.setText("");
        }
      }
    });

    // 把消息输入框加入板块
    panel.add(input);

    // 把板块加入主界面的最下方
    windows.add(BorderLayout.SOUTH, panel);
    windows.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    windows.pack();
  }


  /**
   * 登陆到聊天室
   */
  public void login() {
    showStatus("准备登录!");
    // 如果已经的登录,则返回
    if (loggedIn) {
      return;
    }
    // 没有登录,开始尝试连接到聊天室服务器
    try {
      // 聊天室服务器地址使用了默认的localhost(127.0.0.1)地址
      sock = new Socket(serverHost, port);
      TITLE += userName;
      is = new BufferedReader(
          new InputStreamReader(sock.getInputStream(),"utf-8"));
      pw = new PrintWriter(
          new OutputStreamWriter(sock.getOutputStream(),"utf-8"),true);
      showStatus("获取到聊天服务器的socket");
      // 现在假登录,不需要输入密码
      pw.println(ChatProtocol.CMD_LOGIN + userName);
      loggedIn = true;
    } catch (IOException ex) {
      showStatus("获取不到服务器的socket " + serverHost + "/" + port + ": " + ex.toString());
      windows.add(new Label("获取socket失败: " + ex.toString()));
      return;
    }

    //构建和启动reader: 读取服务器的消息到消息展示区
    new Thread(new Runnable() {
      @Override
      public void run() {
        String line;
        try {
          // 只要登录并且服务器有消息可读
          while (loggedIn && ((line = is.readLine()) != null)) {
            // 每读取一行消息,换行
            messageView.append(line + "\n");
          }
        } catch (IOException ex) {
          showStatus("与其他客户端连接中断!\n" + ex.toString());
          return;
        }
      }
    }).start();
  }

  /** 登出聊天室 */
  public void logout() {
    // 如果已经登出了,则直接返回
    if(!loggedIn) {
      return;
    }
    // 修改登录状态,释放socket资源
    loggedIn = false;
    try {
      if (sock != null) {
        sock.close();
      }
    } catch (Exception ex) {
      // 处理异常
      System.out.println("聊天室关闭异常: " + ex.toString());
    }
  }

  /**
   * 控制输出状态,便于调试
   * @param message 状态信息
   */
  public void showStatus(String message) {
    System.out.println(message);
  }

  /** main方法 允许客户端作为一个应用程序*/
  public static void main(String[] args) {
    ChatClient room = new ChatClient();
    room.pack();
    room.setVisible(true);
  }

}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!