---随笔--Java实现TCP通信(双端通信接收与发送)
- 引言
- 1. 什么是TCP通信
- 2. 服务器与客户端核心代码
- 2.1 服务器ServerSocket端核心代码
- 2.2 用户Socket端核心代码
- 2.3 小贴士之关于try-with-resources自动关闭资源的使用
- 3. 具体服务器端实现
- 4. 具体客户端实现
- 5. 运行效果图
- 6. 总结
引言
设计一个基于最基本Socket的P2P“聊天软件”(文本互传),要求在同一个网关内可以通过任意常用端口建立会话并进行socket通讯、双向文本收发。
1. 什么是TCP通信
TCP(Transmission Control Protocol),也称传输控制协议
,是一种在计算机网络中常用的协议,用于在两个设备之间建立可靠的连接并进行数据交换。它是一种面向连接的协议,意味着通信的两端在交换数据之前必须先建立连接
。TCP提供了许多功能,使得数据可以在网络上可靠地传输,包括数据分段、重传机制、流量控制和拥塞控制等。
具体来说,TCP通信的过程通常包括以下几个步骤:
-
建立连接(三次握手):通信的两端(客户端和服务器端)首先需要通过三次握手来建立连接。在这个过程中,客户端向服务器发送一个连接请求报文(SYN),服务器收到后回复一个确认报文(SYN-ACK),并确认客户端的连接请求,最后客户端再回复一个确认报文(ACK),确认服务器的确认。这样,连接就建立起来了。
-
数据传输:连接建立之后,通信双方就可以开始传输数据。数据会被分成小的数据段,并通过网络传输到接收端。TCP会对数据进行编号,以保证数据的顺序不会被打乱,同时会对数据进行检验,以确保数据的完整性。
-
连接终止(四次挥手):当数据传输完成后,通信的两端需要进行连接的关闭。这个过程包括四次挥手:首先,客户端发送一个连接释放报文(FIN),服务器接收到后发送确认报文(ACK),但仍然允许数据传输;然后,服务器发送一个连接释放报文(FIN),客户端收到后发送确认报文(ACK),进入 TIME-WAIT 状态。最后,客户端发送最后一个确认报文(ACK),完成连接的关闭。
关于更详细的三次握手和四次挥手可以参考我的以下文章:–随笔–带你轻松理解TCP中的三次握手,–随笔–带你轻松理解TCP中的四次挥手
2. 服务器与客户端核心代码
自己做了个图,方便大家理解下,看了图再往下看就能很容易理解了:
2.1 服务器ServerSocket端核心代码
//绑定监听端口
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server is running, listening on port " + port);
while (true) {
// 等待并监听客户端的连接请求
Socket clientSocket = serverSocket.accept();
// 为客户端创建一个线程处理请求
new Thread(new ClientHandler(clientSocket)).start();
}
这段代码片段建立了一个服务器,它在指定的端口上监听传入的客户端连接。当客户端连接时,它创建一个新的线程来处理该客户端的请求,从而允许并发的客户端-服务器交互。这段代码的主要功能是创建一个能够处理多个客户端连接的多线程服务器。
以上代码主要进行了以下操作:
-
初始化ServerSocket:
ServerSocket serverSocket = new ServerSocket(port);
在这里,创建了一个名为serverSocket的ServerSocket对象,并将其绑定到特定的端口(port在main方法中预先定义为1234)。这个ServerSocket负责在指定端口上监听传入的客户端连接请求。 -
服务器运行反馈:
System.out.println("Server is running, listening on port " + port);
这行代码简单地向控制台打印一条消息,指示服务器正在运行并在特定端口上进行监听。 -
客户端连接处理循环:
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(new ClientHandler(clientSocket)).start();
}
在代码块中,while循环无限地运行,不断等待并处理客户端连接。
serverSocket.accept()
: 这是一个阻塞方法,它等待客户端连接到服务器。当检测到客户端连接时,它会返回代表客户端的Socket对象。new Thread(new ClientHandler(clientSocket)).start()
: 这一行代码创建一个新的Thread,并将一个ClientHandler实例设置为其目标。ClientHandler负责管理与连接的客户端的交互。当线程启动时,ClientHandler的run方法将独立执行,允许通过多线程同时为多个客户端提供服务。
2.2 用户Socket端核心代码
//创建指定主机和端口的Socket
Socket socket = new Socket(host, port);
//获取Socket的输出流和输入流
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取系统标准输入流
BufferedReader sysReader = new BufferedReader(new InputStreamReader(System.in));
这段代码用于创建一个 socket 连接到指定的主机和端口,并为与服务器的通信建立输入和输出流。此外,它创建一个读取器,用于从标准输入流中捕获输入,允许用户与客户端程序进行交互。该代码的主要功能是为客户端设置通信基础设施,使其能够发送和接收与服务器的消息。
这段代码是用于创建一个客户端,通过 Socket 连接到服务器。具体步骤如下:
- Socket socket = new Socket(host, port):
创建一个新的 Socket 对象,指定连接的主机和端口
。这个 Socket 代表了客户端和服务器之间通信的端点。 - PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)
初始化一个 PrintWriter 对象,用于向 Socket 的输出流写入数据
。PrintWriter 类提供了便捷的方法,用于向流中写入各种数据类型。 - BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))
通过初始化 BufferedReader 对象,用于从 Socket 的输入流中读取数据
。BufferedReader 类允许从字符输入流中高效地读取字符。 - BufferedReader sysReader = new BufferedReader(new InputStreamReader(System.in))
创建一个 BufferedReader 对象,用于从标准输入流(键盘)中读取输入。这将用于捕获用户输入,并将其发送到服务器
。
2.3 小贴士之关于try-with-resources自动关闭资源的使用
代码中运用到许多try-with-resources
的语法,try-with-resources
是 Java 7 中引入的一个语言特性,用于自动关闭资源。在 Java 中,程序常常需要操作文件、数据库连接、网络连接等资源,这些资源需要在不再需要时及时关闭,以释放系统资源并避免资源泄漏。传统的做法是在 finally 块中关闭资源,但这样的代码容易出错,而且代码量较多。
try-with-resources 语句简化了资源管理的代码,确保在代码块结束后自动关闭资源,无需手动编写 finally 块。其语法结构如下:
try (ResourceType1 resource1 = new ResourceType1();
ResourceType2 resource2 = new ResourceType2();
// 可以有多个资源声明,用分号隔开
) {
// 使用资源的代码块
// 资源会在代码块结束后自动关闭
} catch (Exception e) {
// 异常处理代码块
}
即在try后使用小括号,将资源写进小括号中再接大括号,即 try( Resources ){ your code }。
需要注意的是,在 try 关键字后面的括号内声明需要管理的资源,这些资源必须实现 java.lang.AutoCloseable 接口或其子接口(例如 java.io.Closeable)
。在代码块结束后,Java 会自动调用资源的 close() 方法关闭资源,即使代码块中出现异常也会关闭资源。如果资源的 close() 方法抛出异常,这些异常会被抑制,并且可以通过 Throwable.getSuppressed() 方法获取。
3. 具体服务器端实现
MyServer.class
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {
public static void main(String[] args) {
int port = 1234;
try {
//绑定监听端口
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server is running, listening on port " + port);
while (true) {
// 等待并监听客户端的连接请求
Socket clientSocket = serverSocket.accept();
// 为客户端创建一个线程处理请求
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientHandler implements Runnable {
private Socket clientSocket;
private BufferedReader reader;
private PrintWriter writer;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
try {
this.reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
this.writer = new PrintWriter(clientSocket.getOutputStream(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
// 向客户端发送欢迎消息
writer.println("Welcome to our Chat! Type anything pressing enter to send message.(Type 'exit' to end the chat)");
BufferedReader sysReader = new BufferedReader(new InputStreamReader(System.in));
String message;
while (true) {
if (reader.ready()) { // 检查是否有消息从客户端发来
message = reader.readLine();
//监听客户端若输入exit则关闭客户端连接
if ("exit".equalsIgnoreCase(message)) {
System.out.println("Client exit, System shutdown...");
//立即终止JVM,自动释放资源
System.exit(0);
}
System.out.println("Client: " + message);
}
if (sysReader.ready()) { // 检查是否有自定义消息要发送给客户端
String messageToClient = sysReader.readLine();
writer.println(messageToClient);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (clientSocket != null) {
clientSocket.close(); // 关闭Socket
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4. 具体客户端实现
MyClient.class
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class MyClient {
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 1234;
// try-with-resources 语法块自动关闭资源
try(
//创建指定主机和端口的Socket
Socket socket = new Socket(host, port);
//获取Socket的输出流和输入流
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取系统标准输入流
BufferedReader sysReader = new BufferedReader(new InputStreamReader(System.in));)
{
new Thread(() -> {
try {
String receivedMessage;
while (true) {
//立即返回,而不是通过readLine()等待,避免阻塞
if (reader.ready()) {
receivedMessage = reader.readLine();
System.out.println("Server: " + receivedMessage);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
String messageToServer;
while (true) {
if (sysReader.ready()) {
messageToServer = sysReader.readLine();
writer.println(messageToServer);
//exit命令退出系统
if ("exit".equalsIgnoreCase(messageToServer)){
System.out.println("You have exited the chat");
System.exit(0);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. 运行效果图
服务器运行后接受到客户端发来的消息并回发内容
客户端向服务器发送消息并获取服务器返回的消息
6. 总结
一开始想着用Java简单实现下TCP并不难,但实际动手后发现需要注意的点还挺多的,要考虑到双方数据的发送和获取,资源的关闭,对话的终结,真正做起来还是花了点时间,但至少强化了记忆也学习到了一些新方法,内心还是挺快乐的,总之还是要多动手多实践,相信你也可以做到一次编写一次过的,加油!