实验目的:
使用客户机/服务器模式、基于TCP协议编写一对多“群聊”程序。其中客户机端单击“连接服务器”或“断开连接”按钮,均能即时更新服务器和所有客户机的在线人数和客户名。
实验要求:
设计一对多的网络聊天程序,要求:
1、基于TCP/IP设计聊天程序
2、采用图形界面设计
3、能够进行一对多聊天
项目截图
服务器端代码:
import javax.swing.*; import javax.swing.border.TitledBorder; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Vector; public class Server extends JFrame { // TODO 该图形界面拥有三块区域,分别位于上、中、下 (up、middle、down)。 private JPanel panUp = new JPanel(); private JPanel panMid = new JPanel(); private JPanel panDown = new JPanel(); // panUp 区域的子节点定义,标签、输入框、按钮 private JLabel lblLocalPort = new JLabel("本机服务器监听端口:"); protected JButton butStart = new JButton("启动服务器"); protected JTextField tfLocalPort = new JTextField(25); // panMid 区域的子节点定义,显示框 以及 滚动条 protected JTextArea taMsg = new JTextArea(25, 25); JScrollPane scroll = new JScrollPane(taMsg); // panDown 区域的子节点定义,lstUsers在线用户界面 JList lstUsers = new JList(); // TODO 以下是存放数据的变量 public static int localPort = 8000; // 默认端口 8000 static int SerialNum = 0; // 用户连接数量 ServerSocket serverSocket; // 服务器端 Socket ArrayList<AcceptRunnable.Client> clients = new ArrayList<>(); // 用户连接对象数组 Vector<String> clientNames = new Vector<>(); // lstUsers 中存放的数据 // TODO 构造方法 public Server() { init(); } // TODO 初始化方法:初始化图形界面布局 private void init() { // panUp 区域初始化:流式区域 panUp.setLayout(new FlowLayout()); panUp.add(lblLocalPort); panUp.add(tfLocalPort); panUp.add(butStart); tfLocalPort.setText(String.valueOf(localPort)); butStart.addActionListener(new startServerHandler()); // 注册 "启动服务器" 按钮点击事件 // panMid 区域初始化 panMid.setBorder(new TitledBorder("监听消息")); taMsg.setEditable(false); panMid.add(scroll); // panDown 区域初始化 panDown.setBorder(new TitledBorder("在线用户")); panDown.add(lstUsers); lstUsers.setVisibleRowCount(10); // 图形界面的总体初始化 + 启动图形界面 this.setTitle("服务器端"); this.add(panUp, BorderLayout.NORTH); this.add(panMid, BorderLayout.CENTER); this.add(panDown, BorderLayout.SOUTH); this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); this.setPreferredSize(new Dimension(600, 400)); this.pack(); this.setVisible(true); } // TODO “启动服务器”按钮的动作事件监听处理类 private class startServerHandler implements ActionListener { @Override public void actionPerformed(ActionEvent e) { try { // 当点击按钮时,获取端口设置并启动新进程、监听端口 localPort = Integer.parseInt(tfLocalPort.getText()); serverSocket = new ServerSocket(localPort); Thread acptThrd = new Thread(new AcceptRunnable()); acptThrd.start(); taMsg.append("**** 服务器(端口" + localPort + ")已启动 ****\n"); } catch (Exception ex) { System.out.println(ex); } } } // TODO 接受用户连接请求的线程关联类 private class AcceptRunnable implements Runnable { public void run() { // 持续监听端口,当有新用户连接时 再开启新进程 while (true) { try { Socket socket = serverSocket.accept(); // 新的用户已连接,创建 Client 对象 Client client = new Client(socket); taMsg.append("——客户【" + client.nickname + "】加入\n"); Thread clientThread = new Thread(client); clientThread.start(); clients.add(client); } catch (Exception ex) { System.out.println(ex); } } } // TODO 服务器存放用户对象的客户类(主要编程)。每当有新的用户连接时,该类都会被调用 // TODO 该类继承自 Runnable,内部含有 run()方法 private class Client implements Runnable { private Socket socket; // 用来保存用户的连接对象 private BufferedReader in; // IO 流 private PrintStream out; private String nickname; // 保存用户昵称 // Client类的构建方法。当有 新用户 连接时会被调用 public Client(Socket socket) throws Exception { this.socket = socket; InputStream is = socket.getInputStream(); in = new BufferedReader(new InputStreamReader(is)); OutputStream os = socket.getOutputStream(); out = new PrintStream(os); nickname = in.readLine(); // 获取用户昵称 for (Client c : clients) { // 将新用户的登录消息发给所有用户 c.out.println("——客户【" + nickname + "】加入\n"); } } //客户类线程运行方法 public void run() { try { while (true) { String usermsg = in.readLine(); //读用户发来消息 String secondMsg = usermsg.substring(usermsg.lastIndexOf(":") + 1); // 字符串辅助对象 // 如果用户发过来的消息不为空 if (usermsg != null && usermsg.length() > 0) { // 如果消息是 bye,则断开与此用户的连接 并 告知所有用户当前信息,跳出循环终止当前进程 if (secondMsg.equals("bye")) { clients.remove(this); for (Client c : clients) { c.out.println(usermsg); } taMsg.append("——客户离开:" + nickname + "\n"); // 更新在线用户数量 lstUsers的界面信息 updateUsers(); break; } /** * 每当有新用户连接时,服务器就会接收到 USERS 请求 * 当服务器接收到此请求时,就会要求现在所有用户更新 在线用户数量 的列表 * */ if (usermsg.equals("USERS")) { updateUsers(); continue; } // 当用户发出的消息都不是以上两者时,消息才会被正常发送 for (Client c : clients) { c.out.println(usermsg); } } } socket.close(); } catch (Exception ex) { System.out.println(ex); } } // TODO 更新在线用户数量 lstUsers 信息,并要求所有的用户端同步更新 public void updateUsers() { // clientNames 是 Vector<String>对象,用来存放所有用户的名字 clientNames.removeAllElements(); StringBuffer allname = new StringBuffer(); for (AcceptRunnable.Client client : clients) { clientNames.add(0, client.nickname); allname.insert(0, "|" + client.nickname); } panDown.setBorder(new TitledBorder("在线用户(" +clientNames.size() + "个)")); // 要求所有的用户端同步更新 for (Client c : clients) { c.out.println(clientNames); } lstUsers.setListData(clientNames); } } } // TODO 主方法 public static void main(String[] args) { new Server(); } }
客户端代码:
import javax.swing.*; import javax.swing.border.TitledBorder; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.util.Vector; public class Client extends JFrame { //客户机窗体类 // TODO 该图形界面拥有四块区域,分别位于上、左、中、下 (up、Left、middle、down)。 private JPanel panUp = new JPanel(); private JPanel panLeft = new JPanel(); private JPanel panMid = new JPanel(); private JPanel panDown = new JPanel(); // panUp 区域的子节点定义,3个标签、3个输入框、2个按钮 private JLabel lblLocalPort1 = new JLabel("服务器IP: "); private JLabel lblLocalPort2 = new JLabel("端口: "); private JLabel lblLocalPort3 = new JLabel("本人昵称: "); protected JTextField tfLocalPort1 = new JTextField(15); protected JTextField tfLocalPort2 = new JTextField(5); protected JTextField tfLocalPort3 = new JTextField(5); protected JButton butStart = new JButton("连接服务器"); protected JButton butStop = new JButton("断开服务器"); // TODO // panLeft 区域的子节点定义,显示框、滚动条 protected JTextArea taMsg = new JTextArea(25, 25); JScrollPane scroll = new JScrollPane(taMsg); // panMid 区域的子节点定义,lstUsers在线用户界面 JList lstUsers = new JList(); // panDown 区域的子节点定义,标签,输入框 private JLabel lblLocalPort4 = new JLabel("消息(按回车发送): "); protected JTextField tfLocalPort4 = new JTextField(20); /** * ===== 变量分割 ===== * 上面是图形界面变量,下面是存放数据的变量 */ BufferedReader in; PrintStream out; public static int localPort = 8000; // 默认端口 public static String localIP = "127.0.0.1"; // 默认服务器IP地址 public static String nickname = "Cat"; // 默认用户名 public Socket socket; public static String msg; // 存放本次发送的消息 Vector<String> clientNames = new Vector<>(); // TODO 构造方法 public Client() { init(); } // TODO 初始化方法:初始化图形界面 private void init() { // panUp 区域初始化:流式面板,3个标签、3个输入框,2个按钮 panUp.setLayout(new FlowLayout()); panUp.add(lblLocalPort1); panUp.add(tfLocalPort1); panUp.add(lblLocalPort2); panUp.add(tfLocalPort2); panUp.add(lblLocalPort3); panUp.add(tfLocalPort3); tfLocalPort1.setText(localIP); tfLocalPort2.setText(String.valueOf(localPort)); tfLocalPort3.setText(nickname); panUp.add(butStart); panUp.add(butStop); butStart.addActionListener(new linkServerHandlerStart()); butStop.addActionListener(new linkServerHandlerStop()); butStop.setEnabled(false); // 断开服务器按钮的初始状态应该为 不可点击,只有连接服务器之后才能点击 // 添加 Left taMsg.setEditable(false); panLeft.add(scroll); panLeft.setBorder(new TitledBorder("聊天——消息区")); scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); // 添加 Middle panMid.setBorder(new TitledBorder("在线用户")); panMid.add(lstUsers); lstUsers.setVisibleRowCount(20); // 添加 Down // TODO 此处注意:JTextField输入框 的回车事件默认存在,无需添加 panDown.setLayout(new FlowLayout()); panDown.add(lblLocalPort4); panDown.add(tfLocalPort4); tfLocalPort4.addActionListener(new Client.SendHandler()); // 图形界面的总体初始化 + 启动图形界面 this.setTitle("客户端"); this.add(panUp, BorderLayout.NORTH); this.add(panLeft, BorderLayout.WEST); this.add(panMid, BorderLayout.CENTER); this.add(panDown, BorderLayout.SOUTH); this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); this.addWindowListener(new WindowHandler()); this.setPreferredSize(new Dimension(800, 600)); this.pack(); this.setVisible(true); } // TODO “连接服务器”按钮的动作事件监听处理类: private class linkServerHandlerStart implements ActionListener { @Override public void actionPerformed(ActionEvent e) { // 当点击"连接服务器"按钮之后,该按钮被禁用(不可重复点击)。同时"断开服务器按钮"被恢复使用 butStart.setEnabled(false); butStop.setEnabled(true); localIP = tfLocalPort1.getText(); localPort = Integer.parseInt(tfLocalPort2.getText()); nickname = tfLocalPort3.getText(); linkServer(); // 连接服务器 Thread acceptThread = new Thread(new Client.ReceiveRunnable()); acceptThread.start(); } } // TODO “断开服务器”按钮的动作事件监听处理类 private class linkServerHandlerStop implements ActionListener { /** * 当点击该按钮之后,断开服务器连接、清空图形界面所有数据 */ @Override public void actionPerformed(ActionEvent e) { taMsg.setText(""); clientNames = new Vector<>(); updateUsers(); out.println("——客户【" + nickname + "】离开:bye\n"); butStart.setEnabled(true); butStop.setEnabled(false); } } // TODO 连接服务器的方法 public void linkServer() { try { socket = new Socket(localIP, localPort); } catch (Exception ex) { taMsg.append("==== 连接服务器失败~ ===="); } } // TODO 接收服务器消息的线程关联类 private class ReceiveRunnable implements Runnable { public void run() { try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintStream(socket.getOutputStream()); out.println(nickname); // 当用户首次连接服务器时,应该向服务器发送自己的用户名、方便服务器区分 taMsg.append("——本人【" + nickname + "】成功连接到服务器......\n"); out.println("USERS"); // 向服务器发送"神秘代码",请求 当前在线用户 列表 while (true) { msg = in.readLine(); // 读取服务器端的发送的数据 // 此 if 语句的作用是:过滤服务器发送过来的 更新当前在线用户列表 请求 if (msg.matches(".*\\[.*\\].*")) { clientNames.removeAllElements(); String[] split = msg.split(","); for (String single : split) { clientNames.add(single); } updateUsers(); continue; } // 更新 "聊天——消息区" 信息 taMsg.append(msg + "\n"); // 此 if 语句作用:与服务器进行握手确认消息。 // 当接收到服务器端发送的确认离开请求bye 的时候,用户真正离线 msg = msg.substring(msg.lastIndexOf(":") + 1); if (msg.equals(nickname)) { socket.close(); clientNames.remove(nickname); updateUsers(); break; // 终止线程 } } } catch (Exception e) { } } } // TODO "发送消息文本框" 的动作事件监听处理类 private class SendHandler implements ActionListener { @Override public void actionPerformed(ActionEvent e) { out.println("【" + nickname + "】:" + tfLocalPort4.getText()); tfLocalPort4.setText(""); // 当按下回车发送消息之后,输入框应该被清空 } } // TODO 窗口关闭的动作事件监听处理类 // 当用户点击 "x" 离开窗口时,也会向服务器发送 bye 请求,目的是为了同步更新数据。 private class WindowHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { cutServer(); } } private void cutServer() { out.println("——客户【" + nickname + "】离开:bye"); } // TODO 更新 "在线用户列表" 的方法 public void updateUsers() { panMid.setBorder(new TitledBorder("在线用户(" + clientNames.size() + "个)")); lstUsers.setListData(clientNames); } // TODO 主方法 public static void main(String[] args) { new Client(); } }
如何同时开启两个客户端进行聊天?
将上述的 Client 类复制一份,改名为 Client2 ,然后同时启动 Client 和 Client2 程序。
The above is the detailed content of How to use Java to write an online chat program. For more information, please follow other related articles on the PHP Chinese website!