14.8 Socket 一收一發通訊

lyshark發表於2023-10-16

通常情況下我們在編寫套接字通訊程式時都會實現一收一發的通訊模式,當客戶端傳送資料到服務端後,我們希望服務端處理請求後同樣返回給我們一個狀態值,並以此判斷我們的請求是否被執行成功了,另外增加收發同步有助於避免資料包粘包問題的產生,在多數開發場景中我們都會實現該功能。

Socket粘包是指在使用TCP協議傳輸資料時,傳送方連續向接收方傳送多個資料包時,接收方可能會將它們合併成一個或多個大的資料包,而不是按照傳送方傳送的原始資料包拆分成多個小的資料包進行接收。

造成粘包的原因主要有以下幾個方面:

  • TCP協議的特性:TCP是一種面向連線的可靠傳輸協議,保證了資料的正確性和可靠性。在TCP協議中,傳送方和接收方之間建立了一條虛擬的連線,透過三次握手來建立連線。當資料在傳輸過程中出現丟失、損壞或延遲等問題時,TCP會自動進行重傳、校驗等處理,這些處理會導致接收方在接收資料時可能會一次性接收多個資料包。
  • 緩衝區的大小限制:在接收方的緩衝區大小有限的情況下,如果傳送方傳送的多個小資料包的總大小超過了接收方緩衝區的大小,接收方可能會將它們合併成一個大的資料包來接收。
  • 資料的處理方式:接收方在處理資料時,可能會使用不同的方式來處理資料,比如按照位元組流方式讀取資料,或者按照固定長度讀取資料等方式。不同的處理方式可能會導致接收方將多個資料包合併成一個大的資料包。

如果讀者是一名Windows平臺開發人員並從事過網路套接字開發,那麼一定很清楚此缺陷的產生,當我們連續呼叫send()時就會產生粘包現象,而解決此類方法的最好辦法是在每次send()後呼叫一次recv()函式接收一個返回值,至此由於資料包不連續則也就不會產生粘包的現象。

14.8.1 服務端實現

服務端我們實現的功能只有一個接收,其中RecvFunction函式主要用於接收資料包,透過使用recv函式接收來自socket連線通道的資料,並根據接收到的資料判斷條件,決定是否傳送資料回應。如果接收到的資料中命令引數滿足command_int_a=10command_int_b=20,那麼該函式會構建一個新的資料包,將其傳送回客戶端,其中包括一個表示成功執行的標誌、一個包含歡迎資訊的字串以及其他資料資訊。如果接收到的資料命令引數不滿足上述條件,則函式會構建一個新的資料包,將其傳送回客戶端,其中只包括一個表示執行失敗的標誌。最後,函式返回一個BOOL型別的布林值,表示接收函式是否成功執行。

#include <iostream>
#include <winsock2.h>
#include <WS2tcpip.h>

#pragma comment(lib,"ws2_32.lib")

typedef struct
{
  int command_int_a;
  int command_int_b;
  int command_int_c;
  int command_int_d;

  unsigned int command_uint_a;
  unsigned int command_uint_b;

  char command_string_a[256];
  char command_string_b[256];
  char command_string_c[256];
  char command_string_d[256];

  int flag;
  int count;
}send_recv_struct;

// 呼叫接收函式
BOOL RecvFunction(SOCKET &sock)
{
  // 接收資料
  char recv_buffer[8192] = { 0 };
  int recv_flag = recv(sock, (char *)&recv_buffer, sizeof(send_recv_struct), 0);
  if (recv_flag <= 0)
  {
    return FALSE;
  }

  send_recv_struct *buffer = (send_recv_struct *)recv_buffer;

  std::cout << "接收引數A: " << buffer->command_int_a << std::endl;

  // 接收後判斷,判斷後傳送標誌或攜帶引數
  if (buffer->command_int_a == 10 && buffer->command_int_b == 20)
  {
    send_recv_struct send_buffer = { 0 };
    send_buffer.flag = 1;
    strcpy(send_buffer.command_string_a, "hello lyshark");

    // 傳送資料
    int send_flag = send(sock, (char *)&send_buffer, sizeof(send_recv_struct), 0);
    if (send_flag <= 0)
    {
      return FALSE;
    }
  }
  else
  {
    send_recv_struct send_buffer = { 0 };
    send_buffer.flag = 0;

    // 傳送資料
    int send_flag = send(sock, (char *)&send_buffer, sizeof(send_recv_struct), 0);
    if (send_flag <= 0)
    {
      return FALSE;
    }

    return FALSE;
  }
  return TRUE;
}

