一. 程序内容
二. 要求分析
三. 程序编写
0. 程序结构
1. 服务端程序的GUI设计
2. 服务端业务逻辑的编写
3. 为GUI界面绑定按钮事件
4. 将服务端的源码复制后,进行重构,并加以修改为客户端
四、源代码
这是合工大软件工程专业Java程序设计课程实验二的内容,该实验要求编写Java程序完成以下功能:
1. 设计一个基于GUI的客户-服务器的通信应用程序,如图1、图2所示。
图1 Socket通信服务器端界面 | 图2 Socket通信客户端界面 |
2. 图1为Socket通信服务器端界面,点击该界面中的【Start】按钮,启动服务器监听服务(在图1界面中间的多行文本区域显示“Server starting…”字样)。图2为Socket通信客户端界面,点击该界面中的【Connect】按钮与服务器建立链接,并在图2所示界面中间的多行文本区域显示“Connect to server…”字样,当服务器端监听到客户端的连接后,在图1界面中间的多行文本区域追加一行“Client connected…”字样,并与客户端建立Socket连接。
3. 当图1所示的服务器端和图2所示的客户机端建立Socket连接后,编程实现服务端、客户端之间的“单向通信”:在客户端的输入界面发送消息,在服务端接收该消息,并将接收到对方的数据追加显示在多行文本框中。
4. 在完成上述实验内容的基础上,尝试实现“双向通信”功能,即服务端、客户端之间可以相互发送、接收消息,并以此作为实验成绩评优的加分依据。
总的来看,我们需要依次完成以下几个工作:
1. 服务端程序的GUI设计。
2. 服务端业务逻辑的编写。
3. 为GUI界面绑定按钮事件。
4. 将服务端的源码复制后,进行重构,并加以修改为客户端。
5. 测试服务端和客户端的连通性。
整理思路后,就可以开始编写我们的程序。
共三个类:
1. 主类Main,用于封装main函数。
2. 继承自JFrame的公共类ServerWindow,封装了服务端程序的GUI界面。
3. 继承自Thread的公共类Server,封装了服务端的业务逻辑。
Main类代码:
import javax.swing.*; public class Main { public static void main(String[] args) { ServerWindow mainWindow = new ServerWindow(); } }
Ⅰ 原理介绍
Swing 是一个为Java设计的GUI工具包,提供了许多比AWT更精致的屏幕显示元素。支持可更换的面板和主题,缺点则是执行速度较慢,优点就是可以在所有平台上采用统一的样式和行为。
Java Swing 示例程序:Java Swing 介绍 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/java-swing-demo-intro.html
Ⅱ 具体思路
整个GUI界面的结构如上图所示。
我们将界面分为上、中、下三个部分,分别使用三个JPanel包裹(为了方便布局,建议将组件放置于JPanel而非直接置于顶层容器JFrame中。),在ServerWindow类中也加入这些组件。
在ServerWindow类中如下声明所有的GUI组件:
JPanel serverSettings; JTextField portField; JButton startBtn; JPanel areaPanel; JTextArea messageArea; JPanel sendPanel; JTextField sendField; JButton sendBtn;
其后,在该类的构造函数中需要对以上变量进行初始化:
public ServerWindow() { super("服务端"); this.setSize(500,300); this.setResizable(false); this.setLayout(new BorderLayout()); initializeServerSettings(); initializeAreaPanel(); initializeSendPanel(); this.add(serverSettings,BorderLayout.NORTH); this.add(areaPanel,BorderLayout.CENTER); this.add(sendPanel,BorderLayout.SOUTH); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); }
为了优化代码可读性,将三个JPanel内组件的初始化代码单独列为private函数,如下:
private void initializeServerSettings() { serverSettings = new JPanel(); portField = new JTextField(30); startBtn = new JButton("Start"); serverSettings.setBorder(new EmptyBorder(10, 5, 10, 5)); serverSettings.add(new JLabel("Port:")); serverSettings.add(portField); serverSettings.add(startBtn); } private void initializeSendPanel() { sendPanel = new JPanel(); sendBtn = new JButton("Send"); sendField = new JTextField(30); sendPanel.setBorder(new EmptyBorder(10, 5, 10, 5)); sendPanel.add(new JLabel("Send:")); sendPanel.add(sendField); sendPanel.add(sendBtn); } private void initializeAreaPanel() { areaPanel = new JPanel(); messageArea = new JTextArea(9, 40); areaPanel.add(new JScrollPane(messageArea)); }
至此,GUI的绘制部分就基本完成。
Ⅰ 原理 + 具体实现
整个服务端的业务逻辑,即从启动服务、等待连接、发送和接收消息、关闭连接均在Server类中完成。
关于WebSocket基本原理,在这片博文中有浅显易懂的解释,我在此就不再重复造轮子了:WebSocket 教程 - 阮一峰的网络日志 (ruanyifeng.com)https://www.ruanyifeng.com/blog/2017/05/websocket.html
在Java中,实现WebSocket通信,主要依靠java.net.Socket和java.net.ServerSocket两个类。
在服务端中,需要ServerSocket和Socket两个对象。
Socket client; ServerSocket server;
ServerSocket用于在服务端计算机的指定端口建立一个监听服务,并回应随时可能到来的客户端请求。考虑如下的语句:
Socket LinkSocket = MyListener.accept();
该语句调用了ServerSocket对象的accept()方法,这个方法的执行将使Server端的程序处于等待状态,程序将一直阻塞(使用多线程的原因),直到捕捉到一个来自Client端的请求,并返回一个用于与该Client通信的Socket对象Link-Socket。此后Server程序只要向这个Socket对象读写数据,就可以实现向远端的Client读写数据。
打个简单的比方,SeverSocket就好比站在酒店门口的迎宾小姐,而Socket好比大堂内接待顾客的接待员,迎宾小姐的工作就是迎接到来的顾客,并交付给大堂内的接待员。
为了能将具体的连接信息和发送、接收的数据显示在GUI上,需要同时传入GUI界面中messageArea的引用。
JTextArea messageArea;
这一段的具体代码如下图所示:
server = new ServerSocket(port); messageArea.append("- 服务已在端口 " + port + "上启动。\n"); //从ServerSocket等待新连接的Socket。 client = server.accept(); messageArea.append("- " + client.getInetAddress().getLocalHost() + " 已连接到服务。\n");
上述代码会阻塞程序,因此需要在新线程中运行。我选择使用继承Thread类的方式实现多线程。在Server类的构造函数中,完成对传入参数的处理后,便直接调用对象的start()函数,启动新线程。
Server(int port,JTextArea msgArea) { this.port = port; this.messageArea = msgArea; this.start(); }
Java多线程:Java多线程看这一篇就足够了(吐血超详细总结) - Java团长 - 博客园 (cnblogs.com)https://www.cnblogs.com/java1024/archive/2019/11/28/11950129.html
Socket对象有两个关键的方法,一个是getInputStream方法,另一个是getOutputStream方法。getInputStream方法可以得到一个输入流,服务端的Socket对象上的getInputStream方法得到的输入流其实就是从客户端发回的数据流。GetOutputStream方法得到一个输出流,服务端Socket对象上的getOutputStream方法返回的输出流就是将要发送到客户端的数据流,(其实是一个缓冲区,暂时存储将要发送过去的数据)。
BufferedReader br; BufferedWriter bw; InputStream is; OutputStream os;
因此服务端与客户端的数据传输,需要依靠Socket对象的InputStream和OutputStream完成,具体如下实现:
is = client.getInputStream(); os = client.getOutputStream(); br = new BufferedReader(new InputStreamReader(is)); bw = new BufferedWriter(new OutputStreamWriter(os)); while(true) { String newMsg = br.readLine(); if (newMsg != null) //意味着客户端发来了新消息。 { messageArea.append(">> " + newMsg + "\n"); }
上述代码中,通过不断读取InputStream,来得到客户端发送来的新消息,并将新消息显示在messageArea中。这段代码同样会阻塞程序。
因为在Socket连接中可能会发生异常,因此整段代码完整包裹在try语句中,并通过以下异常处理语句确定异常、显示异常消息:
catch (IOException e) { e.printStackTrace(); if (e instanceof java.net.ConnectException) messageArea.append("- 服务启动失败,请重试或更换端口。" + "\n"); else messageArea.append("- 与客户端的连接已断开,服务停止。\n"); } finally { try { server.close();//无论如何都应当调用 } catch (IOException e) { e.printStackTrace(); } }
无论如何,最后都应当调用server.close()语句,关闭ServerSocket对端口的占用。
最后,Server类还应当提供一个sendMsg方法,用于向客户端主动发送信息:
public void sendMsg(String msg) { System.out.println("sendMsg"); try { bw.write(msg + "\n");//务必在一条信息后加上换行符,代表发送完成。 bw.flush(); messageArea.append("<< " + msg + "\n"); } catch (IOException e) { e.printStackTrace(); } }
下面,我们回到GUI界面中,为其中的两个按钮绑定事件。
首先是启动服务的Start按钮,按下按钮时,应当创建一个新的Server对象,传入端口号和messageArea组件,如下:
startBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { int port = Integer.parseInt(portField.getText()); server = new Server(port,messageArea); } catch(java.lang.NumberFormatException exception) { messageArea.append("- 端口格式有误,请重新输入。\n"); } System.out.println(portField.getText()); } });
其后是发送消息的Send按钮,按下按钮后,调用Server对象的sendMsg方法,传入要发送的信息,如下:
sendBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { server.sendMsg(sendField.getText()); sendField.setText(""); } });
至此,服务器端的代码全部完成了。客户端的代码只需要在服务端的基础上稍加修改即可完成。
这里建议使用IDEA的代码重构功能,在需要修改的类名、变量名上右键,使用重构 - 重命名,即可将整个代码中所有出现的该标识符自动替换为新名字。
Client端除了不需要ServerSocket以外,具体业务逻辑与Server端基本一致,这里就不再细说,建议直接参照源码变化。
Server.Java
package exp.server; import javax.swing.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.Buffer; import java.util.ArrayList; //服务器类,用于处理最初的PortWaiter的创建任务以及向客户端发送消息。 class Server extends Thread{ Socket client; ServerSocket server; JTextArea messageArea; BufferedReader br; BufferedWriter bw; InputStream is; OutputStream os; int port; Server(int port,JTextArea msgArea) { this.port = port; this.messageArea = msgArea; this.start(); } @Override public void run() { super.run(); try { server = new ServerSocket(port); messageArea.append("- 服务已在端口 " + port + "上启动。\n"); //从ServerSocket等待新连接的Socket。 client = server.accept(); messageArea.append("- " + client.getInetAddress().getLocalHost() + " 已连接到服务。\n"); is = client.getInputStream(); os = client.getOutputStream(); br = new BufferedReader(new InputStreamReader(is)); bw = new BufferedWriter(new OutputStreamWriter(os)); while(true) { String newMsg = br.readLine(); if (newMsg != null) { messageArea.append(">> " + newMsg + "\n"); } } } catch (IOException e) { e.printStackTrace(); if (e instanceof java.net.ConnectException) messageArea.append("- 服务启动失败,请重试或更换端口。" + "\n"); else messageArea.append("- 与客户端的连接已断开,服务停止。\n"); } finally { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } public void sendMsg(String msg) { System.out.println("sendMsg"); try { bw.write(msg + "\n"); bw.flush(); messageArea.append("<< " + msg + "\n"); } catch (IOException e) { e.printStackTrace(); } } }
ServerWindow.Java
package exp.server; import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; public class ServerWindow extends JFrame{ JPanel serverSettings; JTextField portField; JButton startBtn; JPanel areaPanel; JTextArea messageArea; JPanel sendPanel; JTextField sendField; JButton sendBtn; Server server; public ServerWindow() { super("服务端"); this.setSize(500,300); this.setResizable(false); this.setLayout(new BorderLayout()); initializeServerSettings(); initializeAreaPanel(); initializeSendPanel(); this.add(serverSettings,BorderLayout.NORTH); this.add(areaPanel,BorderLayout.CENTER); this.add(sendPanel,BorderLayout.SOUTH); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); startBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { int port = Integer.parseInt(portField.getText()); server = new Server(port,messageArea); } catch(java.lang.NumberFormatException exception) { messageArea.append("- 端口格式有误,请重新输入。\n"); } System.out.println(portField.getText()); } }); sendBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { server.sendMsg(sendField.getText()); sendField.setText(""); } }); } private void initializeServerSettings() { serverSettings = new JPanel(); portField = new JTextField(30); startBtn = new JButton("Start"); serverSettings.setBorder(new EmptyBorder(10, 5, 10, 5)); serverSettings.add(new JLabel("Port:")); serverSettings.add(portField); serverSettings.add(startBtn); } private void initializeSendPanel() { sendPanel = new JPanel(); sendBtn = new JButton("Send"); sendField = new JTextField(30); sendPanel.setBorder(new EmptyBorder(10, 5, 10, 5)); sendPanel.add(new JLabel("Send:")); sendPanel.add(sendField); sendPanel.add(sendBtn); } private void initializeAreaPanel() { areaPanel = new JPanel(); messageArea = new JTextArea(9, 40); areaPanel.add(new JScrollPane(messageArea)); } }
Client.Java
package exp.server; import javax.swing.*; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; //服务器类,用于处理最初的PortWaiter的创建任务以及向客户端发送消息。 class Client extends Thread{ Socket server; JTextArea messageArea; BufferedReader br; BufferedWriter bw; InputStream is; OutputStream os; int port; String address; Client(int port, JTextArea msgArea, String address) { this.port = port; this.messageArea = msgArea; this.address = address; this.start(); } @Override public void run() { super.run(); try { server = new Socket(address, port); messageArea.append("- 已连接到主机 " + server.getInetAddress().getLocalHost() + "\n"); is = server.getInputStream(); os = server.getOutputStream(); br = new BufferedReader(new InputStreamReader(is)); bw = new BufferedWriter(new OutputStreamWriter(os)); while(true) { String newMsg = br.readLine(); if (newMsg != null) { messageArea.append(">> " + newMsg + "\n"); } } } catch (IOException e) { e.printStackTrace(); if(e instanceof java.net.ConnectException) messageArea.append("- 无法连接到主机,请重试或检查地址和端口。" + "\n"); else messageArea.append("- 与远程主机的连接已断开。\n"); } } public void sendMsg(String msg) { System.out.println("sendMsg"); try { bw.write(msg + "\n"); bw.flush(); messageArea.append("<< " + msg + "\n"); } catch (IOException e) { e.printStackTrace(); } } }
ClientWindow.Java
package exp.server; import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class ClientWindow extends JFrame{ JPanel clientSettings; JTextField addressField; JTextField portField; JButton connectBtn; JPanel areaPanel; JTextArea messageArea; JPanel sendPanel; JTextField sendField; JButton sendBtn; Client client; public ClientWindow() { super("客户端"); this.setSize(500,300); this.setResizable(false); this.setLayout(new BorderLayout()); initializeServerSettings(); initializeAreaPanel(); initializeSendPanel(); this.add(clientSettings,BorderLayout.NORTH); this.add(areaPanel,BorderLayout.CENTER); this.add(sendPanel,BorderLayout.SOUTH); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); connectBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { int port = Integer.parseInt(portField.getText()); client = new Client(port,messageArea, addressField.getText()); } catch(java.lang.NumberFormatException exception) { messageArea.append("- 端口格式有误,请重新输入。\n"); } System.out.println(portField.getText()); } }); sendBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println(sendField.getText()); client.sendMsg(sendField.getText()); sendField.setText(""); } }); } private void initializeServerSettings() { clientSettings = new JPanel(); addressField = new JTextField(20); portField = new JTextField(10); connectBtn = new JButton("Connect"); clientSettings.setBorder(new EmptyBorder(10, 5, 10, 5)); clientSettings.add(new JLabel("IP:")); clientSettings.add(addressField); clientSettings.add(new JLabel("Port:")); clientSettings.add(portField); clientSettings.add(connectBtn); } private void initializeSendPanel() { sendPanel = new JPanel(); sendBtn = new JButton("Send"); sendField = new JTextField(30); sendPanel.setBorder(new EmptyBorder(10, 5, 10, 5)); sendPanel.add(new JLabel("Send:")); sendPanel.add(sendField); sendPanel.add(sendBtn); } private void initializeAreaPanel() { areaPanel = new JPanel(); messageArea = new JTextArea(9, 40); areaPanel.add(new JScrollPane(messageArea)); } }