首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java TCP 文件傳輸實戰:客戶端到服務端完整流程與「寫入路徑陷阱」避坑指南

     編輯:Java 綜合教學

Java TCP 文件傳輸實戰:客戶端到服務端完整流程與「寫入路徑陷阱」避坑指南

基於 TCP Socket 實現文件傳輸是 Java 網絡編程經典場景。本文提供完整的客戶端/服務端代碼,重點剖析一個極易踩中的「文件寫入路徑」陷阱,並給出生產級的資源管理、異常處理、雙向通信最佳實踐。

一、核心架構與協議設計

  • 傳輸協議:TCP(可靠、有序、面向連接)
  • 交互流程:
    1. 服務端啟動監聽(ServerSocket.accept())
    2. 客戶端連接(new Socket(host, port))
    3. 客戶端讀取本地文件 → 輸出流寫入 Socket
    4. 客戶端 shutdownOutput() 發送 EOF 信號
    5. 服務端輸入流讀取 → 寫入本地文件
    6. 服務端回應「接收成功」 → 客戶端讀取確認
    7. 雙向關閉流與 Socket
  • 關鍵點:shutdownOutput() 必須調用,否則服務端 read() 永遠阻塞等待數據

二、完整代碼實現(JUnit 測試版)

1. 客戶端

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) {}
    }
}

2. 服務端

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() 行

客戶端寫入拋出異常,但服務端代碼看似無誤。

根因分析

  1. 服務端代碼:fos = new FileOutputStream("D:/Test");
  2. "D:/Test" 是目錄路徑,非文件路徑
  3. Windows 下 FileOutputStream 構造方法接收目錄時,不拋出 FileNotFoundException,但後續 write() 會失敗
  4. 服務端 fos.write() 失敗 → 異常拋出 → finally 關閉 Socket
  5. 客戶端仍在 os.write(),檢測到連接被重置 → 拋出 SocketException: socket write error
  6. 錯誤堆疊指向客戶端寫入行,實際錯誤在服務端文件路徑——極易誤導排查方向

正確做法

// 方案 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

五、局域網測試不通常見原因排查

  1. 防火牆攔截:Windows 防火牆入站規則允許 9090 端口,或臨時關閉防火牆測試
  2. IP 地址錯誤:客戶端填寫服務端 ipconfig 顯示的 IPv4 地址,非 127.0.0.1
  3. 網段不通:確認雙方在同一子網,ping 互通
  4. 端口衝突:netstat -an | findstr 9090 檢查端口佔用
  5. 綁定地址:new ServerSocket(port) 默認綁定 0.0.0.0(所有網卡),正確

六、總結

  1. TCP 文件傳輸核心:Socket + InputStream/OutputStream + shutdownOutput() 發送 EOF
  2. 致命陷阱:FileOutputStream 必須指向具體文件,不能是目錄;錯誤表現為客戶端寫入拋異常,極具迷惑性
  3. 資源管理:finally 逆序關閉,或使用 Try-with-resources (Java 7+)
  4. 生產環境需補全:協議頭、斷點續傳、加密、併發模型、監控指標

掌握基礎流程並避開路徑陷阱,是構建可靠文件傳輸服務的第一步。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved