JavaFX实现多线程程序设计技术(互联网程序设计课程 第3讲)


教学与实践目的:学会在网络应用开发中运用Java多线程技术。

一、程序的基本调试技术

程序无语法错误、能运行,但没有出现预期的结果,说明程序可能存在逻辑错误,解决这类错误的主要方法是查看程序运行过程中的内存变量值。一个常用的手段是通过打印语句打印出变量的值,例如使用System.out.println(待排查的变量)。但更强大的方法是使用IDE提供的断点功能。

在idea设断点并查看变量的方法:

鼠标点击要查看变量所在代码行的行号右侧空白处,出现棕红色实心圆,即表示在此处打了断点,调试时程序会在此处停住,方便观察程序运行的状况和各变量的即时值。
首先新建一个包,命名为chapter03,然后将上一讲的TCPServer.java、TCPClient.java、TCPClientFX.java复制到这个包中,注意程序中第一行语句是否自动修改为package chapter03;
假如我们要观察获取的IP地址是否符合预期,可以在客户端窗口程序TCPClientFX中选择一行有相关变量的代码行,如图3.1,鼠标点击行号右侧标注断点;
右上角下拉框选中“TCPClientFX”,再点击“调试”图标(也可以直接从“run”菜单或右键点击主窗体的弹出菜单中选择debug方式运行),窗口程序运行到红色断点行时会停留,便于观察此时IP、port等变量的状态值,如图3.2所示。通过图3.2所示红色框区域,可以让程序单步执行,一步一步地观察程序执行的情况,如果当前行代码中有方法的调用,step over表示跳过方法之间运行下一行代码,而step into则继续下钻,可以进入方法内部,一般只是用于进入自定义方法。
如果要调试的代码行是在一个新的线程的代码块中,则需要右键点击断点图标,在弹出菜单中,将"Suspend:" 后面的单选按钮从"All" 改为 “Thread”。
在这里插入图片描述
在这里插入图片描述
图3.2 调试暂停界面

二、理解阻塞语句

在同一个进程中,一条阻塞语句的执行影响着下条语句何时被执行。如果该条语句没有执行完,那么下条语句是不可能进入执行状态的,因此,从字面层上理解,该条语句阻塞了下面语句的执行。
JAVA类BufferedReader中readLine( )方法的调用是阻塞语句,若该套接字的输入流中没有带行结束符(如\n)的字符可读,则该语句会处于阻塞状态,直到条件出现行结束符,才会执行下面的语句。

阻塞状态程序演示:

(1)将TCPServer.java程序中的发送语句临时禁用(验证完再还原),如:

		//向输出流中输出一行字符串,远程客户端可以读取该字符串
        //pw.println("来自服务器:" + msg);  临时禁用

即服务器不回传信息;
(2)启动TCPServer.java服务程序,再启动TCPClientFX.java客户端程序,发送信息,发现客户程序不能正常运行,发送按钮甚至整个程序失去响应。
(3)强行终止TCPClientFX,在窗口程序的发送语句处设置断点,如图3.3所示。然后在调试状态运行该程序,逐行调试(遇到自定义的方法,建议使用step into跟踪进入)。在执行到receive()方法时,使用step into跟踪进方法会发现程序会阻塞在msg = br.readLine(); 处(因为服务器没有返回,客户端的输入流队列中是空的,所以被阻塞)。所以程序设计时一定要小心。
在这里插入图片描述
图3.3 断点位置

三、理解读一行功能

同理,若套接字的输入流中有多行信息,调用一次readLine()方法,只是读出当前的一行(当然你可以调用其他的“读”方法)。

程序演示:

(1)在TCPServer.java程序中多增加一条信息返回语句,如:

pw.println("来自服务器:" + msg);
//下面多增加一条信息返回语句
pw.println("来自服务器,重复发送: " + msg);

然后启动服务端程序;
(2)启动客户端TCPClientFX程序,发现客户显示区每次只显示一条信息,且与你发送的信息不同步。因为每一次互动,服务器返回两行信息,而客户端只是读取最前面的一行信息。
如何解决阻塞和多行信息的读写问题? 一个常用的解决方案就是多线程。

四、多线程技术

多线程程序的执行如图3.4所示。
在这里插入图片描述
图3.4 程序调用的顺序执行与线程调用的并行执行

有了多线程技术,我们就有了更多选择。

1. 编写读取服务器信息的线程

在TCPClientFX.java程序中,发送信息是可以通过“发送”按钮来实现主动控制,可接收信息是被动的,你不知道输入流中有多少信息。
为此,在窗口程序中添加一个线程专门负责读取输入流中的信息, 同时,“发送”按钮动作中,读取输入流信息的代码就需要删除。
现在右键选择TCPClientFX.java重构(Refactor),重命名为TCPClientThreadFX.java(采用如图3.5所示的方式)
在这里插入图片描述
并在合适的位置(例如,在btnConnect的动作事件中,在建立了连接之后)编写如下线程代码,用于接收服务器的信息,为了简洁,匿名内部类使用了lambda的写法:

public class TCPClientThreadFX extends Application {
Thread receiveThread; //定义成员变量,读取服务器信息的线程
//…… 省略……

//以下代码位于btnConnect.setOnActon方法中的合适位置
//用于接收服务器信息的单独线程
receiveThread = new Thread(()->{
  String msg = null;
  //不知道服务器有多少回传信息,就持续不断接收
  //由于在另外一个线程,不会阻塞主线程的正常运行
  while ((msg = tcpClient.receive()) != null) {
    //runLater中的lambda表达式不能直接访问外部非final类型局部变量
    //所以这里使用了一个临时常量,可以省略final,但本质还是会作为常量使用
    final String msgTemp = msg; //msgTemp实质是final类型
    Platform.runLater(()->{
      taDisplay.appendText( msgTemp + "\n");
    });
  }
//跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
  Platform.runLater(()->{
    taDisplay.appendText("对话已关闭!\n" );
  });
}, "my-readServerThread"); //给新线程取别名,方便识别
receiveThread.start(); //启动线程
…… 省略……

以上代码中有四点注意:
(1)由于是新开的一个线程循环读取服务器的信息,所以不用考虑服务器是否有发欢迎信息,就算读取不到信息也只是阻塞这个线程,主程序本身使用没有任何影响(单线程就会卡住)。事实上服务器发多少信息都没问题,该线程通过循环语句来读取,没信息过来就阻塞等待,当服务器关闭连接时,就会跳出循环语句,结束本线程;
(2)现在接收并显示服务端信息的任务交给了一个单独的线程,那么原来主线程中连接按钮和发送按钮的动作事件代码中,关于接收并显示服务端信息的代码都必须删除,因为该任务完全由单独线程接管,如果这些代码保留,会存在潜在隐患(可能出现什么问题?)
(3)对于JavaFX窗体界面,在新线程中无法直接更新界面中有关控件的内容,只能将更新代码放在PlatForm.runLater(Runnable XXX)方法的Runnable子类实例中,如以上代码第15-17行、20-22行所示;
(4)匿名内部类或lambda表达式中,不能访问外部类方法中的非final类型的局部变量,例如上面第16行代码如果直接使用taDisplay.appendText( msg + “\n”);就会报错,所以代码第14行使用了个临时常量来解决这个问题,其实不使用final关键字,也会自动识别为final类型来使用(如果将msg定义为类中的成员变量,就没有这个限制,可以直接访问)

2. 程序退出部分思考

由于“退出”按钮和关闭窗体的事件响应都需要调用这部分代码,所以将之封装为exit()方法:

private void exit() {
  if(tcpClient != null){
    tcpClient.send("bye"); //向服务器发送关闭连接的约定信息
    tcpClient.close();
  }
  System.exit(0);
}

先成功连接服务器,在正常发送一些信息后,不通过按钮发送信息输入区的bye告知服务器,这时候直接点击退出,很大概率会抛出异常信息后结束程序,其实这不算问题,交互还是可以正常完成。如果你的程序出现了这种情况,请思考抛出异常的原因?请尝试解决。

项目结构

在这里插入图片描述

完整代码

chapter03/TCPClient.java

package chapter03;

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

    }
}

chapter03/TCPClientThreadFX.java

package chapter03;

import chapter01.TextFileIO;
import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * @projectName: NetworkApp
 * @package: chapter02
 * @className: TCPClientFX
 * @author: GCT
 * @description: TODO
 * @date: 2022/9/4 18:08
 * @version: 1.0
 */
public class TCPClientThreadFX extends Application {
    private Button btnExit = new Button("退出");
    private Button btnSend = new Button("发送");
    private Button btnConnect = new Button("连接");


    Thread receiveThread; //定义成员变量,读取服务器信息的线程

//    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");

                //用于接收服务器信息的单独线程
                receiveThread = new Thread(()->{
                    String msg = null;
                    //不知道服务器有多少回传信息,就持续不断接收
                    //由于在另外一个线程,不会阻塞主线程的正常运行
                    while ((msg = tcpClient.receive()) != null) {
                        //runLater中的lambda表达式不能直接访问外部非final类型局部变量
                        //所以这里使用了一个临时常量,可以省略final,但本质还是会作为常量使用
                        final String msgTemp = msg; //msgTemp实质是final类型
                        Platform.runLater(()->{
                            taDisplay.appendText( msgTemp + "\n");
                        });
                    }
//跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
                    Platform.runLater(()->{
                        taDisplay.appendText("对话已关闭!\n" );
                    });
                }, "my-readServerThread"); //给新线程取别名,方便识别
                receiveThread.start(); //启动线程
            } 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);});
        btnExit.setOnAction(event -> endSystem());
//        发送按钮事件
        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();
        }

        System.exit(0);
    }

}

chapter03/TCPServer.java

package chapter03;

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

                    //下面多增加一条信息返回语句
                    pw.println("来自服务器,重复发送: " + 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();
    }
}

lookupscore/LookUpScore.java

package lookupscore;

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 LookUpScore {
    private Socket socket; //定义套接字
    //定义字符输入流和输出流
    private PrintWriter pw;
    private BufferedReader br;

    public LookUpScore(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{
        LookUpScore tcpClient = new LookUpScore("127.0.0.1", "8008");
        tcpClient.send("hello");
        System.out.println(tcpClient.receive());

    }
}

lookupscore/LookUpScoreFX.java

package lookupscore;

import chapter01.TextFileIO;
import chapter03.TCPClient;
import javafx.application.Application;
import javafx.application.Platform;
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.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * @projectName: NetworkApp
 * @package: chapter02
 * @className: TCPClientFX
 * @author: GCT
 * @description: TODO
 * @date: 2022/9/4 18:08
 * @version: 1.0
 */
public class LookUpScoreFX extends Application {
    private Button btnExit = new Button("退出");
    private Button btnSend = new Button("发送");
    private Button btnConnect = new Button("连接");


    Thread receiveThread; //定义成员变量,读取服务器信息的线程

//    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.setTitle("登录查成绩");
        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");
            }
            //用于接收服务器信息的单独线程
            receiveThread = new Thread(()->{
                String msg = null;
                //不知道服务器有多少回传信息,就持续不断接收
                //由于在另外一个线程,不会阻塞主线程的正常运行
                while ((msg = tcpClient.receive()) != null) {
                    //runLater中的lambda表达式不能直接访问外部非final类型局部变量
                    //所以这里使用了一个临时常量,可以省略final,但本质还是会作为常量使用
                    final String msgTemp = msg; //msgTemp实质是final类型
                    Platform.runLater(()->{
                        taDisplay.appendText( msgTemp + "\n");
                    });
                }
//跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
                Platform.runLater(()->{
                    taDisplay.appendText("对话已关闭!\n" );
                });
            }, "my-readServerThread"); //给新线程取别名,方便识别
            receiveThread.start(); //启动线程
        });

        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);});
        btnExit.setOnAction(event -> endSystem());
//        发送按钮事件
        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();
        }

        System.exit(0);
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GCTTTTTT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值