• 互联网程序设计课程 第2讲 网络对话程序设计


    简单网络对话程序

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

    在C/S软件架构程序设计技术中,实现网络通信的两个应用进程,一个叫做服务进程,另一个叫做客户进程,如图2.1所示。服务进程首先被动打开一个监听端口,如8008,客户进程主动访问这个端口,完成对话聊天前的TCP三次握手连接。

    图: TCP连接建立的过程
    在这里插入图片描述
    Java的TCP/IP 套接字编程将底层的细节进行了封装,其编程模型如图2.2所示。

    图2.2 Socket完整通信模型在这里插入图片描述
    在Java TCP/IP编程模型中,有两个套接字类:服务进程中的是ServerSocket类,客户进程中的是Socket类。
    服务进程首先开启一个或多个监听端口,客户进程向服务进程发起TCP三次握手连接。
    TCP连接成功后,逻辑上可理解为通信进程的双方具有两个流(输出流和输入流)。逻辑上可将两个流理解为两个通信管道的全双工通信模式,一个用于向对方发送数据,另一个用于接收对方的数据。

    套接字类有两个基本的方法可以获得两个通信管道的入口:

     socket.getInputStream()方法可获得输入字节流的入口地址;
     socket.getOutputStream()方法可获得输出字节流的出口地址;
    
    • 1
    • 2

    功能详细描述:
    客户端程序1:TCPClient.java具有网络接收和发送能力的程序。
    客户端程序2:TCPClientFX.java为界面模块。
    服务器程序:TCPServer.java 用于监听客户端的连接,具有网络接收和发送功能。

    网络对话方式是:
    客户端连接服务器,连接成功后,服务器首先给客户端发送一条欢迎信息;之后客户端程序每发送一条信息给服务器TCPServer.java,服务器接收并回送该信息到客户端,客户端接收并显示该信息;当客户端发送"bye",则结束对话。

    1. 程序设计第一步

    新建一个程序包,建议命名为chapter02;
    编写并运行 TCPServer程序,通过命令行窗口(netstat -ano | findstr "8008")察看是否已开启8008监听端口。作为初写服务端程序,可先仿照附录TCPServer.java完成(第五讲再介绍多用户版本)。
    服务器程序需要一直运行,所以处理代码一般放在while(true)这种无限循环中,TCPServer只能运行一次,且自身不能终止运行,要终止它运行,只能通过强制方式(如果通过IDE环境运行,则可以在IDE环境强制关闭)。

    2. 程序设计第二步

    编写并理解TCPCilent.java程序,之后再举一反三,去理解前一步的TCPServer.java的代码,理解服务端和客户端如何互动。
    
    • 1

    (1)定义对象构造方法的内容:

    socket = new Socket(host,port); //向服务进程发起TCP三次握手连接
    
    • 1

    Socket连接成功后,通过调用socket.getXXXXXStream( )方法,可获得字节输出流和字节输入流,输出流用于发送信息,输入流用于接收信息。并可通过以下组合方式封装为输入输出的字符流:

    new PrintWriter( // 设置最后一个参数为true,表示自动flush数据
        new OutputStreamWriter(//设置utf-8编码
            outputStream, "utf-8"), true);
    new BufferedReader(
        new InputStreamReader(inputStream, "utf-8"));
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:通过输出流往网络写出数据时,为了将缓存中的所有数据都推送出去,需要在写操作后执行flush命令,PrintWriter可以通过构造方法实现自动flush,所以就不需要显式的执行flush方法。另外,为了避免乱码,输入输出流的编码需要保持一致,为了兼容性,建议使用utf-8编码。
    (2)定义网络信息发送方法供外部调用:

     public void send(String msg) {
      //输出字符流,由Socket调用系统底层函数,经网卡发送字节流
      pw.println(msg);
    }
    
    • 1
    • 2
    • 3
    • 4

    (3)定义网络信息接收方法供外部调用:

    public String receive() {
      String msg = null;
      try {
        //从网络输入字符流中读信息,每次只能接受一行信息
        //如果不够一行(无行结束符),则该语句阻塞,
        // 直到条件满足,程序才往下运行
        msg = br.readLine();
      } catch (IOException e) {
        e.printStackTrace();
      }
      return msg;
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    (4)定义网络连接关闭方法供外部调用

    public void close() {
      try {
    if (socket != null) {
          //关闭socket连接及相关的输入输出流,实现四次握手断开,如图2.3所示
          socket.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    图2.3 TCP连接释放过程
    在这里插入图片描述
    注意:理解TCPCilent程序中的设计思路,如何连接对方,如何发送信息给对方,如何接收对方的信息。

    3. 程序设计第三步

    将客户端图形化,内部调用TCPClient模块中相应的方法完成网络对话功能:

    创建新界面并命名为TCPClientFX.java程序,其界面布局如图2.4所示。

    图2.4 网络对话界面
    在这里插入图片描述
    该窗体界面可参考第一讲SimpleFX的界面设计方法(在其基础上略作修改,并删除无关的文件IO部分)。例如可以添加一个HBox面板,用于容纳最上一行ip地址、端口输入框及连接按钮等控件,然后把这个HBox添加到主界面中央的VBox中,就可以达到类似图示效果。

    在“连接”按钮中设置如下动作:

    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");
      } catch (Exception e) {
        taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");      
      }
    }); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在“退出”按钮中设置如下动作:

    btnExit.setOnAction(event -> {
      if(tcpClient != null){
        //向服务器发送关闭连接的约定信息
        tcpClient.send("bye");
        tcpClient.close();
      }
      System.exit(0);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果用户不是通过退出按钮关闭窗体,而是点击右上角的×关闭,那么不会执行关闭socket的代码,所以可以把上面退出动作的代码封装成一个方法,在窗体关闭响应的事件中也执行,即使用:

    primaryStage.setOnCloseRequest(event -> {
      ……
    });
    
    • 1
    • 2
    • 3

    在“发送”按钮中添加网络发送和接收方法:

    btnSend.setOnAction(event -> {
      String sendMsg = tfSend.getText();
      tcpClient.send(sendMsg);//向服务器发送一串字符
      taDisplay.appendText("客户端发送:" + sendMsg + "\n");
      String receiveMsg = tcpClient.receive();//从服务器接收一行字符
      taDisplay.appendText(receiveMsg + "\n");
    }); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4. 建议

    我们可以看出,在一个设计良好的TCP服务器/客户端程序中,为了能够友好地完成整个通信过程,建议:
    (1)客户端成功连接服务器,服务器应该给客户端主动发送一条欢迎或通知等信息,作为整个通信的第一条信息,然后服务器进入监听阻塞状态,等待客户端的信息;客户端也需要相应获取服务端的信息。
    (2)服务器一般是不关闭的,一直等待客户连接,但其并不能主动知道客户端是否准备离开。所以客户端关闭时,给服务器发送一条约定的表示离开的信息(在本例中使用bye作为约定信息),以方便服务器可以做出响应。
    这两条都需要服务器和客户端互相约定,否则就可能有问题,例如,如果服务器在一个客户端连接成功后,并没有一条欢迎信息发送给客户端,客户端的读取欢迎信息的语句无法读取到内容,就被阻塞住,由于是单线程,甚至整个程序都会被卡住。要解决这个问题,可以使用下一讲的知识。

    扩展

    在TCPClientFX客户端窗体程序中,连接成功服务器后,如果用户再次点击“连接”按钮,会造成服务器资源浪费,还可能使程序运行不正常,无法正常发送信息;
    没有连接服务器时,或者发送bye以后,点击“发送”按钮,控制台也会产生异常;

    请修改程序,避免这种误操作。

    提示:一个简单的方案,就是在合适的时候禁用及启用对应的按钮。例如程序启动的时候,发送按钮需要禁用,在连接成功后启用,发送bye后又要再次禁用;用户连接成功后,禁用“连接”按钮,当用户通过按钮发送bye结束通话,重新启用“连接”按钮。

    项目结构

    在这里插入图片描述

    完整参考代码

    chapter02.TCPClient.java(客户端)

    package chapter02;
    
    import java.io.*;
    import java.net.Socket;
    
    /**
     * @projectName: NetworkApp
     * @package: chapter02
     * @className: TCPCilent
     * @author: GCT
     * @description: TODO
     * @date: 2022/9/4 18:06
     * @version: 1.0
     */
    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();
            }
        }
    
        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
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    chapter02.TCPServer.java(服务器)

    package chapter02;
    
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * @projectName: NetworkApp
     * @package: chapter02
     * @className: TCPServer
     * @author: GCT
     * @description: TODO
     * @date: 2022/8/30 20:28
     * @version: 1.0
     */
    public class TCPServer {
        private int port = 8008; //服务器监听端口
        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 (true) {
                Socket socket = null;
                try {
                    //此处程序阻塞等待,监听并等待客户发起连接,有连接请求就生成一个套接字。
                    socket = serverSocket.accept();
                    //本地服务器控制台显示客户端连接的用户信息
                    System.out.println("New connection accepted: " + socket.getInetAddress().getHostAddress());
                    BufferedReader br = getReader(socket);//定义字符串输入流
                    PrintWriter pw = getWriter(socket);//定义字符串输出流
                    //客户端正常连接成功,则发送服务器的欢迎信息,然后等待客户发送信息
                    pw.println("From 服务器:欢迎使用本服务!");
    
                    String msg = null;
                    //此处程序阻塞,每次从输入流中读入一行字符串
                    while ((msg = br.readLine()) != null) {
                        //如果客户发送的消息为"bye",就结束通信
                        if (msg.trim().equals("bye")) {
                            //向输出流中输出一行字符串,远程客户端可以读取该字符串
                            pw.println("From服务器:服务器断开连接,结束服务!");
                            System.out.println("客户端离开");
                            break; //结束循环
                        }
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        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
    • 83
    • 84
    • 85

    chapter02.TCPClientFX.java(界面)

    package chapter02;
    
    import chapter01.TextFileIO;
    import javafx.application.Application;
    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.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;
    
    /**
     * @projectName: NetworkApp
     * @package: chapter02
     * @className: TCPClientFX
     * @author: GCT
     * @description: TODO
     * @date: 2022/9/4 18:08
     * @version: 1.0
     */
    public class TCPClientFX extends Application {
        private Button btnExit = new Button("退出");
        private Button btnSend = new Button("发送");
        private Button btnConnect = new Button("连接");
    
    //    private Button btnOpen = new Button("加载");
    //    private Button btnSave = new Button("保存");
        //待发送信息的文本框
    
        private TextField tfip = new TextField();
        private TextField tfport = new TextField();
    
        private TextField tfSend = new TextField();
        //显示信息的文本区域
        private TextArea taDisplay = new TextArea();
    
        private TCPClient tcpClient;
    
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) {
    
            //    将TextFileIO类实例化为textFileIO
            TextFileIO textFileIO = new TextFileIO();
    
            BorderPane mainPane = new BorderPane();
    
    //        顶部的ip和端口输入框区域
            HBox topHBox = new HBox();
            topHBox.setSpacing(10);
            topHBox.setPadding(new Insets(10,20,10,20));
            topHBox.setAlignment(Pos.CENTER);
            topHBox.getChildren().addAll(new Label("IP: "),tfip,new Label("端口号:"),tfport,btnConnect);
            mainPane.setTop(topHBox);
    
    
            //内容显示区域
            VBox vBox = new VBox();
            vBox.setSpacing(10);//各控件之间的间隔
            //VBox面板中的内容距离四周的留空区域
            vBox.setPadding(new Insets(10,20,10,20));
            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);
    //        hBox.getChildren().addAll(btnSend,btnSave,btnOpen,btnExit);
            hBox.getChildren().addAll(btnSend,btnExit);
            mainPane.setBottom(hBox);
            Scene scene = new Scene(mainPane,700,400);
            primaryStage.setScene(scene);
            primaryStage.show();
    //……
    --------事件处理代码部分--------
    //……
    
    //        连接
            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");
                } catch (Exception e) {
                    taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");
                }
            });
    
            btnExit.setOnAction(event -> {
                if(tcpClient != null){
                    //向服务器发送关闭连接的约定信息
                    tcpClient.send("bye");
                    tcpClient.close();
                }
                System.exit(0);
            });
    
    
    //        设置taDisplay自动换行
            taDisplay.setWrapText(true);
    //        设置taDisplay只读
            taDisplay.setEditable(false);
    //        退出按钮事件
            btnExit.setOnAction(event -> {System.exit(0);});
    //        发送按钮事件
            btnSend.setOnAction(event -> {
                String sendMsg = tfSend.getText();
                if (sendMsg.equals("bye")){
                    btnConnect.setDisable(false);
                    btnSend.setDisable(true);
                }
    
                tcpClient.send(sendMsg);//向服务器发送一串字符
                taDisplay.appendText("客户端发送:" + sendMsg + "\n");
                String receiveMsg = tcpClient.receive();//从服务器接收一行字符
                taDisplay.appendText(receiveMsg + "\n");
            });
    
    
    
    //        tfSend.setOnKeyPressed(event -> {
    //            if (event.getCode() == KeyCode.ENTER){
    //                if (event.isShiftDown()){
    //                    String msg = tfSend.getText();
    //                    taDisplay.appendText("echo: "+ msg + "\n");
    //                    tfSend.clear();
    //                }
    //                else{
    //                    String msg = tfSend.getText();
    //                    taDisplay.appendText(msg + "\n");
    //                    tfSend.clear();
    //                }
    //            }
    //
    //        });
    
    //        btnSave.setOnAction(event -> {
    //            //添加当前时间信息进行保存
    //            textFileIO.append(
    //                    LocalDateTime.now().withNano(0) +" "+ taDisplay.getText());
    //        });
    //
    //        btnOpen.setOnAction(event -> {
    //            String msg = textFileIO.load();
    //            if(msg != null){
    //                taDisplay.clear();
    //                taDisplay.setText(msg);
    //            }
    //        });
        }
    
        private void endSystem(){
            if (tcpClient!=null){
                tcpClient.send("bye");
                tcpClient.close();
            }
        }
    
    }
    
    
    • 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
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182

    运行结果

    chapter02.TCPServer.java
    在这里插入图片描述
    chapter02.TCPClient.java
    在这里插入图片描述
    chapter02.TCPClientFX.java
    在这里插入图片描述

  • 相关阅读:
    【经验】解决重置 Windows 10 时报错:“无法找到介质” 的错误
    自动化测试如此容易!多语言自动化测试框架 Selenium 编程(C#篇)
    Mybatis动态sql条件查询、判断空值和空字符串
    Springboot零星知识点1
    ASP.NET基于Ajax+Lucene构建搜索引擎的设计和实现(源代码+论文)
    基于单片机的灭火机器人设计
    Airbnb 如何实现 Kubernetes 集群动态扩展
    网站seo怎么优化?
    (echarts)折线图封装相关总结及使用
    Linux:mongodb数据库基础操作(3.4版本)
  • 原文地址:https://blog.csdn.net/GCTTTTTT/article/details/126769428