一、TCP 协议基础与 Java 核心类
1.1 TCP 协议核心特性
TCP 协议之所以成为可靠通信的首选,源于其三大核心机制:
面向连接
- 通信前需通过"三次握手"建立可靠连接:
- 客户端发送SYN报文(同步序列号)发起连接请求
- 服务器回复SYN+ACK报文(确认客户端的SYN并提供自己的序列号)
- 客户端回复ACK报文确认服务器的SYN
- 三次握手确保双方通信通道通畅,防止历史连接请求导致错误
- 断开连接时采用"四次挥手"机制确保数据完整传输
可靠传输
- 序列号机制:每个字节都有唯一序列号,接收方可以按序重组数据
- 确认应答(ACK):接收方必须对收到的数据发送确认
- 重传机制:发送方启动定时器,超时未收到ACK则重发数据包
- 流量控制:通过滑动窗口动态调整发送速率,避免接收方缓冲区溢出
- 拥塞控制:采用慢启动、拥塞避免、快速重传等算法适应网络状况
- 通过这些机制确保数据不丢失、不重复、按序到达
字节流服务
- TCP将数据视为连续的字节流,不保留应用层消息边界
- 这是"粘包"问题的根源:多个应用层消息可能被合并为一个TCP包传输
- 解决方案:应用层需要定义消息分割规则,常见方法包括:
- 固定长度分隔
- 特殊字符分隔(如换行符)
- 消息头+消息体(头部包含消息长度)
1.2 Java TCP 编程核心类
Java通过java.net
包提供TCP编程核心API,核心类仅有两个,职责明确:
Socket类(客户端)
- 代表客户端TCP连接端点
- 关键方法和功能:
Socket(String host, int port)
:创建并连接到指定主机和端口getInputStream()
:获取输入流读取服务器数据getOutputStream()
:获取输出流向服务器发送数据setSoTimeout(int timeout)
:设置读取超时时间(毫秒)close()
:关闭连接释放资源
- 典型使用场景:
// 客户端示例
try (Socket socket = new Socket("127.0.0.1", 8080)) {
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
// 读写数据...
} catch (IOException e) {
e.printStackTrace();
}
ServerSocket类(服务器端)
- 代表服务器端监听套接字
- 关键方法和功能:
ServerSocket(int port)
:绑定到指定端口开始监听accept()
:阻塞等待客户端连接,返回Socket对象setSoTimeout(int timeout)
:设置accept超时时间close()
:停止监听释放资源
- 典型使用场景:
// 服务器端示例
try (ServerSocket server = new ServerSocket(8080)) {
while (true) {
Socket client = server.accept(); // 等待客户端连接
new Thread(() -> {
try (InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream()) {
// 处理客户端请求...
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
重要说明
- Socket是"双向通道":既可以通过输出流发送数据,也可以通过输入流接收数据
- ServerSocket仅负责"开门":不直接处理数据,真正的通信由accept()返回的Socket完成
- 资源管理:必须使用try-with-resources或finally块确保关闭连接
- 多线程处理:服务器通常需要为每个客户端连接创建新线程处理,避免阻塞其他连接
二、Java TCP 编程核心知识点
Java TCP 编程分为客户端和服务器端两大模块,两者流程不同但相互依赖
2.1 客户端流程
客户端的核心目标是实现 "连接服务器 → 交换数据 → 关闭连接" 的标准TCP通信流程。该流程适用于大多数客户端开发场景,如即时通讯、文件传输、远程控制等。以下是详细步骤说明:
步骤 1:创建 Socket 并建立连接
- 实现方式:通过
Socket
类构造方法Socket(String host, int port)
指定服务器IP和端口 - 连接成功:TCP三次握手完成,进入可通信状态
- 连接失败:可能由于以下原因抛出
IOException
- 服务器未启动(
Connection refused
) - 网络不可达(
No route to host
) - 端口错误(
Port unreachable
) - 防火墙拦截
- 服务器未启动(
- 调试建议:先用
telnet
或ping
测试网络连通性
步骤 2:获取 IO 流并交换数据
- 底层机制:TCP基于字节流传输,需处理以下关键点:
- 流的获取:
InputStream in = socket.getInputStream(); // 接收数据流 OutputStream out = socket.getOutputStream(); // 发送数据流
- 流包装(推荐方案):
- 字符流包装(需显式指定编码,如UTF-8):
BufferedReader reader = new BufferedReader( new InputStreamReader(in, "UTF-8")); PrintWriter writer = new PrintWriter( new OutputStreamWriter(out, "UTF-8"), true); // autoFlush
- 二进制数据包装(文件传输场景):
BufferedInputStream bin = new BufferedInputStream(in); BufferedOutputStream bout = new BufferedOutputStream(out);
- 字符流包装(需显式指定编码,如UTF-8):
- 流的获取:
- 通信模式:
- 短连接:单次请求-响应后立即关闭(如HTTP)
- 长连接:保持连接持续通信(如WebSocket)
步骤 3:关闭资源(关键!)
- 关闭顺序:必须遵循 "先打开的后关闭" 原则:
- 关闭所有IO流
- 最后关闭Socket
- 推荐方案:
- try-with-resources(Java 7+):
try (Socket socket = new Socket(ip, port); InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream()) { // 通信代码... } // 自动调用close()
- 传统方式(需finally块):
try { // 通信代码... } finally { if (out != null) out.close(); if (in != null) in.close(); if (socket != null) socket.close(); }
- try-with-resources(Java 7+):
- 资源泄漏风险:
- 未关闭Socket会导致端口占用(TIME_WAIT状态)
- 未关闭流可能导致内存泄漏
客户端示例代码(增强版)
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 增强版TCP客户端示例:
* 1. 使用try-with-resources确保资源释放
* 2. 支持控制台交互式通信
* 3. 添加心跳检测机制(每5秒发送PING)
*/
public class EnhancedTcpClient {
private static final int HEARTBEAT_INTERVAL = 5000; // 心跳间隔5秒
public static void main(String[] args) {
final String serverIp = "127.0.0.1"; // 本地回环地址
final int serverPort = 8888; // 建议使用1024-49151的注册端口
try (Socket socket = new Socket(serverIp, serverPort);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(),
StandardCharsets.UTF_8), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(),
StandardCharsets.UTF_8));
BufferedReader consoleIn = new BufferedReader(
new InputStreamReader(System.in,
StandardCharsets.UTF_8))) {
System.out.println("已连接服务器 " + socket.getRemoteSocketAddress());
// 启动心跳线程
Thread heartbeatThread = new Thread(() -> {
try {
while (!socket.isClosed()) {
out.println("PING");
Thread.sleep(HEARTBEAT_INTERVAL);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
heartbeatThread.setDaemon(true);
heartbeatThread.start();
// 主通信循环
System.out.println("输入消息发送(输入exit退出):");
String userInput;
while ((userInput = consoleIn.readLine()) != null) {
if ("exit".equalsIgnoreCase(userInput.trim())) {
out.println("CLIENT_EXIT"); // 发送退出指令
break;
}
// 发送数据并获取响应
out.println(userInput);
String response = in.readLine();
System.out.println("[" + System.currentTimeMillis() + "] 服务器响应: " + response);
}
} catch (IOException e) {
System.err.println("通信异常: " + e.getClass().getSimpleName() +
": " + e.getMessage());
e.printStackTrace();
} finally {
System.out.println("客户端已终止");
}
}
}
关键注意事项
- 编码问题:必须统一客户端和服务端的字符编码(推荐UTF-8)
- 异常处理:需要捕获
SocketTimeoutException
(连接超时)、UnknownHostException
(域名解析失败)等特定异常 - 性能优化:
- 使用缓冲流(BufferedReader/PrintWriter)减少IO操作次数
- 大数据传输时采用分块(chunked)方式
- 网络安全:生产环境应使用SSL/TLS加密(如SSLSocket)
2.2 服务器端流程
服务器端程序的核心目标是实现"监听端口 → 接收客户端连接 → 处理通信 → 关闭资源"的完整流程。其中"处理通信"环节需要结合多线程技术,否则单线程服务器只能串行处理一个客户端,无法满足实际应用需求。
详细步骤说明
步骤 1:创建 ServerSocket 并绑定端口
- 使用
ServerSocket
类的构造方法绑定指定端口- 示例:
ServerSocket serverSocket = new ServerSocket(8888);
- 端口号范围应在 1024-65535 之间(0-1023 为系统保留端口)
- 示例:
- 端口冲突处理
- 如果端口已被占用(如 8888 端口被其他程序使用),会抛出
BindException
- 解决方案:更换端口号或终止占用端口的程序
- 如果端口已被占用(如 8888 端口被其他程序使用),会抛出
步骤 2:阻塞监听客户端连接
- 调用
ServerSocket.accept()
方法- 该方法会阻塞当前线程,直到有客户端连接请求到达
- 返回一个与该客户端对应的
Socket
对象
- 连接特性
- 每个客户端连接都会创建一个独立的
Socket
对象 - 服务器端通过该
Socket
与特定客户端进行数据交换
- 每个客户端连接都会创建一个独立的
步骤 3:处理客户端通信(多线程实现关键)
- 单线程模式的局限性
- 服务器在
accept()
后直接处理当前客户端的通信 - 在此期间无法接收其他客户端的连接请求
- 服务器在
- 多线程解决方案
- 每接收一个客户端连接,就创建一个新线程处理该客户端的 IO 操作
- 主线程继续监听新的客户端连接
- 线程池优化
- 直接创建线程会导致性能开销
- 推荐使用线程池管理客户端处理线程
- 优点:控制并发数、复用线程资源、避免频繁创建销毁线程
步骤 4:关闭资源
- 关闭顺序
- 先关闭所有客户端
Socket
连接 - 最后关闭
ServerSocket
- 先关闭所有客户端
- 资源泄漏风险
- 必须确保所有
Socket
和ServerSocket
都被正确关闭 - 推荐使用 try-with-resources 语句自动关闭
- 必须确保所有
服务器端基础版(单线程演示)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 单线程服务器演示
* 特点:只能处理一个客户端,处理完后才能接收下一个
* 实际应用中不应使用此模式
*/
public class TcpSingleThreadServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听端口
// try-with-resources自动关闭ServerSocket
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,监听端口:" + port + "(单线程,仅支持1个客户端)");
// 步骤2:阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
// 步骤3:处理客户端通信(单线程阻塞在此处)
handleClientCommunication(clientSocket);
} catch (IOException e) {
System.err.println("服务器启动/通信异常:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 处理单个客户端的通信逻辑
* @param clientSocket 客户端Socket对象
*/
private static void handleClientCommunication(Socket clientSocket) {
// try-with-resources自动关闭客户端Socket、输入流、输出流
try (
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream(), "UTF-8")
);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"),
true
)
) {
String clientMsg;
// 循环读取客户端消息(客户端关闭则in.readLine()返回null)
while ((clientMsg = in.readLine()) != null) {
System.out.println("客户端[" + clientSocket.getPort() + "]发送:" + clientMsg);
if ("exit".equals(clientMsg)) {
out.println("已收到退出请求,断开连接");
break; // 退出循环,关闭连接
}
// 向客户端发送响应
out.println("已收到:" + clientMsg);
}
System.out.println("客户端[" + clientSocket.getPort() + "]断开连接");
} catch (IOException e) {
System.err.println("客户端[" + clientSocket.getPort() + "]通信异常:" + e.getMessage());
e.printStackTrace();
}
}
}
服务器端进阶版(多线程+线程池,生产级实现)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多线程服务器(线程池实现)
* 特点:支持并发处理多个客户端连接
* 适用于实际生产环境
*/
public class TcpThreadPoolServer {
// 线程池配置:固定大小线程池,最大并发处理5个客户端
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,监听端口:" + port + "(支持5个并发客户端)");
// 主线程循环监听客户端连接(无限循环,除非手动关闭服务器)
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞等待连接
System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());
// 将客户端通信任务提交给线程池处理
THREAD_POOL.submit(() -> handleClientCommunication(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器异常:" + e.getMessage());
e.printStackTrace();
} finally {
// 关闭服务器时关闭线程池(不再接收新任务,等待已提交任务完成)
THREAD_POOL.shutdown();
System.out.println("服务器线程池已关闭");
}
}
/**
* 客户端通信处理方法(与单线程版一致)
* @param clientSocket 客户端Socket对象
*/
private static void handleClientCommunication(Socket clientSocket) {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true)
) {
String clientMsg;
while ((clientMsg = in.readLine()) != null) {
System.out.println("客户端[" + clientSocket.getPort() + "]:" + clientMsg);
if ("exit".equals(clientMsg)) {
out.println("已断开连接");
break;
}
out.println("服务器响应:" + clientMsg);
}
System.out.println("客户端[" + clientSocket.getPort() + "]断开");
} catch (IOException e) {
System.err.println("客户端[" + clientSocket.getPort() + "]异常:" + e.getMessage());
}
}
}
生产环境扩展建议
-
线程池优化:
- 根据服务器硬件配置调整线程池大小
- 考虑使用
ThreadPoolExecutor
自定义线程池参数
-
异常处理:
- 添加更完善的异常处理机制
- 记录详细的错误日志
-
性能监控:
- 添加连接数统计功能
- 实现服务器负载监控
-
安全考虑:
- 添加客户端认证机制
- 实现通信数据加密
-
资源管理:
- 设置连接超时时间
- 实现空闲连接自动回收
2.3 关键扩展知识点
2.3.1 数据粘包问题与解决方案
问题详解
TCP协议作为面向字节流的传输层协议,其核心特性是不保证数据边界。在网络传输过程中,发送方的多个数据包可能被合并传输,接收方也可能分批接收。例如:
- 客户端连续发送两条消息:"Hello"和"World"
- 服务器可能一次性收到"HelloWorld"(粘包)
- 也可能分两次收到"He"和"lloWorld"(半包)
产生原因深度分析
-
发送方机制:
- Nagle算法优化:默认启用,会缓冲小于MSS(最大报文段长度)的小数据包,等待更多数据或超时(200ms)再发送
- 内核缓冲区合并:当应用层快速连续调用write()时,内核可能合并多个小数据包
-
接收方机制:
- 滑动窗口机制:接收缓冲区未及时读取时,多个数据包会在缓冲区堆积
- 网络延迟抖动:不同数据包可能通过不同路由到达,导致接收顺序异常
解决方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
定长消息 | 实现简单 | 空间浪费 | 固定格式协议(如金融报文) |
分隔符 | 灵活可变长 | 需转义处理 | 文本协议(如Redis) |
消息头+体 | 高效精准 | 复杂度略高 | 主流二进制协议 |
消息头+消息体实现进阶
-
协议设计优化:
// 协议格式:4字节魔数(0xCAFEBABE) + 4字节长度 + N字节消息体 dos.writeInt(0xCAFEBABE); // 协议标识 dos.writeInt(msgBytes.length); dos.write(msgBytes);
-
异常处理增强:
try { int magic = dis.readInt(); if(magic != 0xCAFEBABE) throw new ProtocolException(); int length = dis.readInt(); if(length > MAX_LENGTH) throw new OverflowException(); // ...读取消息体 } catch(EOFException e) { // 处理连接中断 }
2.3.2 Java NIO与Selector(高并发优化)
性能对比数据
模型 | 线程数 | 内存占用 | QPS(测试值) |
---|---|---|---|
BIO | 1:1 | 1MB/线程 | 2,000 |
NIO | 1:N | 50KB/连接 | 50,000 |
核心组件详解
-
Channel类型:
- ServerSocketChannel:监听TCP连接
- SocketChannel:客户端连接通道
- DatagramChannel:UDP通道
- FileChannel:文件IO通道
-
Buffer工作机制:
ByteBuffer buf = ByteBuffer.allocateDirect(1024); // 堆外内存 buf.put(data); // position移动 buf.flip(); // limit=position, position=0 channel.write(buf); buf.clear(); // position=0, limit=capacity
-
Selector事件类型:
- OP_ACCEPT:服务端接收连接
- OP_CONNECT:客户端连接完成
- OP_READ:数据可读
- OP_WRITE:通道可写
生产级实现建议
-
多Reactor模式:
// 主Reactor处理accept selector1.register(serverChannel, SelectionKey.OP_ACCEPT); // 子Reactor线程池处理IO Selector[] workers = new Selector[Runtime.getRuntime().availableProcessors()];
-
内存管理优化:
// 使用对象池复用ByteBuffer BufferPool pool = new BufferPool(1024, 100); ByteBuffer buf = pool.borrowBuffer(); try { // ...使用buffer } finally { pool.returnBuffer(buf); }
-
Netty对比优势:
- 零拷贝支持(FileRegion)
- 内存泄漏检测
- 完善的编解码框架
- 流量整形(TrafficShaping)
完整示例:NIO文件传输
FileChannel fileChannel = FileChannel.open(Paths.get("data.txt"));
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
// 零拷贝传输
long transferSize = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("传输字节数:" + transferSize);
三、Java TCP 编程关键注意事项
3.1 端口相关注意事项
端口范围与权限
-
端口号范围:0~65535(16位整型)
- 0~1023:知名端口(Well-Known Ports),由IANA分配给系统服务,普通用户程序无法直接绑定(需root或管理员权限)
- 常见示例:80(HTTP)、443(HTTPS)、22(SSH)、21(FTP)、53(DNS)
- 1024~49151:注册端口(Registered Ports),可自由使用但建议避免与已注册服务冲突
- 49152~65535:动态/私有端口(Ephemeral Ports),临时连接使用
- 0~1023:知名端口(Well-Known Ports),由IANA分配给系统服务,普通用户程序无法直接绑定(需root或管理员权限)
-
最佳实践:
- 开发测试建议使用1024~49151范围内端口
- 生产环境端口应记录在《服务部署手册》中
- 通过Spring的
@Value
或配置文件灵活配置端口:# application.properties server.port=8080
端口被占用问题
-
检测命令:
# Windows netstat -ano | findstr :8080 taskkill /PID <进程ID> /F # Linux/Mac lsof -i :8080 kill -9 <进程ID>
-
编程解决方案:
- 自动端口递增算法(示例):
public int findAvailablePort(int startPort) { while (startPort < 65535) { try (ServerSocket ignored = new ServerSocket(startPort)) { return startPort; } catch (IOException e) { startPort++; } } throw new RuntimeException("No available port found"); }
- 自动端口递增算法(示例):
3.2 异常处理与资源释放
必须捕获的异常类型
异常类型 | 触发场景 | 处理建议 |
---|---|---|
BindException | 端口被占用 | 记录日志并尝试备用端口 |
ConnectException | 连接拒绝 | 检查服务状态和网络配置 |
SocketTimeoutException | 超时设置生效 | 增加超时阈值或优化网络 |
IOException | 流读写异常 | 关闭连接并重建会话 |
资源释放最佳实践
// 传统方式(易遗漏)
try {
Socket socket = new Socket(...);
// 业务代码
socket.close(); // 可能被异常跳过
} catch (...) {...}
// 推荐方式(Java 7+)
try (ServerSocket server = new ServerSocket(port);
Socket client = server.accept();
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream()) {
// 业务逻辑
} // 自动调用close()
3.3 超时设置(避免无限阻塞)
连接超时设置
Socket socket = new Socket();
// 设置DNS解析超时(Java 8+)
socket.setSoTimeout(5000);
// 设置TCP握手超时
socket.connect(
new InetSocketAddress("example.com", 80),
3000 // 3秒超时
);
读写超时优化
// 设置SO_TIMEOUT(影响所有IO操作)
socket.setSoTimeout(10000); // 10秒
// 针对不同操作的超时控制
BufferedReader reader = new BufferedReader(...);
long start = System.currentTimeMillis();
while ((line = reader.readLine()) != null) {
if (System.currentTimeMillis() - start > 5000) {
throw new SocketTimeoutException("Read timeout");
}
// 处理数据
}
3.4 半关闭状态与shutdown方法
半关闭工作流程
-
客户端发送完数据后:
socket.shutdownOutput(); // 发送FIN包 // 仍可接收服务器响应
-
服务器检测到EOF:
while ((bytesRead = input.read(buffer)) != -1) { // 处理数据 } // 收到FIN后发送响应
-
完全关闭连接:
socket.close(); // 发送RST包
应用场景对比
场景 | 关闭方式 | 优点 |
---|---|---|
HTTP请求 | 全关闭 | 简单高效 |
文件传输 | 半关闭 | 确认接收结果 |
视频流 | 保持连接 | 减少握手开销 |
3.5 编码一致性(避免乱码)
编码处理规范
// 错误示范(使用平台默认编码)
new InputStreamReader(socket.getInputStream());
// 正确做法(显式指定UTF-8)
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8
);
// 写入时同样处理
new OutputStreamWriter(
socket.getOutputStream(),
StandardCharsets.UTF_8
);
BOM头处理
// 检测UTF-8 BOM头
PushbackInputStream pis = new PushbackInputStream(in, 3);
byte[] bom = new byte[3];
if (pis.read(bom) == 3
&& bom[0] == (byte)0xEF
&& bom[1] == (byte)0xBB
&& bom[2] == (byte)0xBF) {
// 是BOM头
} else {
pis.unread(bom); // 非BOM头回退
}
3.6 多线程安全与线程池参数
线程安全实践
// 不安全示例
public class Counter {
private int count;
public void increment() { count++; } // 竞态条件
}
// 安全解决方案
public class SafeCounter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
}
线程池配置公式
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maxPoolSize = corePoolSize * 2;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
unit,
workQueue,
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
监控指标
// 获取线程池状态
executor.getActiveCount(); // 活动线程数
executor.getQueue().size(); // 队列积压
executor.getCompletedTaskCount(); // 已完成任务
四、TCP 文件传输(客户端→服务器)
4.1 客户端实现(文件发送端)
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 文件传输客户端实现
* 功能:向指定服务器发送文件并接收响应
*/
public class FileClient {
public static void main(String[] args) {
// 服务器配置
String serverIp = "127.0.0.1"; // 服务器IP地址
int serverPort = 9999; // 服务器端口
// 待传输文件配置
String filePath = "D:/test.jpg"; // 待发送文件路径(可替换为实际文件路径)
try (
// 自动关闭资源:Socket连接、文件输入流、数据输出流
Socket socket = new Socket();
FileInputStream fis = new FileInputStream(filePath);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream())
) {
// 连接设置
socket.connect(new InetSocketAddress(serverIp, serverPort), 5000); // 5秒连接超时
socket.setSoTimeout(5000); // 5秒读超时(接收服务器响应)
// 1. 发送文件名(消息头:4字节长度 + 消息体:文件名字节)
File file = new File(filePath);
String fileName = file.getName();
byte[] fileNameBytes = fileName.getBytes("UTF-8");
dos.writeInt(fileNameBytes.length); // 写入文件名长度
dos.write(fileNameBytes); // 写入文件名内容
// 2. 发送文件大小(8字节long)
long fileSize = file.length();
dos.writeLong(fileSize);
// 3. 发送文件内容(分块传输)
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区(可根据网络情况调整)
int readLen;
long sentSize = 0;
System.out.println("开始发送文件:" + fileName + "(大小:" + fileSize + "字节)");
// 分块读取文件并发送
while ((readLen = fis.read(buffer)) != -1) {
dos.write(buffer, 0, readLen);
sentSize += readLen;
// 打印传输进度(每发送一个块更新一次)
System.out.printf("发送进度:%.2f%%\r", (sentSize * 100.0) / fileSize);
}
dos.flush(); // 确保所有数据已发送
System.out.println("\n文件发送完成");
// 4. 接收服务器响应
DataInputStream dis = new DataInputStream(socket.getInputStream());
String response = dis.readUTF();
System.out.println("服务器回复:" + response);
} catch (IOException e) {
System.err.println("文件传输异常:" + e.getMessage());
e.printStackTrace();
}
}
}
4.2 服务器实现(文件接收端)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 文件传输服务器实现
* 功能:接收客户端发送的文件并保存到指定目录
*/
public class FileServer {
// 线程池配置(支持并发处理多个客户端连接)
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(3);
// 文件保存目录配置
private static final String SAVE_DIR = "D:/tcp-file-receive/"; // 文件保存目录
public static void main(String[] args) {
// 创建保存目录(不存在则创建)
File saveDir = new File(SAVE_DIR);
if (!saveDir.exists()) {
boolean created = saveDir.mkdirs();
if (!created) {
System.err.println("无法创建保存目录:" + SAVE_DIR);
return;
}
}
int port = 9999; // 监听端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("文件服务器已启动,监听端口:" + port + ",保存目录:" + SAVE_DIR);
// 持续监听客户端连接
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接:" + clientSocket.getInetAddress());
// 使用线程池处理文件接收(支持并发)
THREAD_POOL.submit(() -> receiveFile(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器异常:" + e.getMessage());
} finally {
THREAD_POOL.shutdown(); // 优雅关闭线程池
}
}
/**
* 文件接收处理逻辑
* @param clientSocket 客户端Socket连接
*/
private static void receiveFile(Socket clientSocket) {
try (
DataInputStream dis = new DataInputStream(clientSocket.getInputStream());
DataOutputStream dos = new DataOutputStream(clientSocket.getOutputStream())
) {
// 设置30秒操作超时(适应大文件传输)
clientSocket.setSoTimeout(30000);
// 1. 接收文件名
int fileNameLen = dis.readInt();
byte[] fileNameBytes = new byte[fileNameLen];
dis.readFully(fileNameBytes);
String fileName = new String(fileNameBytes, "UTF-8");
String savePath = SAVE_DIR + fileName;
// 2. 接收文件大小
long fileSize = dis.readLong();
System.out.println("开始接收文件:" + fileName + "(大小:" + fileSize + "字节)");
// 3. 接收文件内容
try (FileOutputStream fos = new FileOutputStream(savePath)) {
byte[] buffer = new byte[1024 * 8]; // 8KB接收缓冲区
int readLen;
long receivedSize = 0;
while ((readLen = dis.read(buffer)) != -1) {
fos.write(buffer, 0, readLen);
receivedSize += readLen;
// 打印接收进度(每接收一个块更新一次)
System.out.printf("接收进度:%.2f%%\r", (receivedSize * 100.0) / fileSize);
// 检查是否接收完成
if (receivedSize == fileSize) {
break;
}
}
fos.flush();
System.out.println("\n文件接收完成,保存路径:" + savePath);
// 4. 向客户端发送成功响应
dos.writeUTF("文件接收成功!保存路径:" + savePath);
}
} catch (IOException e) {
System.err.println("文件接收异常:" + e.getMessage());
} finally {
try {
clientSocket.close(); // 确保连接关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
实现特点说明
-
可靠传输机制:
- 采用固定格式的消息头(文件名长度+文件名+文件大小)
- 实现进度监控和完整性校验
- 设置合理的超时机制
-
性能优化:
- 使用8KB缓冲区减少I/O操作次数
- 支持大文件传输(分块处理)
- 服务器使用线程池处理并发连接
-
健壮性设计:
- 自动创建保存目录
- 使用try-with-resources确保资源释放
- 详细的异常处理和日志记录
五、扩展
Netty 框架详解
Netty 是一个基于 Java NIO 的异步事件驱动网络应用框架,用于快速开发高性能、高可靠性的网络服务器和客户端程序。其核心优势包括:
- TCP/UDP 编程简化:通过 Channel、EventLoop 等抽象概念,简化了底层网络编程复杂度
- 粘包/拆包解决方案:
- 内置多种编解码器(如 LengthFieldBasedFrameDecoder)
- 支持自定义协议编解码(如 ProtobufCodec)
- 高可用机制:
- 自动断线重连(通过 ChannelFutureListener 监听连接状态)
- 心跳检测(IdleStateHandler 实现读写空闲检测)
- 应用案例:
- Dubbo 的底层通信框架
- Elasticsearch 的节点间通信
- RocketMQ 的 Broker 与 NameServer 通信
SSL/TLS 加密实现
- 核心类:
- SSLContext:安全套接字协议实现
- SSLSocketFactory:创建安全套接字
- SSLSocket:加密通信套接字
- 典型配置流程:
// 1. 获取SSLContext实例 SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); // 2. 初始化信任管理器 sslContext.init(null, trustManager, new SecureRandom()); // 3. 创建SSLSocket SSLSocket socket = (SSLSocket)sslContext.getSocketFactory() .createSocket(host, port); // 4. 启用加密套件 socket.setEnabledCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"});
- 适用场景:
- 金融支付交易
- 用户认证登录
- 医疗数据传输
- 政府机密通信
TCP 性能优化方案
-
缓冲区调优:
- 接收缓冲区:
socket.setReceiveBufferSize(64 * 1024)
(建议64KB起) - 发送缓冲区:
socket.setSendBufferSize(64 * 1024)
- 需与操作系统参数
net.core.rmem_max
协调
- 接收缓冲区:
-
Nagle算法禁用:
// 适用于实时性要求高的场景 socket.setTcpNoDelay(true);
- 效果:减少小数据包延迟(约40ms)
- 适用:在线游戏、实时交易系统
-
保活机制:
// 操作系统层心跳检测 socket.setKeepAlive(true); // 配合应用层心跳(如每30秒发送心跳包)
-
其他优化参数:
socket.setReuseAddress(true)
:端口快速重用socket.setSoTimeout(3000)
:设置读取超时socket.setPerformancePreferences(1, 2, 0)
:偏好延迟优化