14.8 Socket 一收一發通信

来源:https://www.cnblogs.com/LyShark/archive/2023/10/16/17768108.html
-Advertisement-
Play Games

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


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

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

文章作者:lyshark (王瑞)
文章出處:https://www.cnblogs.com/LyShark/p/17768108.html
本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當談到架構的高可用時,無論是高可用計算架構,還是高可用存儲架構,其本質的設計目的都是為瞭解決部分伺服器故障的場景下,如何保證系統能夠繼續提供服務。但在一些極端場景下,有可能所有伺服器都出現故障。例如,典型的有機房斷電、機房火災、地震、水災……這些極端情況會導致某個系統所有伺服器都故障,或者業務整體癱 ...
  • 複製字典 您不能簡單地通過輸入 dict2 = dict1 來複制一個字典,因為 dict2 只會成為 dict1 的引用,對 dict1 的更改也會自動應用於 dict2。 有多種方法可以複製字典,一種方法是使用內置的 copy() 方法。 示例,使用 copy() 方法製作字典的副本: this ...
  • MySQL欄位的時間類型該如何選擇?千萬數據下性能提升10%~30%🚀 前言 在MySQL中時間類型的選擇有很多,比如:date、time、year、datetime、timestamp... 在某些情況下還會使用整形int、bigint來存儲時間戳 根據節省空間的原則,當只需要存儲年份、日期、時 ...
  • 本文介紹基於Python中matplotlib模塊與seaborn模塊,利用多個列表中的數據,繪製小提琴圖(Violin Plot)的方法~ ...
  • 正文 直接說答案,這個問題無法實現。原因是因為std::vector容器的插入一定會調用類對象的構造函數或者移動構造函數。 說一下為什麼會有這個問題,因為不想用指針,我想直接通過類對象本身的RAII機制來實現的資源的控制,智能指針是一個解決方案,不過智能指針是寫起來很繁瑣,終究比不上值類型方便。不過 ...
  • 修改字典項 您可以通過引用其鍵名來更改特定項的值: 示例,將 "year" 更改為 2018: thisdict = { "brand": "Ford", "model": "Mustang", "year": 1964 } thisdict["year"] = 2018 更新字典 update() ...
  • 草船借箭 題目: 題目描述: 程式員小周同學這幾天在看《三國演義》。今天他看到了“草船借箭”這一回,在欽佩諸葛亮巧借東風向曹操“借"箭的同 時,小周想到這麼一個問題: 如果諸葛亮一共派出了N條放置草人的船來“借"箭。“悚慨”的曹操向第1條草船上射了A支 箭、第2條草船上射了B支箭,第3條草船上射的箭 ...
  • 寫這篇文章的主要原因是工作中需要寫一個用訓練好的模型批量生圖的腳本,開始是想用python直接載入模型,但後來發現webui的界面中有不少好用的插件和參數,所以最終改成調用WebUI介面的方式來批量生圖。 Stable-diffusion的webui界面使用比較方便,但是它的api文檔比較簡陋,很多 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...