一. 程序内容
二. 要求分析
三. 程序编写
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));
}
}