一、简单网络对话程序
设计任务:客户端向服务器发送字符串,并能读取服务器返回的字符串。
知识点: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作为约定信息),以方便服务器可以做出响应。
这两条都需要服务器和客户端互相约定,否则就可能有问题,例如,如果服务器在一个客户端连接成功后,并没有一条欢迎信息发送给客户端,客户端的读取欢迎信息的语句无法读取到内容,就被阻塞住,由于是单线程,甚至整个程序都会被卡住。要解决这个问题,可以使用下一讲的知识。