目录
- 前置
- 案例1:TCP回显服务器&客户端(Echo)
- [TCP服务端代码 TcpServer.java](#TCP服务端代码 TcpServer.java)
- [TCP客户端代码 TcpClient.java](#TCP客户端代码 TcpClient.java)
- 运行步骤
- 案例2:多客户端支持(多线程版本TCP服务端)
- 结合代码理解TCP核心机制
- [1. 三次握手(建立连接)](#1. 三次握手(建立连接))
- [2. 四次挥手(断开连接)](#2. 四次挥手(断开连接))
- [3. TCP面向字节流 & 粘包问题](#3. TCP面向字节流 & 粘包问题)
- [4. IP地址在代码中的体现](#4. IP地址在代码中的体现)
- [5. 端口占用异常](#5. 端口占用异常)
- [6. 抓包验证TCP/IP(Wireshark)](#6. 抓包验证TCP/IP(Wireshark))
- 补充:UDP对比代码,凸显TCP面向连接特性
- [关键总结(代码 ↔ TCP/IP对应)](#关键总结(代码 ↔ TCP/IP对应))
TCP/IP 不是单一协议,是一整套协议簇。IP负责寻址路由(网络层),TCP负责可靠端到端传输(传输层)。
前置
- IP(网络层):解决「我这个包要发给哪台机器,怎么走路由」;
- TCP(传输层):解决「数据可靠、有序传输,流量控制,连接管理」;
- 应用层Java代码,我们只操作Socket,操作系统内核完成TCP/IP头部封装、分用。
- TCP核心特性:有连接、可靠传输、面向字节流、全双工。
注意:Java代码我们写在应用层;TCP、IP头部处理全部由操作系统内核完成,我们代码看不到TCP报文,但是代码行为会映射TCP协议行为。
案例1:TCP回显服务器&客户端(Echo)
功能:客户端输入字符串发给服务端,服务端原封不动返回。
流程:客户端
new Socket()触发TCP三次握手建立连接 ;双方通过输入输出流收发字节;调用close()触发四次挥手断开连接。
TCP服务端代码 TcpServer.java
java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP BIO 回显服务端
*/
public class TcpServer {
private ServerSocket serverSocket;
public TcpServer(int port) throws IOException {
// 绑定本机端口,开始监听
serverSocket = new ServerSocket(port);
System.out.println("TCP服务端启动,监听端口:" + port);
}
public void start() throws IOException {
// BIO单线程,一次只能处理1个客户端
while (true) {
// accept() 阻塞!等待客户端连接,三次握手完成之后,才返回Socket对象
Socket clientSocket = serverSocket.accept();
System.out.printf("客户端接入:%s:%d%n",
clientSocket.getInetAddress(),
clientSocket.getPort());
// 处理该客户端连接
handleClient(clientSocket);
}
}
private void handleClient(Socket clientSocket) {
// try‑with‑resource:自动关闭流
try (InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream();
Scanner scanner = new Scanner(is);
PrintWriter printWriter = new PrintWriter(os)) {
// 循环读取客户端发来的数据
while (scanner.hasNext()) {
// 读取客户端一行字符串
String req = scanner.next();
System.out.println("收到客户端消息:" + req);
// 业务逻辑:回显,原样返回
String resp = req;
// println 会追加换行符\n,作为应用层报文边界
printWriter.println(resp);
// ⚠️ 必须flush!缓冲区数据不会立刻发送,flush才会把数据交给TCP内核缓冲区
printWriter.flush();
}
// scanner.hasNext() 返回false:代表客户端调用close发送FIN报文,连接关闭
System.out.println("客户端正常断开连接");
} catch (IOException e) {
System.err.println("客户端异常断开:" + e.getMessage());
} finally {
try {
clientSocket.close(); // 关闭socket,发出FIN报文,开启四次挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
TcpServer server = new TcpServer(9000);
server.start();
}
}
TCP客户端代码 TcpClient.java
java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* TCP BIO 客户端
*/
public class TcpClient {
private Socket socket;
public TcpClient(String ip, int port) throws IOException {
// new Socket 这一步:发起TCP三次握手!和服务端建立连接,阻塞直到连接建立成功
socket = new Socket(ip, port);
System.out.println("客户端连接服务端成功");
}
public void start() {
try (InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
Scanner consoleScanner = new Scanner(System.in);
Scanner netScanner = new Scanner(is);
PrintWriter printWriter = new PrintWriter(os)) {
while (true) {
System.out.print("请输入发送给服务端的内容:");
String input = consoleScanner.next();
// 发送给服务端
printWriter.println(input);
printWriter.flush();
// 读取服务端回显响应
String resp = netScanner.next();
System.out.println("收到服务端返回:" + resp);
if ("exit".equals(input)) {
System.out.println("客户端准备关闭连接");
break;
}
}
} catch (IOException e) {
System.err.println("通信异常:" + e.getMessage());
} finally {
try {
socket.close(); // 客户端发送FIN,开始四次挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
// 127.0.0.1 本机回环IP,不走物理网卡,用于本地测试
TcpClient client = new TcpClient("127.0.0.1", 9000);
client.start();
}
}
运行步骤
- 先运行
TcpServer,控制台输出:TCP服务端启动,监听端口:9000 - 再运行
TcpClient,建立TCP连接(三次握手完成) - 客户端控制台输入文字,服务端回显;输入
exit关闭连接(触发四次挥手)
代码对应TCP/IP底层行为
new Socket("127.0.0.1",9000):客户端内核发出SYN报文,三次握手开始;握手全部完成,这个构造方法才返回。serverSocket.accept()阻塞,直到三次握手完成,返回Socket对象。printWriter.println + flush:数据从JVM缓冲区拷贝到操作系统TCP发送缓冲区;内核TCP协议栈加上TCP头部,IP协议栈加上IP头部,交给链路层封装以太网帧。socket.close():内核发送FIN报文,开启四次挥手断开TCP连接。scanner.hasNext()返回false:代表收到对端FIN报文,对方已经关闭输出。
案例2:多客户端支持(多线程版本TCP服务端)
上面单线程版本服务端同一时刻只能服务一个客户端;TCP本身支持多路连接,BIO下需要每个客户端分配一个线程。
java
public void start() throws IOException {
System.out.println("多线程TCP服务端启动,监听端口9000");
while (true) {
Socket clientSocket = serverSocket.accept();
// 每来一个客户端就开一个新线程处理
new Thread(() -> {
handleClient(clientSocket);
}).start();
}
}
原理:操作系统内核可以维护成千上万TCP连接;BIO问题:每个连接占用一个线程,高并发下线程太多,开销巨大;生产环境会使用NIO。
结合代码理解TCP核心机制
1. 三次握手(建立连接)
代码触发点:
Socket client = new Socket(ip,port)
- 客户端内核发送
SYN报文(同步报文段) - 服务端内核回复
SYN+ACK - 客户端回复
ACK;三次握手完成。
- 握手作用:确认双向收发能力,协商MSS最大报文段大小,初始化双方序列号。
Java应用层代码看不到SYN/SYN+ACK报文,全部操作系统内核完成。
2. 四次挥手(断开连接)
代码触发点:调用
socket.close()TCP是全双工,读、写通道可以分别关闭。
- 主动关闭方调用close → 内核发出
FIN报文,表示我不再发送数据。 - 对端回复
ACK确认FIN。 - 对端应用调用close → 内核发出自己的FIN。
- 本端回复ACK,连接释放。
TIME_WAIT状态
主动关闭连接一方,收到对方FIN、发出最后一个ACK之后,进入TIME_WAIT状态,保持2MSL时间。
目的:
- 保证最后一个ACK报文能够到达对方,如果ACK丢失,可以重发ACK;
- 网络中延迟滞留的旧TCP报文全部消失,防止旧连接报文干扰新连接。
在Windows cmd执行
netstat -ano可以观察TIME_WAIT状态连接。
3. TCP面向字节流 & 粘包问题
TCP没有报文边界,内核缓冲区是字节流。上面示例代码我们用
println()加换行符\n作为报文边界。
假设:
客户端连续发送两段消息:hello 和 world
内核TCP缓冲区中字节是连续的:hello\nworld\n
如果服务端一次性读全部字节,分不清哪一段是一个请求,这就是粘包。
解决粘包三种方案(应用层处理,TCP内核不管)
- 分隔符 :例如换行符
\n,上面代码示例;适合文本协议; - 定长包:每次固定长度读取;
- 包头携带数据长度:最常用;先4字节表示后续数据长度,再发送数据。
UDP不会粘包;UDP保留数据包边界。
4. IP地址在代码中的体现
127.0.0.1:本地回环IP,数据包直接本机内核流转,不经过网卡硬件。socket.getInetAddress()获取对端IP地址(IP协议,网络层)。- 五元组:源IP、源端口、目的IP、目的端口、协议(TCP=6),标识一条TCP连接。
5. 端口占用异常
启动服务端如果抛出
java.net.BindException: Address already in use: bind
代表该端口被别的进程占用。
Windows排查命令:
cmd
netstat -ano | findstr 9000
得到PID,任务管理器结束对应进程。
6. 抓包验证TCP/IP(Wireshark)
- 打开Wireshark,选择Loopback回环接口;
- 运行上面Java程序;
- 过滤器输入
tcp.port == 9000
可以肉眼观察:SYN、SYN‑ACK、ACK握手报文;数据报文;FIN挥手报文,看TCP头部、IP头部。
补充:UDP对比代码,凸显TCP面向连接特性
UDP没有三次握手,
DatagramSocket.send()直接发数据包,不需要建立连接。
java
// UDP简单发送片段
DatagramSocket socket = new DatagramSocket();
byte[] data = "hello udp".getBytes();
InetSocketAddress address = new InetSocketAddress("127.0.0.1",9001);
DatagramPacket packet = new DatagramPacket(data,data.length,address);
socket.send(packet);
UDP代码执行send就直接发包,没有任何握手过程;丢包不会重传。
关键总结(代码 ↔ TCP/IP对应)
| Java代码行为 | 底层TCP/IP发生的事情 |
|---|---|
new Socket(ip,port) |
内核发起TCP三次握手 |
ServerSocket.accept() |
阻塞,等待三次握手完成返回连接 |
printWriter.flush() |
数据拷贝到内核TCP发送缓冲区,封装TCP头+IP头 |
| socket.getInputStream().read | 从内核TCP接收缓冲区读取字节 |
socket.close() |
内核发送FIN报文,四次挥手释放连接 |
小结:Java网络编程只操作应用层Socket API;TCP、IP头部封装、确认应答、超时重传、滑动窗口全部由操作系统内核完成,应用层代码感知不到这些细节。