chapter02(网络对话)
简单网络对话程序
设计任务:客户端向服务器发送字符串,并能读取服务器返回的字符串。
知识点:TCP套接字技术,C/S软件架构程序设计
重点理解:Java客户套接字类Socket和服务器套接字类ServerSocket,以及配套使用流的读/写类BufferedReader/PrintWriter
。
在C/S软件架构程序
设计技术中,实现网络通信的两个应用进程,一个叫做服务进程,另一个叫做客户进程,如图所示。服务进程首先被动打开一个 监听端口,如8008,客户进程主动访问这个端口,完成对话聊天前的TCP三 次握手连接。
Java 的 TCP/IP 套接字编程将底层的细节进行了封装,其编程模型如图
socket套接字类
在Java TCP/IP
编程模型中,有两个套接字类:服务进程中的是 ServerSocket 类,客户进程中的是Socket类。 服务进程首先开启一个或多个监听端口,客户进程向服务进程发起TCP三次握手连接。
TCP 连接成功后,逻辑上可理解为通信进程的双方具有两个流(输出流和输入流)。逻辑上可将两个流理解为两个通信管道的全双工通信模式,一个用于向对方发送数据,另一个用于接收对方的数据。
成员方法
套接字类有两个基本的方法可以获得两个通信管道:
- socket.getInputStream() 方法可获得输入字节流的入口;
- socket.getOutputStream() 方法可获得输出字节流的出口;
监听套接字和通信套接字
在网络编程中,监听套接字(Listening Socket)和通信套接字(Communication Socket)是两个重要的概念,通常用于实现客户端和服务器之间的通信。以下是对这两个概念的详细解释:
1. 监听套接字(Listening Socket)
- 定义:监听套接字是服务器端创建的套接字,用于监听来自客户端的连接请求。它不直接用于数据传输,而是用于接受连接。
- 创建:在服务器端,首先需要创建一个套接字并将其绑定到一个特定的地址和端口,然后调用
listen()
方法使其进入监听状态。 - 功能:
- 等待客户端的连接请求。
- 一旦有客户端请求连接,服务器会接受这个连接并创建一个新的通信套接字。
示例代码(C++):
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字
if (server_fd == 0) {
std::cerr << "Socket creation failed" << std::endl;
return -1;
}
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口
address.sin_port = htons(8080); // 端口号
bind(server_fd, (struct sockaddr *)&address, sizeof(address)); // 绑定地址和端口
listen(server_fd, 3); // 开始监听,最大连接数为3
std::cout << "Listening on port 8080..." << std::endl;
return 0;
}
2. 通信套接字(Communication Socket)
- 定义:通信套接字是服务器在接受客户端连接请求后创建的套接字,用于与特定客户端进行数据传输。
- 创建:当监听套接字接受到连接请求后,服务器会调用
accept()
方法,返回一个新的套接字(通信套接字),用于与该客户端进行通信。 - 功能:
- 进行数据的发送和接收。
- 每个连接的客户端都有一个独立的通信套接字。
- 示例代码(C++):
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
int addrlen = sizeof(address);
// 省略监听套接字的创建和绑定代码...
listen(server_fd, 3);
std::cout << "Listening on port 8080..." << std::endl;
int client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); // 接受连接
if (client_socket < 0) {
std::cerr << "Accept failed" << std::endl;
return -1;
}
const char *message = "Hello from server";
send(client_socket, message, strlen(message), 0); // 发送数据
std::cout << "Message sent to client" << std::endl;
close(client_socket); // 关闭通信套接字
close(server_fd); // 关闭监听套接字
return 0;
}
总结
- 监听套接字:用于等待和接受客户端的连接请求。
- 通信套接字:用于与已连接的客户端进行数据传输。
这种设计使得服务器能够同时处理多个客户端连接,因为每个连接都有自己的通信套接字,而监听套接字则保持在监听状态,等待新的连接请求。如果你有更多问题或需要进一步的帮助,请告诉我!
模拟Button的点击事件
在JavaFX中,模拟按钮(Button)的点击可以通过直接调用按钮的fire()
方法来实现,但请注意,Button
类本身并没有直接提供fire()
方法。不过,Button
类继承自javafx.scene.control.Control
,而Control
类有一个受保护的fire()
方法,但这个方法通常不是用来直接模拟用户点击事件的。
更好的方法是定义一个可以在代码中调用的方法,该方法包含你想要在按钮点击时执行的代码。然后,你可以在按钮的事件监听器中调用这个方法,也可以直接从代码的其他部分调用它。
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ButtonClickSimulationExample extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Click Me!");
// 定义点击时执行的方法
EventHandler<ActionEvent> onButtonClick = event -> {
System.out.println("Button clicked!");
// 这里可以放置更多的代码
};
// 将事件监听器绑定到按钮
btn.setOnAction(onButtonClick);
// 直接从代码中“模拟”按钮点击
// 注意:这不是真正的模拟点击,而是直接调用了点击时要执行的代码
onButtonClick.handle(new ActionEvent());
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Button Click Simulation Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
可以通过设置button的disable属性来控制是否能点击按钮
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class DisableButtonExample extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button("点击我");
// 按钮点击事件
btn.setOnAction(event -> {
btn.setDisable(true);
btn.setText("按钮已禁用");
// 假设这里有一个异步操作,完成后重新启用按钮
new Thread(() -> {
try {
Thread.sleep(2000); // 模拟异步操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 重新启用按钮
btn.setDisable(false);
btn.setText("点击我");
}).start();
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("JavaFX Disable Button Example");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
最终代码
TCPServer.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class TCPServer {
private final int port; // 服务器监听端口号
private final ServerSocket serverSocket; //定义服务器套接字
public TCPServer() throws IOException {
Scanner scanner = new Scanner(System.in); // 创建一个Scanner对象来读取标准输入
System.out.println("请输入服务器监听的端口号:");
if (scanner.hasNextInt()) { // 检查是否有下一个输入项并且是一个整数
port = scanner.nextInt(); // 读取整数并赋值给port
} else {
System.out.println("输入错误,请输入一个有效的整数端口号。");
// 这里可以根据需要处理错误情况,比如使用默认值或者退出程序
port = 8080; // 例如,使用8080作为默认端口号
}
scanner.close(); // 关闭scanner对象
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, StandardCharsets.UTF_8), true);
}
private BufferedReader getReader(Socket socket) throws IOException {
//获得输入流缓冲区的地址
InputStream socketIn = socket.getInputStream();
return new BufferedReader(
new InputStreamReader(socketIn, StandardCharsets.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; //结束循环
}
//向输出流中输出一行字符串,远程客户端可以读取该字符串
pw.println("From服务器:" + msg);
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
finally {
try {
if(socket != null)
socket.close(); //关闭socket连接及相关的输入输出流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException {
TCPServer server = new TCPServer();
System.out.println("服务器将监听端口号: " + server.port);
server.Service();
}
}
TCPClient.java
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class TCPClient {
private final Socket socket; // 定义套接字
private final PrintWriter pw; // 定义字符输出流
private final BufferedReader br; // 定义字符输入流
public TCPClient(String ip, String port) throws IOException {
// 主动向服务器发起连接,实现TCP的三次握手过程
// 如果不成功,则抛出错误信息,其错误信息交由调用者处理
socket = new Socket(ip, Integer.parseInt(port));
// 得到网络输出字节流地址,并封装成网络输出字符流
// 设置最后一个参数为true,表示自动flush数据
OutputStream socketOut = socket.getOutputStream();
pw = new PrintWriter(new OutputStreamWriter(socketOut, StandardCharsets.UTF_8), true);
// 得到网络输入字节流地址,并封装成网络输入字符流
InputStream socketIn = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(socketIn, StandardCharsets.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;
}
// 实现close方法以关闭socket连接及相关的输入输出流
public void close() {
try {
if (pw != null) {
pw.close(); // 关闭PrintWriter会先flush再关闭底层流
}
if (br != null) {
br.close(); // 关闭BufferedReader
}
if (socket != null) {
socket.close(); // 关闭Socket连接
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
SimpleFx(窗口)
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.*;
import javafx.stage.Stage;
import java.io.IOException;
public class SimpleFx extends Application {
private TCPClient tcpClient;
private final Button btnCon = new Button("连接");
private final Button btnExit = new Button("退出");
private final Button btnSend = new Button("发送");
private final TextField IpAdd_input = new TextField();
private final TextField Port_input = new TextField();
private final TextArea OutputArea = new TextArea();
private final TextField InputField = new TextField();
public void start(Stage primaryStage) {
BorderPane mainPane = new BorderPane();
VBox mainVBox = new VBox();
HBox hBox = new HBox();
hBox.setSpacing(10);//各控件之间的间隔
//HBox面板中的内容距离四周的留空区域
hBox.setPadding(new Insets(20, 20, 10, 20));
hBox.getChildren().addAll(new Label("IP地址: "), IpAdd_input, new Label("端口: "), Port_input, btnCon);
hBox.setAlignment(Pos.TOP_CENTER);
//内容显示区域
VBox vBox = new VBox();
vBox.setSpacing(10);//各控件之间的间隔
//VBox面板中的内容距离四周的留空区域
vBox.setPadding(new Insets(10, 20, 10, 20));
vBox.getChildren().addAll(new Label("信息显示区:"), OutputArea, new Label("信息输入区"), InputField);
//设置显示信息区的文本区域可以纵向自动扩充范围
VBox.setVgrow(OutputArea, Priority.ALWAYS);
// 设置文本只读和自动换行
OutputArea.setEditable(false);
OutputArea.setStyle("-fx-wrap-text: true; /* 实际上是默认的 */ -fx-font-size: 14px;");
InputField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
btnSend.fire();
}
});
//底部按钮区域
HBox hBox2 = new HBox();
hBox2.setSpacing(10);
hBox2.setPadding(new Insets(10, 20, 10, 20));
// 设置按钮的交互效果
btnCon.setOnAction(event -> {
String ip = IpAdd_input.getText().trim();
String port = Port_input.getText().trim();
try {
//tcpClient不是局部变量,是本程序定义的一个TCPClient类型的成员变量
tcpClient = new TCPClient(ip, port);
//成功连接服务器,接收服务器发来的第一条欢迎信息
String firstMsg = tcpClient.receive();
OutputArea.appendText(firstMsg + "\n");
} catch (Exception e) {
OutputArea.appendText("服务器连接失败!" + e.getMessage() + "\n");
}
});
btnExit.setOnAction(event -> {
if (tcpClient != null){
//向服务器发送关闭连接的约定信息
tcpClient.send("bye");
tcpClient.close();
}
System.exit(0);
});
btnSend.setOnAction(event -> {
String sendMsg = InputField.getText();
tcpClient.send(sendMsg);//向服务器发送一串字符
InputField.clear();
OutputArea.appendText("客户端发送:" + sendMsg + "\n");
String receiveMsg = tcpClient.receive();//从服务器接收一行字符
OutputArea.appendText(receiveMsg + "\n");
});
hBox2.setAlignment(Pos.CENTER_RIGHT);
hBox2.getChildren().addAll(btnSend, btnExit);
mainVBox.getChildren().addAll(hBox, vBox, hBox2);
mainPane.setCenter(mainVBox);
VBox.setVgrow(vBox, Priority.ALWAYS);
Scene scene = new Scene(mainPane, 700, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}