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

一、简单网络对话程序

设计任务:客户端向服务器发送字符串,并能读取服务器返回的字符串。
知识点: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();
    }
}

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

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

请添加图片描述

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

五、建议

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

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

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值