上一篇博客我们介绍了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 {
socket = new Socket(ip, Integer.parseInt(port));
OutputStream socketOut = socket.getOutputStream();
pw = new PrintWriter(
new OutputStreamWriter(
socketOut, "utf-8"), true);
InputStream socketIn = socket.getInputStream();
br = new BufferedReader(
new InputStreamReader(socketIn, "utf-8"));
}
public void send(String msg) {
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.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 {
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));
top.setAlignment(Pos.CENTER);
mainPane.setTop(top);
VBox vBox = new VBox();
vBox.setSpacing(10);
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.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 = 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) {
String msgTemp = msg;
Platform.runLater(()->{
taDisplay.appendText( msgTemp + "\n");
});
}
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) {
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();
}
});
tfSend.setOnKeyPressed(event -> {
if(event.getCode() == KeyCode.ENTER){
event.consume();
if(event.isShiftDown()){
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);
}
tfSend.clear();
}
else {
taDisplay.appendText("Please 先连接服务器!!!!!\n");
tfSend.clear();
}
}
}
});
btnSave.setOnAction(event -> {
TextFileIO textFileIO = new TextFileIO();
textFileIO.append(
LocalDateTime.now().withNano(0) +"\n"+ taDisplay.getText());
});
btnOpen.setOnAction(event -> {
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);
System.out.println("服务器启动监听在 " + port + " 端口");
}
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream socketOut = socket.getOutputStream();
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 (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) {
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);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(socket != null)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException{
new TCPServer().Service();
}
}