首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Java 客戶端上傳文件到 C++ 服務器:Socket 字節流傳輸完整實戰

     編輯:關於C++

前言

跨語言文件傳輸的核心在於:**統一數據表示為字節流**,Java 用 byte[],C++ 用 char[],雙方約定協議頭(命令類型、文件名、長度)+ 文件數據體。本文提供完整的 C++ 服務端(Windows Socket API)與 Java 客戶端代碼,詳細講解協議設計、四大常見坑點及解決方案,可直接用於生產環境參考。

一、傳輸協議設計

+------------------+------------------+------------------+
|  協議頭 (固定長度)  |   文件信息頭       |    文件數據體       |
+------------------+------------------+------------------+
| CMD(4B) | LEN(4B) | FILE_LEN(8B) | FILE_NAME(260B) |  文件二進制內容  |
+------------------+------------------+------------------+

CMD 定義:
0x01 SendFile       - 開始傳輸文件
0x02 StopSendFile   - 停止傳輸
0x03 SendString     - 發送字符串
0x04 FileInfo       - 文件元信息

二、C++ 服務端實現

1. 協議頭文件 (TAG.h)

#pragma once
#include 

#define BUF_LEN 2048
#define CMD_SEND_FILE     0x01
#define CMD_STOP_SEND     0x02
#define CMD_SEND_STRING   0x03
#define CMD_FILE_INFO     0x04

typedef struct {
    int ID;                 // 命令類型
    BYTE lparam[BUF_LEN];   // 參數載荷
} COMMAND;

typedef struct {
    long long fileLength;   // 文件長度(8 字節,匹配 Java long)
    char fileName[MAX_PATH]; // 文件名
} FILEINFO;

2. Socket 初始化封裝 (SocketHead.h)

#pragma once
#include 
#pragma comment(lib, "WS2_32.lib")

class CInitSock {
public:
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2) {
        WSADATA wsaData;
        WORD ver = MAKEWORD(minorVer, majorVer);
        if (WSAStartup(ver, &wsaData) != 0) exit(0);
    }
    ~CInitSock() { WSACleanup(); }
};

3. 核心服務邏輯 (CPlusServer.cpp)

#include "stdafx.h"
#include "SocketHead.h"
#include "TAG.h"
#include 
#include 
#include 
#include  // _beginthreadex

using namespace std;

CInitSock initSock;
const int PORT = 8888;

sockaddr_in initAddr() {
    sockaddr_in addr;
    ZeroMemory(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    return addr;
}

unsigned __stdcall ClientThread(void* param) {
    SOCKET clientSock = (SOCKET)param;
    COMMAND cmd;
    FILEINFO fInfo;
    char buffer[BUF_LEN];
    
    // 1. 接收文件信息頭
    int ret = recv(clientSock, (char*)&fInfo, sizeof(fInfo), 0);
    if (ret <= 0) { closesocket(clientSock); return 0; }
    
    // 2. 發送確認(可選,此處省略)
    
    // 3. 接收文件數據並寫入
    string filePath = "upload_" + string(fInfo.fileName);
    ofstream outfile(filePath, ios::out | ios::binary); // 關鍵:二進制模式!
    if (!outfile) { closesocket(clientSock); return 0; }
    
    long long remaining = fInfo.fileLength;
    while (remaining > 0) {
        int toRecv = (int)min(remaining, BUF_LEN);
        ret = recv(clientSock, buffer, toRecv, 0);
        if (ret <= 0) break;
        outfile.write(buffer, ret); // 關鍵:write 而非 <<
        remaining -= ret;
    }
    outfile.close();
    cout << "File received: " << filePath << " (" << fInfo.fileLength << " bytes)" << endl;
    
    closesocket(clientSock);
    return 0;
}

int main() {
    SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in serverAddr = initAddr();
    bind(serverSock, (sockaddr*)&serverAddr, sizeof(serverAddr));
    listen(serverSock, 5);
    cout << "Server started on port " << PORT << endl;
    
    sockaddr_in clientAddr;
    int addrLen = sizeof(clientAddr);
    while (true) {
        SOCKET clientSock = accept(serverSock, (sockaddr*)&clientAddr, &addrLen);
        if (clientSock == INVALID_SOCKET) continue;
        cout << "Client connected: " << inet_ntoa(clientAddr.sin_addr) << endl;
        _beginthreadex(nullptr, 0, ClientThread, (void*)clientSock, 0, nullptr);
    }
    closesocket(serverSock);
    return 0;
}

三、Java 客戶端實現

1. 數據類型轉換工具 (NetDataTypeTransform.java)

package client;
import java.io.UnsupportedEncodingException;

public class NetDataTypeTransform {
    public static final String CODING = "GB2312"; // 與 C++ 一致

    // int -> 4 字節小端序
    public byte[] intToByteArray(int n) {
        return new byte[] {
            (byte)(n & 0xFF),
            (byte)((n >> 8) & 0xFF),
            (byte)((n >> 16) & 0xFF),
            (byte)((n >> 24) & 0xFF)
        };
    }
    // long -> 8 字節小端序
    public byte[] longToByteArray(long n) {
        byte[] b = new byte[8];
        for (int i = 0; i < 8; i++) b[i] = (byte)((n >> (8*i)) & 0xFF);
        return b;
    }
    // byte[] -> int (小端序)
    public int byteArrayToInt(byte[] b) {
        if (b.length != 4) return -1;
        return ((b[3]&0xFF)<<24) | ((b[2]&0xFF)<<16) | ((b[1]&0xFF)<<8) | (b[0]&0xFF);
    }
    // byte[] -> long
    public long byteArrayToLong(byte[] b) {
        if (b.length != 8) return -1;
        long v = 0;
        for (int i = 7; i >= 0; i--) v = (v<<8) | (b[i]&0xFF);
        return v;
    }
    // String <-> byte[] (GBK)
    public String byteArrayToString(byte[] b, int maxLen) {
        int len = 0;
        while (len < b.length && len < maxLen && b[len] != 0) len++;
        try { return new String(b, 0, len, CODING); }
        catch (UnsupportedEncodingException e) { return ""; }
    }
    public byte[] stringToByteArray(String s) {
        try { return s.getBytes(CODING); }
        catch (UnsupportedEncodingException e) { return new byte[0]; }
    }
}

2. 命令封裝 (NetDataCommand.java)

package client;
public class NetDataCommand {
    private int id;
    private String fileName;
    private NetDataTypeTransform transformer = new NetDataTypeTransform();
    
    public NetDataCommand(int id, String fileName) {
        this.id = id;
        this.fileName = fileName;
    }
    // 序列化為:ID(4B) + fileName(260B, GBK, 補0)
    public byte[] getByteArrayData() {
        byte[] data = new byte[4 + 260];
        byte[] idBytes = transformer.intToByteArray(id);
        byte[] nameBytes = transformer.stringToByteArray(fileName);
        System.arraycopy(idBytes, 0, data, 0, 4);
        System.arraycopy(nameBytes, 0, data, 4, Math.min(nameBytes.length, 260));
        return data;
    }
}

3. 發送文件線程 (SendFileThread.java)

package client;
import java.io.*;
import java.net.Socket;

public class SendFileThread extends Thread {
    private Socket socket;
    private NetDataTypeTransform transformer = new NetDataTypeTransform();
    
    public SendFileThread(Socket socket) { this.socket = socket; }
    
    @Override
    public void run() {
        try (DataOutputStream out = new DataOutputStream(socket.getOutputStream());
             DataInputStream in = new DataInputStream(socket.getInputStream());
             FileInputStream fis = new FileInputStream("F:\\Android\\4.jpg")) {
            
            File file = new File("F:\\Android\\4.jpg");
            long fileLen = file.length();
            
            // 1. 發送文件長度 (8 字節 long)
            out.write(transformer.longToByteArray(fileLen));
            out.flush();
            
            // 2. 等待服務端確認(可選)
            // boolean ok = in.readBoolean();
            
            // 3. 分塊發送文件數據
            byte[] buffer = new byte[2048];
            int len;
            long sent = 0;
            while ((len = fis.read(buffer)) > 0) {
                out.write(buffer, 0, len); // 關鍵:寫實際讀取長度 len,而非 buffer.length!
                out.flush();
                sent += len;
                System.out.printf("Progress: %.2f%%%n", sent * 100.0 / fileLen);
            }
            System.out.println("Send complete: " + sent + " bytes");
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4. 主程序 (Client.java)

package client;
import java.net.Socket;
import java.io.DataOutputStream;

public class Client {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 8888);
             DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
            
            // 發送文件信息命令
            NetDataCommand cmd = new NetDataCommand(1, "4.jpg");
            out.write(cmd.getByteArrayData());
            out.flush();
            
            // 啟動發送線程
            new SendFileThread(socket).start();
        } catch (Exception e) { e.printStackTrace(); }
    }
}

四、四大核心坑點與解決方案

#現象根因分析解決方案
1文本文件傳輸出現亂碼C++ 用 outfile << buffer 而非 outfile.write(buffer, ret),<< 遇 \0 截斷或格式化統一使用 outfile.write(buffer, ret) 二進制寫入
2接收文件比發送文件大Java 發送時寫 buffer.length 而非實際讀取長度 len,導致填充垃圾數據Java 端 out.write(buffer, 0, len);C++ 端 outfile.write(buffer, ret)
3圖片無法打開,多 4 字節Java long 8 字節,C++ long 4 字節(32/64 位均為 4B),接收時只讀 4B,剩 4B 滯留緩衝區混入文件數據C++ 端 FILEINFO 中 fileLength 改為 long long (8B),recv 大小 sizeof(long long)
4二進制文件(圖片)損壞C++ 以文本模式 ios::out 打開文件,Windows 下 \n 被轉為 \r\n以二進制模式打開:ios::out | ios::binary

五、關鍵最佳實踐總結

  1. 協議明確化:固定長度頭部 + 明確字節序(小端序)+ 字符編碼(GBK/UTF-8)。
  2. 二進制模式:文件操作、Socket 收發均使用二進制模式,禁用任何文本轉換。
  3. 長度前置:發送數據前先發長度,接收端按長度循環讀取,避免粘包/半包。
  4. 實際長度收發:send/write(buf, actual_len),recv/read(buf, max_len) 返回值為實際收發長度。
  5. 跨語言類型對齊:Java long(8B) ↔ C++ long long(8B);Java int(4B) ↔ C++ int(4B);字符編碼統一。
  6. 錯誤處理:檢查 recv/send 返回值(SOCKET_ERROR/0/>0),網絡斷開時優雅退出。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved