基於 TCP Socket 實現文件傳輸是 Java 網絡編程經典場景。本文提供完整的客戶端/服務端代碼,重點剖析一個極易踩中的「文件寫入路徑」陷阱,並給出生產級的資源管理、異常處理、雙向通信最佳實踐。
ServerSocket.accept())new Socket(host, port))shutdownOutput() 發送 EOF 信號shutdownOutput() 必須調用,否則服務端 read() 永遠阻塞等待數據package com.tcp.demo;
import org.junit.Test;
import java.io.*;
import java.net.*;
public class FileTransferClient {
@Test
public void sendFile() {
String serverIp = "192.168.1.100"; // 修改為服務端實際 IP
int port = 9090;
String filePath = "findLei.jpg"; // 待發送文件
Socket socket = null;
OutputStream os = null;
FileInputStream fis = null;
InputStream is = null;
try {
// 1. 建立連接
socket = new Socket(InetAddress.getByName(serverIp), port);
System.out.println("連接服務端成功:" + serverIp + ":" + port);
// 2. 發送文件
os = socket.getOutputStream();
fis = new FileInputStream(filePath);
byte[] buffer = new byte[8192]; // 8KB 緩衝區
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
// 3. 關鍵:發送 EOF,告知服務端數據傳輸結束
socket.shutdownOutput();
System.out.println("文件發送完成,等待服務端確認...");
// 4. 接收服務端回應
is = socket.getInputStream();
byte[] respBuffer = new byte[1024];
int respLen = is.read(respBuffer);
if (respLen > 0) {
String response = new String(respBuffer, 0, respLen, "UTF-8");
System.out.println("服務端回應:" + response);
}
} catch (IOException e) {
System.err.println("傳輸失敗:" + e.getMessage());
e.printStackTrace();
} finally {
// 5. 逆序關閉資源
closeQuietly(is);
closeQuietly(fis);
closeQuietly(os);
closeQuietly(socket);
}
}
private void closeQuietly(Closeable c) {
if (c != null) try { c.close(); } catch (IOException ignored) {}
}
private void closeQuietly(Socket s) {
if (s != null) try { s.close(); } catch (IOException ignored) {}
}
}
package com.tcp.demo;
import org.junit.Test;
import java.io.*;
import java.net.*;
public class FileTransferServer {
@Test
public void receiveFile() {
int port = 9090;
String saveDir = "D:/received/"; // 修改為實際保存目錄
ServerSocket ss = null;
Socket s = null;
InputStream is = null;
FileOutputStream fos = null;
OutputStream os = null;
try {
// 1. 啟動監聽
ss = new ServerSocket(port);
System.out.println("服務端啟動,監聽端口:" + port);
// 2. 接受連接(阻塞)
s = ss.accept();
System.out.println("客戶端連接:" + s.getInetAddress());
// 3. 接收文件數據
is = s.getInputStream();
// ⚠️ 關鍵修正:必須指定具體文件名,不能只有目錄!
// 錯誤:new FileOutputStream("D:/Test") → 視為目錄,拋出 SocketException
// 正確:生成唯一文件名
String fileName = "received_" + System.currentTimeMillis() + ".jpg";
File saveFile = new File(saveDir, fileName);
saveFile.getParentFile().mkdirs(); // 確保目錄存在
fos = new FileOutputStream(saveFile);
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
System.out.println("文件保存至:" + saveFile.getAbsolutePath());
// 4. 發送確認回應
os = s.getOutputStream();
os.write("接收成功".getBytes("UTF-8"));
os.flush();
} catch (IOException e) {
System.err.println("接收失敗:" + e.getMessage());
e.printStackTrace();
} finally {
closeQuietly(os);
closeQuietly(fos);
closeQuietly(is);
closeQuietly(s);
closeQuietly(ss);
}
}
private void closeQuietly(Closeable c) {
if (c != null) try { c.close(); } catch (IOException ignored) {}
}
private void closeQuietly(Socket s) {
if (s != null) try { s.close(); } catch (IOException ignored) {}
}
private void closeQuietly(ServerSocket ss) {
if (ss != null) try { ss.close(); } catch (IOException ignored) {}
}
}
new FileOutputStream("D:/Test") 會報錯?java.net.SocketException: Software caused connection abort: socket write error
at java.net.SocketOutputStream.socketWrite0(Native Method)
at com.tcp.demo.FileTransferClient.sendFile(FileTransferClient.java:35) // os.write() 行
客戶端寫入拋出異常,但服務端代碼看似無誤。
fos = new FileOutputStream("D:/Test");"D:/Test" 是目錄路徑,非文件路徑FileOutputStream 構造方法接收目錄時,不拋出 FileNotFoundException,但後續 write() 會失敗fos.write() 失敗 → 異常拋出 → finally 關閉 Socketos.write(),檢測到連接被重置 → 拋出 SocketException: socket write error// 方案 1:指定具體文件名(相對路徑,當前工作目錄)
fos = new FileOutputStream("received.jpg");
// 方案 2:目錄 + 文件名(推薦生產環境)
File saveDir = new File("D:/received/");
saveDir.mkdirs();
String fileName = "file_" + System.currentTimeMillis() + ".jpg";
fos = new FileOutputStream(new File(saveDir, fileName));
// 方案 3:隨機文件名避免衝突
Random r = new Random();
fos = new FileOutputStream(new File(saveDir, "file_" + r.nextInt(10000) + ".jpg"));
| 維度 | 基礎版缺陷 | 增強方案 |
|---|---|---|
| 文件元數據 | 無文件名、大小、類型信息 | 自定義協議頭:JSON/二進制頭部攜帶文件名、長度、MD5 |
| 大文件支撐 | 無斷點續傳、進度監控 | 分塊傳輸、RandomAccessFile 斷點續傳、進度回調 |
| 多文件傳輸 | 單連接單文件 | 壓縮打包、或多路復用協議 |
| 異常恢復 | 網絡中斷直接失敗 | 心跳檢測、重傳機制、冪等性設計 |
| 安全性 | 明文傳輸,無鑒權 | SSL/TLS 加密、Token 鑒權、文件簽名驗證 |
| 併發處理 | 單線程阻塞 accept | 線程池/Netty 非阻塞 I/O、NIO.2 AsynchronousServerSocketChannel |
ipconfig 顯示的 IPv4 地址,非 127.0.0.1ping 互通netstat -an | findstr 9090 檢查端口佔用new ServerSocket(port) 默認綁定 0.0.0.0(所有網卡),正確Socket + InputStream/OutputStream + shutdownOutput() 發送 EOFFileOutputStream 必須指向具體文件,不能是目錄;錯誤表現為客戶端寫入拋異常,極具迷惑性finally 逆序關閉,或使用 Try-with-resources (Java 7+)掌握基礎流程並避開路徑陷阱,是構建可靠文件傳輸服務的第一步。