TCP數據粘包的處理

TCP數據粘包的處理

  • 背鍋俠TCP
  • 解決方案
    • 2.1 發送端
    • 2.2 接收端

背鍋俠TCP

在前面介紹套接字通信的時候說到了TCP是傳輸層協議,它是一個面向連接的、安全的、流式傳輸協議。因為數據的傳輸是基于流的所以發送端和接收端每次處理的數據的量,處理數據的頻率可以不是對等的,可以按照自身需求來進行決策。

TCP協議是優勢非常明顯,但是有時也會給我們造成困擾,正所謂:成也蕭何敗蕭何。假設我們有如下需求:

客戶端和服務器之間要進行基于TCP的套接字通信

  • 通信過程中客戶端會每次會不定期給服務器發送一個不定長度的有特定含義的字符串。
  • 通信的服務器端每次都需要接收到客戶端這個不定長度的字符串,并對其進行解析

根據上面的描述,服務器在接收數據的時候有如下幾種情況:

  • 一次接收到了客戶端發送過來的一個完整的數據包
  • 一次接收到了客戶端發送過來的N個數據包,由于每個包的長度不定,無法將各個數據包拆開
  • 一次接收到了一個或者N個數據包 + 下一個數據包的一部分,還是很悲劇,無法將數據包拆開
  • 一次收到了半個數據包,下一次接收數據的時候收到了剩下的一部分+下個數據包的一部分,更悲劇,頭大了
  • 另外,還有一些不可抗拒的因素:比如客戶端和服務器端的網速不一樣,發送和接收的數據量也會不一致

對于以上描述的現象很多時候我們將其稱之為TCP的粘包問題但是這種叫法不太對的,本身TCP就是面向連接的流式傳輸協議,特性如此,我們卻說是TCP這個協議出了問題,這只能說是使用者的無知。多個數據包粘連到一起無法拆分是我們的需求過于復雜造成的,是程序猿的問題而不是協議的問題,TCP協議表示這鍋它不想背。

現在問題來了,服務器端如果想保證每次都能接收到客戶端發送過來的這個不定長度的數據包,程序猿應該如何解決這個問題呢?下面給大家提供幾種解決方案:

  1. 使用標準的應用層協議(比如:http、https)來封裝要傳輸的不定長的數據包
  2. 在每條數據的尾部添加特殊字符, 如果遇到特殊字符, 代表當條數據接收完畢了
    • 有缺陷: 效率低, 需要一個字節一個字節接收, 接收一個字節判斷一次, 判斷是不是那個特殊字符串
  3. 在發送數據塊之前, 在數據塊最前邊添加一個固定大小的數據頭, 這時候數據由兩部分組成:數據頭+數據塊
    • 數據頭:存儲當前數據包的總字節數,接收端先接收數據頭,然后在根據數據頭接收對應大小的字節
    • 數據塊:當前數據包的內容

解決方案

如果使用TCP進行套接字通信,如果發送的數據包粘連到一起導致接收端無法解析,我們通常使用添加包頭的方式輕松地解決掉這個問題。關于數據包的包頭大小可以根據自己的實際需求進行設定,這里沒有啥特殊需求,因此規定包頭的固定大小為4個字節,用于存儲當前數據塊的總字節數。

在這里插入圖片描述

2.1 發送端

對于發送端來說,數據的發送分為4步:

  1. 根據待發送的數據長度N動態申請一塊固定大小的內存:N+4(4是包頭占用的字節數)
  2. 將待發送數據的總長度寫入申請的內存的前四個字節中,此處需要將其轉換為網絡字節序(大端)
  3. 待發送的數據拷貝到包頭后邊的地址空間中,將完整的數據包發送出去(字符串沒有字節序問題)
  4. 釋放申請的堆內存。

由于發送端每次都需要將這個數據包完整的發送出去,因此可以設計一個發送函數,如果當前數據包中的數據沒有發送完就讓它一直發送,處理代碼如下:

/*
函數描述: 發送指定的字節數
函數參數:- fd: 通信的文件描述符(套接字)- msg: 待發送的原始數據- size: 待發送的原始數據的總字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
*/
int writen(int fd, const char* msg, int size)
{const char* buf = msg;int count = size;while (count > 0){int len = send(fd, buf, count, 0);if (len == -1){close(fd);return -1;}else if (len == 0){continue;}buf += len;count -= len;}return size;
}

有了這個功能函數之后就可以發送帶有包頭的數據塊了,具體處理動作如下:

/*
函數描述: 發送帶有數據頭的數據包
函數參數:- cfd: 通信的文件描述符(套接字)- msg: 待發送的原始數據- len: 待發送的原始數據的總字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
*/
int sendMsg(int cfd, char* msg, int len)
{if(msg == NULL || len <= 0 || cfd <=0){return -1;}// 申請內存空間: 數據長度 + 包頭4字節(存儲數據長度)char* data = (char*)malloc(len+4);int bigLen = htonl(len);memcpy(data, &bigLen, 4);memcpy(data+4, msg, len);// 發送數據int ret = writen(cfd, data, len+4);// 釋放內存free(data);return ret;
}

關于數據的發送最后再次強調:字符串沒有字節序問題,但是數據頭不是字符串是整形,因此需要從主機字節序轉換為網絡字節序再發送。

完整的放在一起如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>/*
函數描述: 發送指定的字節數
函數參數:- fd: 通信的文件描述符(套接字)- msg: 待發送的原始數據- size: 待發送的原始數據的總字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
msg是要發送的字符串指針,size是要發送的字符串的長度
再次while循環的時候,已經發送了len長度,指針后移len長度,發送的字符串長度也減len
*/
int writen(int fd, const char* msg, int size)
{const char* buf = msg;int count = size;while (count > 0){int len = send(fd, buf, count, 0);if (len == -1)   // 表示發送出錯,關閉文件描述符并返回-1。{close(fd);return -1;}else if (len == 0)  // 表示沒有發送任何數據{continue;}buf += len;count -= len;}return size;
}/*
函數描述: 發送帶有數據頭的數據包
函數參數:- cfd: 通信的文件描述符(套接字)- msg: 待發送的原始數據- len: 待發送的原始數據的總字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
*/
int sendMsg(int cfd, char* msg, int len)
{if(msg == NULL || len <= 0 || cfd <=0){return -1;}// 申請內存空間: 數據長度 + 包頭4字節(存儲數據長度)char* data = (char*)malloc(len+4);int bigLen = htonl(len);memcpy(data, &bigLen, 4);memcpy(data+4, msg, len);// 發送數據int ret = writen(cfd, data, len+4);// 釋放內存free(data);return ret;
}

2.2 接收端

了解了套接字的發送端如何發送數據,接收端的處理步驟也就清晰了,具體過程如下:

  1. 首先接收4字節數據,并將其從網絡字節序轉換為主機字節序,這樣就得到了即將要接收的數據的總長度
  2. 根據得到的長度申請固定大小的堆內存,用于存儲待接收的數據
  3. 根據得到的數據塊長度接收固定數目的數據保存到申請的堆內存中
  4. 處理接收的數據
  5. 釋放存儲數據的堆內存

從數據包頭解析出要接收的數據長度之后,還需要將這個數據塊完整的接收到本地才能進行后續的數據處理,因此需要編寫一個接收數據的功能函數,保證能夠得到一個完整的數據包數據,處理函數實現如下:

/*
函數描述: 接收指定的字節數
函數參數:- fd: 通信的文件描述符(套接字)- buf: 存儲待接收數據的內存的起始地址- size: 指定要接收的字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
*/
int readn(int fd, char* buf, int size)
{char* pt = buf;int count = size;while (count > 0){int len = recv(fd, pt, count, 0);if (len == -1){return -1;}else if (len == 0){return size - count;}pt += len;count -= len;}return size;
}

這個函數搞定之后,就可以輕松地接收帶包頭的數據塊了,接收函數實現如下:

/*
函數描述: 接收帶數據頭的數據包
函數參數:- cfd: 通信的文件描述符(套接字)- msg: 一級指針的地址,函數內部會給這個指針分配內存,用于存儲待接收的數據,這塊內存需要使用者釋放
函數返回值: 函數調用成功返回接收的字節數, 發送失敗返回-1
*/
int recvMsg(int cfd, char** msg)
{// 接收數據// 1. 讀數據頭int len = 0;readn(cfd, (char*)&len, 4);len = ntohl(len);printf("數據塊大小: %d\n", len);// 根據讀出的長度分配內存,+1 -> 這個字節存儲\0char *buf = (char*)malloc(len+1);int ret = readn(cfd, buf, len);if(ret != len){close(cfd);free(buf);return -1;}buf[len] = '\0';*msg = buf;return ret;
}

這樣,在進行套接字通信的時候通過調用封裝的sendMsg()和recvMsg()就可以發送和接收帶數據頭的數據包了,而且完美地解決了粘包的問題。

完整的放在一起如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>/*
函數描述: 接收指定的字節數
函數參數:- fd: 通信的文件描述符(套接字)- buf: 存儲待接收數據的內存的起始地址- size: 指定要接收的字節數
函數返回值: 函數調用成功返回發送的字節數, 發送失敗返回-1
*/
int readn(int fd, char* buf, int size)
{char* pt = buf;int count = size;while (count > 0){int len = recv(fd, pt, count, 0);if (len == -1)  // -1:接收數據失敗了{return -1;}else if (len == 0)  //等于0:對方斷開了連接{return size - count;}pt += len;count -= len;}return size;
}/*
函數描述: 接收帶數據頭的數據包
函數參數:- cfd: 通信的文件描述符(套接字)- msg: 一級指針的地址,函數內部會給這個指針分配內存,用于存儲待接收的數據,這塊內存需要使用者釋放
函數返回值: 函數調用成功返回接收的字節數, 發送失敗返回-1
*/
int recvMsg(int cfd, char** msg)
{// 接收數據// 1. 讀數據頭int len = 0;readn(cfd, (char*)&len, 4);len = ntohl(len);printf("數據塊大小: %d\n", len);// 根據讀出的長度分配內存,+1 -> 這個字節存儲\0char *buf = (char*)malloc(len+1);int ret = readn(cfd, buf, len);if(ret != len){close(cfd);free(buf);return -1;}buf[len] = '\0';*msg = buf;return ret;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/214450.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/214450.shtml
英文地址,請注明出處:http://en.pswp.cn/news/214450.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Qt練習題

1.使用手動連接&#xff0c;將登錄框中的取消按鈕使用qt4版本的連接到自定義的槽函數中&#xff0c;在自定義的槽函數中調用關閉函數 將登錄按鈕使用qt5版本的連接到自定義的槽函數中&#xff0c;在槽函數中判斷ui界面上輸入的賬號是否為"admin"&#xff0c;密碼是否…

代碼隨想錄 96. 不同的二叉搜索樹

題目 給你一個整數 n &#xff0c;求恰由 n 個節點組成且節點值從 1 到 n 互不相同的 二叉搜索樹 有多少種&#xff1f;返回滿足題意的二叉搜索樹的種數。 示例 1&#xff1a; 輸入&#xff1a;n 3 輸出&#xff1a;5 示例 2&#xff1a; 輸入&#xff1a;n 1 輸出&#xff1…

【Angular開發】Angular 16發布:發現前7大功能

Angular 于2023年5月3日發布了主要版本升級版Angular 16。作為一名Angular開發人員&#xff0c;我發現這次升級很有趣&#xff0c;因為與以前的版本相比有一些顯著的改進。 因此&#xff0c;在本文中&#xff0c;我將討論Angular 16的前7個特性&#xff0c;以便您更好地理解。…

機器學習基礎介紹

百度百科&#xff1a; 機器學習是一門多領域交叉學科&#xff0c;涉及概率論、統計學、逼近論、凸分析、算法復雜度理論等多門學科。專門研究計算機怎樣模擬或實現人類的學習行為&#xff0c;以獲取新的知識或技能&#xff0c;重新組織已有的知識結構使之不斷改善自身的性能。 …

手工酸奶店如何選址?開在哪里比較合適?

手工酸奶店是一個非常受歡迎的創業項目&#xff0c;但想要成功開店&#xff0c;選址是非常重要的。 本人開酸奶店5年時間&#xff0c;下面我將為大家分享一些選址的小技巧&#xff0c;希望對大家有所幫助。&#xff08;可以點贊收藏&#xff0c;方便以后隨時查閱&#xff09; …

入職字節外包一個月,我離職了。。。

有一種打工人的羨慕&#xff0c;叫做“大廠”。 真是年少不知大廠香&#xff0c;錯把青春插稻秧。 但是&#xff0c;在深圳有一群比大廠員工更龐大的群體&#xff0c;他們頂著大廠的“名”&#xff0c;做著大廠的工作&#xff0c;還可以享受大廠的伙食&#xff0c;卻沒有大廠…

12.11 C++ 作業

完善對話框&#xff0c;點擊登錄對話框&#xff0c;如果賬號和密碼匹配&#xff0c;則彈出信息對話框&#xff0c;給出提示”登錄成功“&#xff0c;提供一個Ok按鈕&#xff0c;用戶點擊Ok后&#xff0c;關閉登錄界面&#xff0c;跳轉到其他界面 如果賬號和密碼不匹配&#xf…

樹根研習社|數據為王,洞察“工業數據采集”背后的價值與實踐

一、工業數據采集是什么&#xff1f; 數據采集是將各種信息傳感設備通過網絡結合起來&#xff0c;實現任何時間、任何地點&#xff0c;人、機、物的互聯互通。數據采集的主要的作用是&#xff1a; “翻譯官”&#xff1a;不同程序語言的設備數據通過協議解析“翻譯”為上層系…

淘寶權益玩法平臺的Serverless化實踐

通過對權益玩法平臺現有業務應用的Serverless化改造&#xff0c;權益團隊在雙十一期間完美地支撐了業務需求&#xff0c;在研發效率、運維保障等方面都體現出了很高的價值和收益。 項目背景 淘寶權益平臺是負責淘寶權益營銷的核心團隊&#xff0c;團隊除了負責拉菲權益平臺外&a…

1.cloud-微服務架構編碼構建

1.微服務cloud整體聚合父工程 1.1 New Project 1.2 Maven選版本 1.3 字符編碼 1.4 注解生效激活 主要為lombok中的Data 1.5 java編譯版本選8 1.6 File Type過濾 *.hprof;*.idea;*.iml;*.pyc;*.pyo;*.rbc;*.yarb;*~;.DS_Store;.git;.hg;.svn;CVS;__pycache__;_svn;vssver.scc;v…

Nginx配置文件的基本用法

Nginx簡介 1.1概述 Nginx是一個高性能的HTTP和反向代理服務器。 是一款輕量級的高性能的web服務器/反向代理服務器/電子郵件&#xff08;IMAP/POP3&#xff09;代理服務器 單臺物理服務器可支持30 000&#xff5e;50 000個并發請求。 1.2Nginx和Apache的優缺點 &#xff…

mybatis數據輸出-insert操作時獲取自增列的值給對應的屬性賦值

jdbc-修改 水果庫存系統的 BaseDao 的 executeUpdate 方法支持返回自增列-CSDN博客 1、建庫建表 CREATE DATABASE mybatis-example;USE mybatis-example;CREATE TABLE t_emp(emp_id INT AUTO_INCREMENT,emp_name CHAR(100),emp_salary DOUBLE(10,5),PRIMARY KEY(emp_id) );INSE…

王炸升級!PartyRock 10分鐘構建 AI 應用

前言 一年一度的亞馬遜云科技的 re:Invent 可謂是全球云計算、科技圈的狂歡&#xff0c;每次都能帶來一些最前沿的方向標&#xff0c;這次也不例外。在看完一些 keynote 和介紹之后&#xff0c;我也去親自體驗了一些最近發布的內容。其中讓我感受最深刻的無疑是 PartyRock 了。…

基于SSM的健身房預約系統設計與實現

末尾獲取源碼 開發語言&#xff1a;Java Java開發工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 數據庫&#xff1a;MySQL5.7和Navicat管理工具結合 服務器&#xff1a;Tomcat8.5 開發軟件&#xff1a;IDEA / Eclipse 是否Maven項目&#xff1a;是 目錄…

網絡安全攻擊預警/態勢預測算法匯總

總結&#xff1a; 網絡安全攻擊預警/態勢預測算法眾多&#xff0c;主要包括&#xff1a; 基于統計學的算法&#xff1a;協方差矩陣、馬爾可夫模型等&#xff1b; 基于機器學習的算法&#xff1a;貝葉斯網絡、聚類算法、支持向量機SVM、遺傳算法、層次分析法AHP、決策樹等&am…

每日一道算法題 1

借鑒文章&#xff1a;Java-敏感字段加密 - 嗶哩嗶哩 題目描述 給定一個由多個命令字組成的命令字符串&#xff1b; 1、字符串長度小于等于127字節&#xff0c;只包含大小寫字母&#xff0c;數字&#xff0c;下劃線和偶數個雙引號 2、命令字之間以一個或多個下劃線_進行分割…

Proxmark3 Easy救磚-20231209

事情是這樣的&#xff0c;在淘寶買了個PM3&#xff0c;拿到手后刷固件的&#xff0c;一不小心刷成磚頭了&#xff0c;現象就是四個燈全亮&#xff0c;插上電腦USB不識別。問商家他也不太懂&#xff0c;也是個半吊子技術&#xff0c;遠程給我刷機搞了半天也沒有搞定&#xff0c;…

微表情檢測(三)----基于光流特征的微表情檢測

Micro-expression spotting based on optical flow features 基于光流特征的微表情檢測 Abstract 本文提出了一種高精度和可解釋性的自動微表情檢測方法。首先&#xff0c;我們設計了基于鼻尖位置的圖像對齊方法&#xff0c;以消除由頭部晃動引起的全局位移。其次&#xff0…

C語言中的一維數組與二維數組

目錄 一維數組數組的創建初始化使用在內存中的存儲 二維數組創建初始化使用在內存中的存儲 數組越界 一維數組 數組的創建 數組是一組相同類型元素的集合。 int arr1[10]; char arr3[10]; float arr4[10]; double arr5[10];下面這個數組能否成功創建&#xff1f; int count…

Linux上編譯和測試V8引擎源碼

介紹 V8引擎是一款高性能的JavaScript引擎&#xff0c;廣泛應用于Chrome瀏覽器和Node.js等項目中。在本篇博客中&#xff0c;我們將介紹如何在Linux系統上使用depot_tools工具編譯和測試V8引擎源碼。 步驟一&#xff1a;安裝depot_tools depot_tools是一個用于Chromium開發…