int main(int argc, char *argv[])
{
  WSADATA WSAData;

  if (WSAStartup(MAKEWORD(2, 0), &WSAData) == SOCKET_ERROR)
  {
    std::cout << "WSA動態庫初始化失敗" << std::endl;
    return 0;
  }

  SOCKET server_socket;

  if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == ERROR)
  {
    std::cout << "Socket 建立失敗" << std::endl;
    WSACleanup();
    return 0;
  }

  struct sockaddr_in ServerAddr;
  ServerAddr.sin_family = AF_INET;
  ServerAddr.sin_port = htons(9999);
  ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

  if (bind(server_socket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR)
  {
    std::cout << "繫結套接字失敗" << std::endl;
    closesocket(server_socket);
    WSACleanup();
    return 0;
  }

  if (listen(server_socket, 10) == SOCKET_ERROR)
  {
    std::cout << "偵聽套接字失敗" << std::endl;
    closesocket(server_socket);
    WSACleanup();
    return 0;
  }

  SOCKET message_socket;

  char buf[8192] = { 0 };

  if ((message_socket = accept(server_socket, (LPSOCKADDR)0, (int*)0)) == INVALID_SOCKET)
  {
    return 0;
  }

  send_recv_struct recv_buffer = { 0 };

  // 接收對端資料到recv_buffer
  BOOL flag = RecvFunction(message_socket);
  std::cout << "接收狀態: " << flag << std::endl;

  closesocket(message_socket);
  closesocket(server_socket);
  WSACleanup();
  return 0;
}

14.8.2 客戶端實現

對於客戶端而言,其與服務端保持一致,只需要封裝一個對等的SendFunction函式,該函式使用send函式將一個send_recv_struct型別的指標send_ptr傳送到指定的socket連線通道。在傳送完成後,函式使用recv函式從socket連線通道接收資料,並將其儲存到一個char型陣列recv_buffer中。接下來,該函式使用send_recv_struct型別的指標buffer將該char型陣列中的資料複製到一個新的send_recv_struct型別的結構體變數recv_ptr中,最後返回一個BOOL型別的布林值,表示傳送接收函式是否成功執行。

#include <iostream>
#include <winsock2.h>

#pragma comment(lib,"ws2_32.lib")

typedef struct
{
  int command_int_a;
  int command_int_b;
  int command_int_c;
  int command_int_d;

  unsigned int command_uint_a;
  unsigned int command_uint_b;

  char command_string_a[256];
  char command_string_b[256];
  char command_string_c[256];
  char command_string_d[256];

  int flag;
  int count;
}send_recv_struct;

// 呼叫傳送接收函式
BOOL SendFunction(SOCKET &sock, send_recv_struct &send_ptr, send_recv_struct &recv_ptr)
{
  // 傳送資料
  int send_flag = send(sock, (char *)&send_ptr, sizeof(send_recv_struct), 0);
  if (send_flag <= 0)
  {
    return FALSE;
  }

  // 接收資料
  char recv_buffer[8192] = { 0 };
  int recv_flag = recv(sock, (char *)&recv_buffer, sizeof(send_recv_struct), 0);
  if (recv_flag <= 0)
  {
    return FALSE;
  }

  send_recv_struct *buffer = (send_recv_struct *)recv_buffer;
  memcpy((void *)&recv_ptr, buffer, sizeof(send_recv_struct));
  return TRUE;
}

int main(int argc, char* argv[])
{
  WSADATA WSAData;
  if (WSAStartup(MAKEWORD(2, 0), &WSAData) == SOCKET_ERROR)
  {
    return 0;
  }
  SOCKET client_socket;
  if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR)
  {
    WSACleanup();
    return 0;
  }

  struct sockaddr_in ClientAddr;
  ClientAddr.sin_family = AF_INET;
  ClientAddr.sin_port = htons(9999);
  ClientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  if (connect(client_socket, (LPSOCKADDR)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
  {
    closesocket(client_socket);
    WSACleanup();
    return 0;
  }

  send_recv_struct send_buffer = {0};
  send_recv_struct response_buffer = { 0 };

  // 填充傳送資料包
  send_buffer.command_int_a = 10;
  send_buffer.command_int_b = 20;
  send_buffer.flag = 0;

  // 傳送資料包,並接收返回結果
  BOOL flag = SendFunction(client_socket, send_buffer, response_buffer);
  if (flag == FALSE)
  {
    return 0;
  }

  std::cout << "響應狀態: " << response_buffer.flag << std::endl;
  if (response_buffer.flag == 1)
  {
    std::cout << "響應資料: " << response_buffer.command_string_a << std::endl;
  }

  closesocket(client_socket);
  WSACleanup();
  return 0;
}

執行上述程式碼片段,讀者可看到如下圖所示的輸出資訊;

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

相關文章