JavaFX实现网络对话程序设计(互联网程序设计课程 第2讲)

编程入门 行业动态 更新时间:2024-10-25 06:28:12

JavaFX实现网络对话<a href=https://www.elefans.com/category/jswz/34/1771020.html style=程序设计(互联网程序设计课程 第2讲)"/>

JavaFX实现网络对话程序设计(互联网程序设计课程 第2讲)

文章目录

  • 简单网络对话程序
    • 1. 程序设计第一步
    • 2. 程序设计第二步
    • 3. 程序设计第三步
    • 4. 建议
    • 扩展
  • 项目结构
  • 完整参考代码
    • chapter02.TCPClient.java(客户端)
    • chapter02.TCPServer.java(服务器)
    • chapter02.TCPClientFX.java(界面)
  • 运行结果

简单网络对话程序

**设计任务:**客户端向服务器发送字符串,并能读取服务器返回的字符串。
知识点: 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: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)定义对象构造方法的内容:

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

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

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

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

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

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

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

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

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

图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");      }
}); 

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

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

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

primaryStage.setOnCloseRequest(event -> {……
});

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

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

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

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构造方法中直接设置为自动flushreturn 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();}
}

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);}@Overridepublic void start(Stage primaryStage) {//    将TextFileIO类实例化为textFileIOTextFileIO 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();}}}

运行结果

chapter02.TCPServer.java

chapter02.TCPClient.java

chapter02.TCPClientFX.java

更多推荐

JavaFX实现网络对话程序设计(互联网程序设计课程 第2讲)

本文发布于:2023-06-14 20:07:45,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/712548.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:程序设计   互联网   课程   网络   JavaFX

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!