• 【互联网程序设计】网络对话程序设计


    一、简单网络对话程序

    设计任务:客户端向服务器发送字符串,并能读取服务器返回的字符串。
    知识点:TCP套接字技术,C/S软件架构程序设计
    重点理解:Java客户套接字类Socket和服务器套接字类ServerSocket,以及配套使用流的读/写BuffferedReader/PrintWriter

    在C/S软件架构程序设计技术中,实现网络通信的两个应用进程,一个叫做服务进程,另一个叫做客户进程,如图所示。服务进程被动打开一个监听端口8008,客户进程主动访问这个端口,完成对话聊天前TCP三次握手连接
    在这里插入图片描述
    Java的TCP/IP 套接字编程将底层的细节进行了封装,其编程模型如图所示:

    Server建立服务端监听socket,等待客服端发来请求,Client创建socket并向服务端发送请求,服务端收到后创建连接socket。 TCP连接成功后,逻辑上可理解为通信进程的双方具有两个流(输出流和输入流)。逻辑上可将两个流理解为两个通信管道的全双工通信模式,一个用于向对方发送数据,另一个用于接收对方的数据。最后结束通信,关闭socket和相关资源。在Java TCP/IP编程模型中,有两个套接字类:服务进程中的是ServerSocket类,客户进程中的是Socket类
    在这里插入图片描述

    客户端程序1:TCPClient.java具有网络接收和发送能力的程序。
    客户端程序2:TCPClientFX.java为界面模块。
    服务器程序:TCPServer.java具有网络接收和发送功能。
    网络对话方式是:客户端连接服务器,连接成功后,服务器首先给客户端发送一条欢迎信息;之后客户端程序每发送一条信息给服务器TCPServer.java,服务器接收并回送该信息到客户端,客户端接收并显示该信息;当客户端发送"bye",则结束对话。

    二、TCPServer程序

    TCPServer.java具有网络接收和发送功能

    package chapter02;
    
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class TCPServer {
        private int port = 8009; //服务器监听端口
        private ServerSocket serverSocket; //定义服务器套接字
    
        public TCPServer() throws IOException {
            serverSocket = new ServerSocket(port);
            System.out.println("服务器启动监听在 " + port + " 端口");
        }
    
        private PrintWriter getWriter(Socket socket) throws IOException {
            //获得输出流缓冲区的地址
            OutputStream socketOut = socket.getOutputStream();
            //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
            return new PrintWriter(
                    new OutputStreamWriter(socketOut, "utf-8"), true);
        }
    
        private BufferedReader getReader(Socket socket) throws IOException {
            //获得输入流缓冲区的地址
            InputStream socketIn = socket.getInputStream();
            return new BufferedReader(
                    new InputStreamReader(socketIn, "utf-8"));
        }
    
        //单客户版本,即每一次只能与一个客户建立通信连接
        public void Service() {
            //服务器程序需要一直运行 放在while
            while (true) {
                Socket socket = null;
                try {
                    //服务器监听并等待客户发起连接,有连接请求就生成一个套接字。
                    socket = serverSocket.accept();
                    
                    //本地服务器控制台显示客户端连接的用户信息
                    System.out.println("New connection accepted: " + socket.getInetAddress());
                    BufferedReader br = getReader(socket);//定义字符串输入流
                    PrintWriter pw = getWriter(socket);//定义字符串输出流
                    //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
                    pw.println("From 服务器:欢迎使用本服务!");
    
                    String msg = null;
                    //此处程序阻塞,每次从输入流中读入一行字符串
                    while ((msg = br.readLine()) != null) {
                        
                        //如果客户发送的消息为"bye" 结束通信
                        if (msg.equals("bye")) {
                            //向输出流中输出一行字符串,远程客户端可以读取该字符串
                            pw.println("From服务器:服务器断开连接,结束服务!");
                            System.out.println("客户端离开");
                            break; //结束循环
                        }
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        // 正则表达式实现“人工智能”(扩展练习)
                        msg = msg.replaceAll("[吗?]", "") + "!";
                        pw.println("From服务器:" + msg);
    
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (socket != null) {
                            socket.close(); //关闭socket连接及相关的输入输出流
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) throws IOException {
            new TCPServer().Service();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    三、TCPCilent

    package chapter02;
    
    import java.io.*;
    import java.net.Socket;
    
    public class TCPClient {
        private Socket socket; //定义套接字
        //定义字符输入流和输出流
        private PrintWriter pw;
        private BufferedReader br;
    
        public TCPClient(String ip, String port) throws IOException {
            //主动向服务器发起连接,实现TCP的三次握手过程
            //如果不成功,则抛出错误信息,其错误信息交由调用者处理
            socket = new Socket(ip, Integer.parseInt(port));
    
            //得到网络输出字节流地址,并封装成网络输出字符流
            OutputStream socketOut = socket.getOutputStream();
            pw = new PrintWriter( // 设置最后一个参数为true,表示自动flush数据
                    new OutputStreamWriter(//设置utf-8编码
                            socketOut, "utf-8"), true);
    
            //得到网络输入字节流地址,并封装成网络输入字符流
            InputStream socketIn = socket.getInputStream();
            br = new BufferedReader(
                    new InputStreamReader(socketIn, "utf-8"));
        }
    
        public void send(String msg) {
            //输出字符流,由Socket调用系统底层函数,经网卡发送字节流
            pw.println(msg);
        }
    
        public String receive() {
            String msg = null;
            try {
                //从网络输入字符流中读信息,每次只能接受一行信息
                //如果不够一行(无行结束符),则该语句阻塞,
                // 直到条件满足,程序才往下运行
                msg = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return msg;
        }
    
        public void close() {
            try {
                if (socket != null) {
                    //关闭socket连接及相关的输入输出流,实现四次握手断开
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //本机模块内测试与运行,需先运行TCPServer
        public static void main(String[] args) throws IOException {
            TCPClient tcpClient = new TCPClient("127.0.0.1", "8008");
            tcpClient.send("hello");//发送一串字符
            //接收服务器返回的字符串并显示
            System.out.println(tcpClient.receive());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    四、TCPClientFX

    将客户端图形化,内部调用TCPClient模块中相应的方法完成网络对话功能:创建新界面并命名为TCPClientFX.java程序,其界面布局如图所示。

    在这里插入图片描述
    注意:运行TCPClientFX前,先运行TCPServer

    package chapter02;
    
    import chapter01.TextFileIO;
    import javafx.application.Application;
    import javafx.event.EventHandler;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextArea;
    import javafx.scene.control.TextField;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyCodeCombination;
    import javafx.scene.input.KeyCombination;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.Priority;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.time.LocalDateTime;
    
    public class TCPClientFX extends Application {
        private Button btnExit = new Button("退出");
        private Button btnSend = new Button("发送");
    
        private TextField tfSend = new TextField();
        private TextArea taDisplay = new TextArea();
    
        private TextField tfIP = new TextField("127.0.0.1");
        private TextField tfPort = new TextField("8010");
        private Button btnConnect = new Button("连接");
    
        private TCPClient tcpClient;
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) {
            //边框布局
            BorderPane mainPane = new BorderPane();
    
            //水平布局
            HBox connHbox = new HBox();
            connHbox.setAlignment(Pos.CENTER);
            connHbox.setSpacing(10);
            connHbox.getChildren().addAll(new Label("IP地址:"), tfIP, new Label("端口:"), tfPort, btnConnect);
            mainPane.setTop(connHbox);
    
    
            //垂直布局 信息区
            VBox vBox = new VBox();
            vBox.setSpacing(10);
            vBox.setPadding(new Insets(10, 20, 10, 20));
            // 设置发送信息的文本框
    
            // 自动换行
            taDisplay.setWrapText(true);
            // 只读
            taDisplay.setEditable(false);
            vBox.getChildren().addAll(new Label("信息显示区: "), taDisplay, new Label("信息输入区:"), tfSend);
            VBox.setVgrow(taDisplay, Priority.ALWAYS);
            mainPane.setCenter(vBox);
    
            HBox hBox = new HBox();
            hBox.setSpacing(10);
            hBox.setPadding(new Insets(10, 20, 10, 20));
            hBox.setAlignment(Pos.CENTER_RIGHT);
    
            // 按钮事件绑定
            btnConnect.setOnAction(event -> {
                String ip = tfIP.getText().trim();
                String port = tfPort.getText().trim();
    
                try {
                    //tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
                    tcpClient = new TCPClient(ip,port);
                    //成功连接服务器,接收服务器发来的第一条欢迎信息
                    String firstMsg = tcpClient.receive();
                    taDisplay.appendText(firstMsg + "\n");
    
                    // 启用发送按钮
                    btnSend.setDisable(false);
    
                    // 停用连接按钮
                    btnConnect.setDisable(true);
                } catch (Exception e) {
                    taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");
                }
    
            });
            btnExit.setOnAction(event -> {
                if(tcpClient != null){
                    //向服务器发送关闭连接的约定信息
                    tcpClient.send("bye");
                    tcpClient.close();
                }
                System.exit(0);
            });
    
            btnSend.setOnAction(event -> {
                String sendMsg = tfSend.getText();
                tcpClient.send(sendMsg);//向服务器发送一串字符
                taDisplay.appendText("客户端发送:" + sendMsg + "\n");
                String receiveMsg = tcpClient.receive();//从服务器接收一行字符
                taDisplay.appendText(receiveMsg + "\n");
                tfSend.clear();
                // 发送bye后重新启用连接按钮,禁用发送按钮
                if (sendMsg.equals("bye")) {
                    btnConnect.setDisable(false);
                    btnSend.setDisable(true);
                }
            });
    
            // 未连接时禁用发送按钮
            btnSend.setDisable(true);
    
            //底部按钮
            hBox.getChildren().addAll(btnSend, btnExit);
            mainPane.setBottom(hBox);
            Scene scene = new Scene(mainPane, 700, 400);
    
            // 响应窗体关闭
            primaryStage.setOnCloseRequest(event -> {
                if(tcpClient != null){
                    //向服务器发送关闭连接的约定信息
                    tcpClient.send("bye");
                    tcpClient.close();
                }
                System.exit(0);
            });
    
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141

    请添加图片描述

    请添加图片描述
    请添加图片描述

    五、建议

    我们可以看出,在一个设计良好的TCP服务器/客户端程序中,为了能够友好地完成整个通信过程,建议:

    客户端成功连接服务器,服务器应该给客户端主动发送一条欢迎或通知等信息,作为整个通信的第一条信息,然后服务器进入监听阻塞状态,等待客户端的信息。而客户端在连接成功后就用一条行读取语句来读取这条信息;

    服务器一般是不关闭,一直等待客户连接,并不能主动知道客户端是否准备离开。所以客户端关闭时,给服务器发送一条约定的表示离开的信息(在本例中使用bye作为约定信息),以方便服务器可以做出响应。

    这两条都需要服务器和客户端互相约定,否则就可能有问题,例如,如果服务器在一个客户端连接成功后,并没有一条欢迎信息发送给客户端,客户端的读取欢迎信息的语句无法读取到内容,就被阻塞住,由于是单线程,甚至整个程序都会被卡住。要解决这个问题,可以使用下一讲的知识。

  • 相关阅读:
    Self -Supervised Learning
    电子商务商城源码 Spring Cloud、Spring Boot、Mybatis、Redis
    Selenium —— 网页frame与多窗口处理!
    从react源码看hooks的原理
    webpack5+vue3搭建一个基础的h5项目结构
    环境主题静态HTML网页作业作品 大学生环保网页设计制作成品 简单DIV CSS布局网站
    【面试题】js 判断数组中是否有某个值
    分析Lua观察者模式最佳实践之构建事件分发系统
    使用BENCHMARKSQL工具对KingbaseES预热数据时执行:select sys_prewarm(‘NDX_OORDER_2 ‘)报错
    React源码解读之React Fiber
  • 原文地址:https://blog.csdn.net/weixin_51293984/article/details/126751740