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

Windows Socket 網路程式設計:C 語言實作檔案下載伺服器與用戶端

     編輯:C 語言入門

基於 Winsock 2 的 TCP 檔案傳輸範例,支援目錄列舉、檔案下載、進度顯示與多執行緒併發。程式碼針對 VC++ 6.0 撰寫,現代 Visual Studio 亦可編譯。

架構概覽

  • 伺服器:listen → accept → 建立執行緒處理客戶端 → 指令分派
  • 用戶端:連線 → 互動式指令列(dir/get/help/exit)
  • 協定:文字指令 + 二進位檔案流,以 quit 標記傳輸結束

伺服器核心實作

初始化與監聽

#pragma comment(lib, "Ws2_32.lib")
#define PORT 8088
#define BUFSZ 4096

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {AF_INET, htons(PORT), INADDR_ANY};
bind(listen_sock, (SOCKADDR*)&addr, sizeof(addr));
listen(listen_sock, 10);

執行緒處理函式

DWORD WINAPI ClientThread(LPVOID lp) {
    CLIENTINFO *ci = (CLIENTINFO*)lp;
    SOCKET sock = ci->clientsocket;
    char cmd[512], path[MAX_PATH];
    GetModuleFileName(NULL, path, MAX_PATH);
    *strrchr(path, '\\') = 0;  // 取得執行檔目錄

    while (1) {
        int n = recv(sock, cmd, sizeof(cmd), 0);
        if (n <= 0) break;
        cmd[n] = 0;

        if (strcmp(cmd, "dir") == 0) {
            system("cmd /c dir /a-d /b > allfiles.txt");
            FILE *f = fopen("allfiles.txt", "r");
            char line[1024];
            while (fgets(line, sizeof(line), f)) {
                send(sock, line, strlen(line), 0);
            }
            fclose(f);
            send(sock, "quit", 4, 0);  // 結束標記
        }
        else {  // get filename
            strcat(path, "\\"); strcat(path, cmd);
            FILE *fp = fopen(path, "rb");
            if (!fp) { send(sock, "ERR", 3, 0); continue; }

            fseek(fp, 0, SEEK_END);
            long fsize = ftell(fp);
            fseek(fp, 0, SEEK_SET);
            char sz[32]; sprintf(sz, "%ld", fsize);
            send(sock, sz, 30, 0);  // 先傳檔案大小

            char buf[BUFSZ];
            size_t r;
            while ((r = fread(buf, 1, BUFSZ, fp)) > 0)
                send(sock, buf, r, 0);
            fclose(fp);
        }
    }
    closesocket(sock);
    return 0;
}

用戶端核心實作

連線與指令迴圈

SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(sock, (SOCKADDR*)&srv_addr, sizeof(srv_addr));

while (1) {
    printf("指令 (help/dir/get/exit): ");
    scanf("%s", cmd);
    if (strcmp(cmd, "help") == 0) { puts("dir/get/exit"); continue; }
    if (strcmp(cmd, "exit") == 0) break;

    send(sock, cmd, strlen(cmd), 0);

    if (strcmp(cmd, "dir") == 0) {
        char buf[32];
        while (recv(sock, buf, 30, 0) > 0) {
            if (strcmp(buf, "quit") == 0) break;
            printf("%s", buf);
        }
    }
    else if (strcmp(cmd, "get") == 0) {
        char fname[256], local[256];
        scanf("%s %s", fname, local);
        send(sock, fname, strlen(fname), 0);

        char sz[32]; recv(sock, sz, 30, 0);
        long total = atol(sz);
        FILE *fp = fopen(local, "wb");

        long got = 0; char buf[BUFSZ];
        while (got < total) {
            int n = recv(sock, buf, BUFSZ, 0);
            if (n <= 0) break;
            fwrite(buf, 1, n, fp);
            got += n;
            printf("\r進度: %.1f%%", got * 100.0 / total);
        }
        fclose(fp);
        puts("\n下載完成");
    }
}

關鍵技術細節與已知問題

  • 進度計算:原版用 int 導致 cnt/size*100 整數除法恆為 0,修為 float 解決
  • 目錄指令偶發退出:system() 呼叫與 socket 非同步互動可能衝突,建議改用 FindFirstFile API
  • 緩衝區殘留:recv 可能一次讀取多條指令,需應用層協定分幀(長度前綴或分隔符)
  • 錯誤處理:生產環境需完善 WSAGetLastError() 判斷與重試邏輯
  • 編碼問題:中文檔名在舊版 Windows 需注意 ANSI/UTF-8 轉換

現代化改進建議

  1. 改用 getaddrinfo 支援 IPv6
  2. 採用 IOCP (I/O Completion Ports) 替代執行緒池
  3. 應用層協定加入長度前綴:uint32_t len + payload
  4. 傳輸大檔案時啟用 TransmitFile 零拷貝 API
  5. 加入 TLS (Schannel/OpenSSL) 保障機密性
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved