14.9 Socket 高效檔案傳輸

2023-10-17 12:02:25

網路上的檔案傳輸功能也是很有必要實現一下的,網路傳輸檔案的過程通常分為使用者端和伺服器端兩部分。使用者端可以選擇上傳或下載檔案,將檔案分塊並逐塊傳送到伺服器,或者從伺服器分塊地接收檔案。伺服器端接收來自使用者端的請求,根據請求型別執行對應的操作,並根據傳送的檔名或其他標識來確定要傳輸的檔案。

在實現檔案傳輸之前,需要先開啟要傳輸的檔案,並獲取檔案的大小資訊,也可以通過其他方式獲取檔案的資訊。在使用者端和伺服器端都準備就緒後,可以通過通訊端來傳送檔案資料。在傳輸檔案的過程中,可以將檔案分解為若干個封包進行傳輸,以減少資料傳輸中的丟包或傳輸錯誤。每個封包的長度可以根據實際情況進行選擇,通常選擇1024位元組或更大,也可以設定成更小的值。傳輸檔案的過程中,還需要實現一定的錯誤處理機制,例如檢測傳輸過程中的超時、丟包、不完整資料等情況,並在必要時進行錯誤重傳或協商其他解決方案。

首先無論時伺服器端還是使用者端都需要封裝兩個函數,其中GetFileName()函數用於當用戶傳入檔案的具體路徑資訊時自動獲取到該檔案的檔名,第二個函數GetFileSize()則用於傳入檔案路徑並自動獲取到該檔案的位元組數。

// 傳入路徑得到檔名
char* GetFileName(char* Path)
{
  if (strchr(Path, '\\'))
  {
    char ch = '\\';
    char* ref = strrchr(Path, ch) + 1;
    return ref;
  }
  else
  {
    char ch = '/';
    char* ref = strrchr(Path, ch) + 1;
    return ref;
  }
}

// 獲取檔案大小
int GetFileSize(std::string FileName)
{
  FILE* pointer = NULL;
  pointer = fopen(FileName.c_str(), "rb");
  if (pointer != NULL)
  {
    fseek(pointer, 0, SEEK_END);
    int size = ftell(pointer);
    fclose(pointer);
    return size;
  }
  return 0;
}

接著我們來看一下RecvFile()接收檔案函數是如何實現的,首先第一個傳送用於向伺服器端發出我需要下載具體的那個目錄下的檔案,接著伺服器端會返回該目錄檔案的長度,此時我們通過fopen()建立一個新檔案,並以此迴圈接收該檔案的長度,每次接收成功後自動的fwrite寫出到檔案中,當檔案被接收完畢後,則通過fclose(pointer)儲存並關閉檔案。

// 接收檔案
bool RecvFile(SOCKET ptr, char* LocalPath, char* RemoteFile)
{
  // 傳送需要下載的檔案路徑
  send(ptr, RemoteFile, strlen(RemoteFile), 0);

  // 接收檔案長度
  long long file_size = 0;
  recv(ptr, (char*)&file_size, sizeof(int), 0);
  if (file_size <= 0)
  {
    return false;
  }

  // 儲存檔案到指定目錄下
  char *file_name = GetFileName(RemoteFile);
  char file_all_name[1024] = { 0 };

  strcat(file_all_name, LocalPath);
  strcat(file_all_name, file_name);

  std::cout << "生成儲存路徑: " << file_all_name << std::endl;
  FILE* pointer = fopen(file_all_name, "wb");
  char buffer[1024] = { 0 };

  if (pointer != NULL)
  {
    long long length = 0;
    long long total_length = 0;

    // 迴圈接收位元組資料,每次接收1024位元組
    while ((length = recv(ptr, buffer, 1024, 0)) > 0)
    {
      // 寫出檔案並判斷是否寫出成功
      if (fwrite(buffer, sizeof(char), length, pointer) < length)
      {
        break;
      }

      // 每次累加遞增
      total_length += length;
      memset(buffer, 0, 1024);

      // 判斷檔案長度是否全部接收完畢
      if (total_length >= file_size)
      {
        std::cout << "檔案接收完畢, 接收位元組數: " << total_length << std::endl;
        fclose(pointer);
        return true;
      }
    }
    fclose(pointer);
  }
  return false;
}

對於SendFile()傳送檔案而言首先我們接收到使用者端傳來的檔案路徑,並通過該路徑得到該檔案的具體長度,第一次呼叫傳送函數將檔案的長度傳遞給使用者端,此時開啟我們所需要傳送的檔案,並通過迴圈的方式向用戶端傳輸,當封包傳輸完畢後則自動關閉檔案。

// 傳送指定檔案
bool SendFile(SOCKET ptr)
{
  // 接收檔案路徑
  char file_path[1024] = { 0 };
  recv(ptr, file_path, 1024, 0);

  // 得到檔案長度並行送給伺服器端
  long long file_size = GetFileSize(file_path);

  if (file_size <= 0)
  {
    return false;
  }
  send(ptr, (char*)&file_size, sizeof(int), 0);
  std::cout << "傳送檔案長度: " << file_size << std::endl;

  // 迴圈傳送資料
  char buffer[1024] = { 0 };
  FILE* pointer = fopen(file_path, "rb");
  if (pointer != NULL)
  {
    long long length = 0;
    long long total_length = 0;

    // 迴圈傳送資料
    while ((length = fread(buffer, sizeof(char), 1024, pointer)) > 0)
    {
      send(ptr, buffer, length, 0);
      memset(buffer, 0, 1024);
      total_length += length;
    }

    if (total_length == file_size)
    {
      return true;
    }
  }
  return false;
}

14.9.1 伺服器端實現

如下程式碼展示瞭如何使用Winsock進行TCP協定的檔案傳輸。首先使用WSAStartup函數對Winsock庫進行初始化。然後建立一個socket,設定IP地址、埠號等資訊,並將該socket和本地伺服器端的地址繫結起來。接下來對該socket進行監聽,等待使用者端的連線請求。

當有使用者端連線請求到來時,accept函數會接收請求,並建立一個新的socket與使用者端進行通訊。在與使用者端通訊的過程中,可以通過sendrecv函數進行資料的傳輸,實現檔案的上傳和下載功能。此處的程式碼呼叫RecvFile函數,該函數為自定義實現的接收檔案函數,負責接收資料並將接收到的檔案儲存到指定的路徑下。

int main(int argc, char* argv[])
{
  WSADATA wsaData;
  if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    exit(1);

  // 宣告並初始化一個伺服器端(本地)的地址結構 
  sockaddr_in server_addr;
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.S_un.S_addr = INADDR_ANY;
  server_addr.sin_port = htons(8087);

  // 建立socket 
  SOCKET m_Socket = socket(AF_INET, SOCK_STREAM, 0);
  if (SOCKET_ERROR == m_Socket)
    exit(1);

  // 繫結socket和伺服器端(本地)地址 
  if (SOCKET_ERROR == bind(m_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)))
    exit(1);

  // 監聽 
  if (SOCKET_ERROR == listen(m_Socket, 10))
    exit(1);

  sockaddr_in client_addr;
  int client_addr_len = sizeof(client_addr);

  SOCKET m_New_Socket = accept(m_Socket, (sockaddr*)&client_addr, &client_addr_len);

  // 接收遠端d://lyshark.exe放到原生的d://11g/目錄下
  bool ref = RecvFile(m_New_Socket, (char*)"d://11g/", (char*)"d://lyshark.exe");
  std::cout << "接收狀態: " << ref << std::endl;

  closesocket(m_New_Socket);
  closesocket(m_Socket);

  WSACleanup();
  system("pause");
  return 0;
}

14.9.2 使用者端實現

如下使用者端程式碼實現了一個基於TCP協定的檔案傳輸使用者端。首先使用WSAStartup函數對Winsock庫進行初始化。然後建立一個socket,並設定伺服器端的IP地址和埠號。之後通過connect函數與伺服器端建立連線,連線成功後呼叫SendFile函數進行檔案傳輸,將指定的檔案傳送到伺服器端。檔案傳輸完成後,關閉socket連線,清除Winsock資源。

int main(int argc, char* argv[])
{
  while (true)
  {
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
      exit(1);

    // 建立socket 
    SOCKET c_Socket = socket(AF_INET, SOCK_STREAM, 0);

    //指定伺服器端的地址 
    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(8087);

    if (SOCKET_ERROR != connect(c_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr)))
    {
      bool ref = SendFile(c_Socket);
      std::cout << "檔案傳送狀態: " << ref << std::endl;
    }
    closesocket(c_Socket);
    WSACleanup();
    Sleep(1000);
  }
  return 0;
}

檔案傳輸功能程式碼就這些,其實理解起來並不難,讀者可自行編譯並執行上述程式碼,執行後則可接收遠端d://lyshark.exe檔案,並放到原生的d://11g/目錄下,輸出效果圖如下;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/323aa250.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!