使用javaFx制作简单的网络通信小程序——互联网程序设计基础(2)

上一篇博客我们介绍了javaFx制作一个小窗口,今天我们将用该小窗口实现网络之间的通信。首先网络通信需要一定的计网知识,所谓网络通信其实分为很多层,比如网络层,链路层,物理层等,而我们使用java编写的程序主要是在应用层这个层面。在该层中,计算机与计算机之间的通信,其实可以概括为ip地址标识一台独一无二的主机,而程序与程序之间通信又需要对应的端口,因此要与一台计算机通信,需要ip和端口号。在java中有socket这个封装的方法,自动帮我们实现建立连接的三次握手和断开连接的四次握手。

要实现建立连接,断开连接和数据的发送接收,我们首先要建一个类,类叫做TCPClient

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(达到buffer值的大小后刷新缓存)数据
                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();
        }
    }
}

这里涉及到java中非常重要的流,我在网络上看来很多的介绍,这里我想总结一下,如果觉得还是不懂可以自行去搜索。首先流就是传输的数据的通道,比如对于程序来说,要读入一些数据,因此我们就要从文件或者从网络中建立一条输入流,数据会从文件中传输到程序,同理输出流就是将程序中的数据输出到文件或者网络中。所有数据在计算机中都是以字节表示,因此最基础的处理字节流为InputStream和OutputStream。

但是Java中字符是以Unicode形式存储的,一个字符占用两个字节,然而InputStream和OutputStream都是以字节形式读取或写出数据的,会将一个字符拆分成两个字节来读写这样会造成乱码,因此又有专门针对字符的流Reader和Writer。上述字节流和字符流只是对数据进行传输,直接操作文件,因此叫做结点流。

在这里插入图片描述

上述的结点流,比如InputStream,在此基础上又有了新的流,比如FileInputStream等,这些都是节点流,只不过FileInputStream类中封装的方法会多一点。

接下来说一下处理流,处理流的对象不是文件,正是流。一个结点流中其实封装的方法是非常简单的,不满足我们日常的需要,如果只用节点流进行开发将要自己写很多方法,为此大佬们针对结点流建立了处理流,我们把一个节点流封装在处理流中,就可以使用处理流的静态方法。

在这里插入图片描述

如果你不太想理解这些琐碎的流细节,只想知道最基本的流应用,那么我可以给大家一个模板:

//处理流(字符流(字节流),"编码类型"),这里建立了一个文件输出流
pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file, true), "utf-8"));

说完流后,我们回到通信程序中。我们建立好了最重要的TCPClient类后,接下来就是在javaFx中调用。

import chapter01.TextFileIO;
import chapter02.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.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;

public class TCPClientThreadFx extends Application {

    //全局定义button按钮
    private Button btnExit = new Button("退出");
    private Button btnSend = new Button("发送");
    private Button btnOpen = new Button("加载");
    private Button btnSave = new Button("保存");
    private Button btnClear = new Button("清空");
    private Button btnConnect = new Button("连接");
    private Button btnLossConnetc = new Button("断开连接");

    private TextField tfSend = new TextField();
    //显示信息的文本区域
    private TextArea taDisplay = new TextArea();
    //顶部两个输入
    private TextField tfIP = new TextField("202.116.195.71");
    private TextField tfPort = new TextField("8008");

    //预先定义好接收和发送信息流的类
    private TCPClient tcpClient;

    //判断是否连接
    private Boolean connect = false;

    //预定义一个多线程
    private Thread receiveThread;


    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        BorderPane mainPane = new BorderPane();

        //顶部的布局
        HBox top = new HBox();
        top.setSpacing(10);
        top.getChildren().addAll(new Label("IP地址:"),tfIP , new Label("端口:"),tfPort,btnConnect,btnLossConnetc);
        top.setPadding(new Insets(20,20,10,20));//设置顶部hbox的留白区域
        top.setAlignment(Pos.CENTER);
        mainPane.setTop(top);

        //内容显示区域
        VBox vBox = new VBox();
        vBox.setSpacing(10);//各个控件之间的间距

        //vbox面板中的内容距离四周的留空区域
        taDisplay.setMaxSize(Double.MAX_VALUE,Double.MAX_VALUE);

        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面板中内容距离四周的留空区域
        hBox.setAlignment(Pos.CENTER_RIGHT);//靠右对齐
        hBox.getChildren().addAll(btnSend,btnSave,btnOpen,btnExit,btnClear);
        mainPane.setBottom(hBox);

        Scene scene = new Scene(mainPane,750 , 500);
        primaryStage.setScene(scene);
        primaryStage.setTitle("阿柴的简单对话小程序");
        primaryStage.show();

        //自动换行只读
        taDisplay.setWrapText(true);
        taDisplay.setEditable(false);
        //发送按钮不可用
        btnSend.setDisable(true);
        btnLossConnetc.setDisable(true);


        //推出按钮动作设置
        btnExit.setOnAction(event -> { System.exit(0);});
     }
}

接下来详细讲解一下按钮触发的事件,首先是btnConnect这个button。

btnConnect.setOnAction(event -> {
            if(!connect) {
                String ip = tfIP.getText().trim(); //删除字符头尾空白字符
                String port = tfPort.getText().trim();

                try {
                    //tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
                    tcpClient = new TCPClient(ip, port);
                    connect = true;
                    btnSend.setDisable(false);
                    btnLossConnetc.setDisable(false);
                    btnConnect.setDisable(true);
                    //每次连接都会建立一个多线程
                    receiveThread = new Thread(()->{
                        String msg = null;
                        //不知道服务器有多少回传信息,就持续不断接收
                        //由于在另外一个线程,不会阻塞主线程的正常运行
                        while ((msg = tcpClient.receive()) != null) {
                            //runLater中的lambda表达式不能直接访问外部非final类型局部变量
                            //所以这里使用了一个临时变量
                            String msgTemp = msg;
                            Platform.runLater(()->{
                                taDisplay.appendText( msgTemp + "\n");
                            });
                        }
                        //跳出了循环,说明服务器已关闭,读取为null,提示对话关闭
                        Platform.runLater(()->{
                            taDisplay.appendText("对话和线程已关闭!\n" );
                        });
                    });;
                    receiveThread.start(); //启动线程
                } catch (Exception e) {
                    taDisplay.appendText("服务器连接失败!" + e.getMessage() + "\n");
                }
            }
            else {
                taDisplay.appendText("已有连接,请先退出连接!\n");
            }
        });

这里我们首先从tfIP和tfPort这个textfield得到要连接的IP地址和端口号,接下来实例化TCPClient类,接下来就实例化一个多线程receiveThread。为什么要建立一个多线程专门接收信息呢?这里就得会看TCPClient中的receive代码。

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

这段代码是在TCPClient中的,可以发现我们接收信息是调用了br(Buffer Reader,一种处理流)中的readline方法。问题就是出在这个方法,如果我们在程序的主线程中调用该方法,那么在主线程没有收到网络输入流信息的时候,就会一直处于阻塞等待状态,直观体验就是卡死。因此当遇到又阻塞语句时候,我们都会开设一个新的线程来处理,新线程被阻塞但不会影响主线程。如果还是不懂,我举个例子。比如有个人就站在网络输入流中,只负责接收,别的时候都不响应。当网络流中又信息就立刻把该消息传回给程序。当这类人出现在主线程中,由于程序是按部就班的,当程序没有收到这个人传来的网络流信息,那么程序就卡死。有点像多米诺骨牌,只有前一张牌倒下,接下来的牌才会受到推力倒下

由于服务器发送的信息是有很多的,因此我们在多线程中要用一个循环来接收。

//不知道服务器有多少回传信息,就持续不断接收
                        //由于在另外一个线程,不会阻塞主线程的正常运行
                        while ((msg = tcpClient.receive()) != null) {
                            //runLater中的lambda表达式不能直接访问外部非final类型局部变量
                            //所以这里使用了一个临时变量
                            String msgTemp = msg;
                            Platform.runLater(()->{
                                taDisplay.appendText( msgTemp + "\n");
                            });
                        }

循环结束的条件就是收到服务器发送的null,表示服务器要断开连接,因此可以断开连接。这里插一嘴,客户端是先与服务器建立连接,而断开连接不能只是客户端随便退出程序就行,要告诉服务器要断开连接才行,当收到服务器确认断开连接(在该程序为收到服务器发送的null)就可以断开连接。

在多线程中,所有的javaFx部件代码都要放在Platform.runLater()中才不会报错。

接下来是断开连接按钮btnLossConetc

btnLossConnetc.setOnAction(event -> {
            if(tcpClient != null){
                //向服务器发送关闭连接的约定信息
                tcpClient.send("bye");
                try {
                    Thread.sleep(100);
                }catch (Exception e){
                    e.printStackTrace();
                }
                tcpClient.close();
                taDisplay.appendText("From local:服务器已断开连接,结束服务!\n");
                connect = false;
                btnSend.setDisable(true);
                btnConnect.setDisable(false);
                btnLossConnetc.setDisable(true);
            }
        });

这里我讲一下中间的Thread.sleep(100)这行代码。首先当我们要关闭连接时,我们会发送约定好的bye信息,当服务器收到bye信息后会发送一个null,然后我们的多线程会退出循环然后完美退出。但是!由于网络传输需要时间,从发送到接收null,这段时间中,我们程序就不会等待而是直接tcpClient.close(),断开连接。那么带来的问题是什么?我们的tcpClient已经关闭了,但是副线程还在用tcpClient.receive()这个方法,因此会报错。此时我们使用Thread.sleep(100),让主线程先休眠一百毫秒,确保副线程接收到服务器的null然后退出循环自动关闭后再断开连接。

后面是一些其他按钮的action,我就不展开说说了,其中要用到的文本读取类TextFileIO在上一篇博客中有源代码。

btnExit.setOnAction(event -> {
            if(tcpClient != null){
                //向服务器发送关闭连接的约定信息
                tcpClient.send("bye");
                try {
                    Thread.sleep(100);
                }catch (Exception e){
                    e.printStackTrace();
                }
                tcpClient.close();
                connect = false;
            }
            System.exit(0);
        });

        //设置当按下整个界面的交叉键,那将和服务器断开连接
        primaryStage.setOnCloseRequest(event -> {
            if(tcpClient != null){
                //向服务器发送关闭连接的约定信息
                tcpClient.send("bye");
                try {
                    Thread.sleep(100);
                }catch (Exception e){
                    e.printStackTrace();
                }
                tcpClient.close();
                connect = false;
                btnSend.setDisable(true);
            }
            System.exit(0);
        });


        btnSend.setOnAction(event -> {
            if(connect) {
                String sendMsg = tfSend.getText();
                tcpClient.send(sendMsg);//向服务器发送一串字符
                taDisplay.appendText("客户端发送:" + sendMsg + "\n");
                if(sendMsg.equals("bye")){
                    connect = false;
                    btnSend.setDisable(true);
                    btnConnect.setDisable(false);
                    btnLossConnetc.setDisable(true);
                }
                tfSend.clear();
            }
            else {
                taDisplay.appendText("Please 先连接服务器!!!!!\n");
                tfSend.clear();
            }
        });

        //设置Shift+回车,则在信息前加上 echo: 的信息头再发送
        tfSend.setOnKeyPressed(event -> {
            if(event.getCode() == KeyCode.ENTER){ //判断是否按下回车
                event.consume();
                if(event.isShiftDown()){  //判断按下回车的同时按下shift
                    if(connect){
                        String sendMsg = tfSend.getText();
                        tcpClient.send(sendMsg);//向服务器发送一串字符
                        taDisplay.appendText("本地服务器echo:" + sendMsg + "\n");
                        if(sendMsg.equals("bye")){
                            connect = false;
                            btnSend.setDisable(true);
                            btnConnect.setDisable(false);
                            btnLossConnetc.setDisable(true);
                        }
                        tfSend.clear();
                    }else {
                        taDisplay.appendText("Please 先连接服务器!!!!!\n");
                        tfSend.clear();
                    }
                }
                else{ //当只输入回车时,就直接发送
                    if(connect) {
                        String sendMsg = tfSend.getText();
                        tcpClient.send(sendMsg);//向服务器发送一串字符
                        taDisplay.appendText("客户端发送:" + sendMsg + "\n");
                        if(sendMsg.equals("bye")){
                            connect = false;
                            btnSend.setDisable(true);
                            btnConnect.setDisable(false);
                            btnLossConnetc.setDisable(true);
                        }
//                        String receiveMsg = tcpClient.receive();//从服务器接收一行字符
//                        taDisplay.appendText(receiveMsg + "\n");
                        tfSend.clear();
                    }
                    else {
                        taDisplay.appendText("Please 先连接服务器!!!!!\n");
                        tfSend.clear();
                    }
                }
            }
        });

        btnSave.setOnAction(event -> {
            //新建一个TextFileIO类实例化为textFileIO
            TextFileIO textFileIO = new TextFileIO();
            //添加当地时间信息进行保存
            textFileIO.append(
                    LocalDateTime.now().withNano(0) +"\n"+ taDisplay.getText());
        });

        btnOpen.setOnAction(event -> {
            //新建一个TextFileIO类实例化为textFileIO
            TextFileIO textFileIO = new TextFileIO();
            String msg = textFileIO.load();
            if(msg != null){
                taDisplay.clear();
                taDisplay.setText(msg);
            }
        });

        btnClear.setOnAction(event -> {
            taDisplay.clear();
        });

    }

这里还提供了服务器的代码TCPServe类,负责监听端口8008,大家可以自己连接自己的电脑IP测试一下。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    private int port = 8008; //服务器监听端口
    private ServerSocket serverSocket; //定义服务器套接字

    public TCPServer() throws IOException {
        serverSocket = new ServerSocket(8008);//初始化类时候,服务器套接字选定8008端口
        System.out.println("服务器启动监听在 " + port + " 端口");
    }

    //服务器发送数据给其他连接的主机的方法
    private PrintWriter getWriter(Socket socket) throws IOException {
        //获得输出流缓冲区的地址,这里socket应该是缓冲区地址,从缓冲区中直接取出数据,数据类型是二进制
        OutputStream socketOut = socket.getOutputStream();
        //网络流写出需要使用flush,这里在PrintWriter构造方法中直接设置为自动flush
        return new PrintWriter( //OutputStreamWrite是字符写流,将socketOut的字节进行编码
                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.equals("bye")) {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:服务器断开连接,结束服务!");
                        System.out.println("客户端离开");
                        break; //结束循环
                    }

                    if (msg.equals("在吗?")){
                        pw.println("From 服务器:死心吧,我有喜欢的人类了");
                    }
                    else if(msg.endsWith("吗?")){
                        pw.println("From服务器:" + msg.substring(0,msg.length() - 2) + "!");
                    }else {
                        //向输出流中输出一行字符串,远程客户端可以读取该字符串
                        pw.println("From服务器:" + msg);
                    }


//                    pw.println("From 服务器二:" + msg);
//                    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();
    }
}
  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值