14.8 Socket 一收一發通訊

2023-10-16 21:00:47

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

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 許可協定。轉載請註明出處!