【Java】基于GUI的网络通信程序设计

 

目录

一. 程序内容

二. 要求分析

三. 程序编写

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. 测试服务端和客户端的连通性。

整理思路后,就可以开始编写我们的程序。

三. 程序编写

0. 程序结构

共三个类:

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();

    }
}

1. 服务端程序的GUI设计

Ⅰ 原理介绍

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的绘制部分就基本完成。

2. 服务端业务逻辑的编写

Ⅰ 原理 + 具体实现

整个服务端的业务逻辑,即从启动服务、等待连接、发送和接收消息、关闭连接均在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();
        }
    }

3. 为GUI界面绑定按钮事件

下面,我们回到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("");
            }
        });

4. 将服务端的源码复制后,进行重构,并加以修改为客户端

至此,服务器端的代码全部完成了。客户端的代码只需要在服务端的基础上稍加修改即可完成。

这里建议使用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));
    }

}

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没头发的米糊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值