Socket编程权威指南(二)完美掌握TCP流式协议及Socket编程的recv()和send()


在上一篇文章中,我们学习了Socket编程的基础知识,包括创建Socket、绑定地址、监听连接、接收连接等操作。然而,真正的套接字编程远不止于此。本文将重点介绍TCP 流式协议,什么是粘包问题?如何解决粘包问题 ?以及recv()和send()这两个函数详细介绍,它们分别用于读取和发送数据,是网络编程中最为关键的环节。我们将详细剖析函数原型、参数含义,并通过实例代码展示具体用法,助你彻底掌握数据传输的精髓。


想详细了解socket 编程基础知识的同学,请前往查阅Socket编程权威指南(一)打通网络通信的任督二脉。


我们先来回顾一下 socket 编程的基本流程:

在这里插入图片描述


接下来,开始我们今天的正题。

一、TCP 流式协议


1、TCP 流式协议介绍

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

以下是 TCP 作为流式协议的一些关键特性和详细说明:

  • 面向连接

    • TCP 需要在数据传输开始前建立一个连接。通过三次握手过程,客户端和服务器交换初始序列号,建立稳定的连接。
  • 字节流

    • 与数据报(如 UDP)不同,TCP 将数据视为字节流,而不是独立的数据包。这意味着 TCP 不保留数据包边界,应用程序需要自己处理数据的边界。
  • 可靠性

    • TCP 保证数据的可靠传输。它使用序列号和确认应答机制来确保数据按顺序、完整地到达目的地。
  • 有序性

    • TCP 保证数据包的有序传输。如果数据包丢失或乱序,TCP 会重传丢失的数据包并重新排序。
  • 拥塞控制

    • TCP 内置拥塞控制机制,通过调整数据的发送速率来避免网络拥塞。这是通过算法如慢启动、拥塞避免、快重传和快恢复实现的。
  • 流量控制

    • TCP 使用滑动窗口机制进行流量控制,接收方根据自己的接收能力告知发送方可以发送多少数据。
  • 全双工通信

    • TCP 允许双向通信,即客户端和服务器可以独立地发送和接收数据。
  • 数据传输

    • TCP 通过套接字接口提供数据传输服务。应用程序可以使用 read()write() 或专门的套接字系统调用如 recv()send() 进行数据传输。
  • 连接管理

    • TCP 连接的建立和终止通过三次握手和四次挥手过程管理。这些过程确保了连接的稳定建立和优雅关闭。
  • 超时和重传

    • 如果发送的数据没有得到及时确认,TCP 会自动重传数据,并且随着时间的推移,重传间隔会指数增长。
  • 保活和死锁检测

    • TCP 提供保活(keepalive)机制,以检测死连接。如果连接长时间没有活动,TCP 可以自动关闭连接。
  • 适用场景

    • TCP 适用于需要可靠传输的应用,如 Web 浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)和远程登录(SSH)。
  • 限制

    • 由于 TCP 是面向连接的协议,它在某些场景下可能不如 UDP 高效,特别是在实时通信或广播通信中。
  • 编程模型

    • TCP 编程通常涉及创建套接字、绑定、监听、连接、数据传输和关闭连接等步骤。

TCP 作为流式协议,其设计目标是提供可靠的数据传输服务。它通过多种机制确保数据的正确、有序传输,并通过拥塞控制和流量控制适应不同的网络条件。


也就是说,内核实现的
TCP 协议将保证将一个
TCP 包发送给另一端。

在这里插入图片描述

如上图:

拥塞控制是确保可靠数据传输协议有效运作的关键组成部分,因此,在TCP中,发送缓冲区和接收缓冲区成为了必不可少的元素。

在标准的Linux操作系统中,TCP的发送缓冲区和接收缓冲区默认的大小通常被设置为208KB。这意味着,如果进程A没有及时从其接收缓冲区中提取数据,那么传入的数据将继续在缓冲区内积累,直至达到其容量上限。


2、TCP 流式协议引发“粘包问题”

在网络上传输的数据本质上是二进制格式的,这意味着接收缓冲区里存放的内容同样是以二进制数据的形式存在。

数据根据其到达的顺序被依次放入接收缓冲区,这就可能导致所谓的“粘包”现象。

“粘包”这个词更像是一个方便描述的概念,实际上它并非一个正式的术语。

这个说法类似于描述从水龙头流出的水“粘”在一起,这当然不是字面上的粘连,而是用来形象地表达数据包在缓冲区中连续存放的情况。

简而言之,流式协议引发的一个问题是,我们如何识别接收缓冲区中的数据分别属于哪个TCP数据包?


在这里插入图片描述

以一个具体示例来说明,如果客户端向服务器发送了三个TCP数据包,分别包含内容QA、QB、QC,每个内容代表一个不同的请求。那么在服务器端的接收缓冲区(RecvBuffer)中,这些数据可能会连续排列成一串数据QAQBQC。服务器需要将这串连续的数据正确地拆分成三个独立的数据包,并对每个数据包进行相应的处理。


3、如何解决“粘包”问题?

通常,为了解决如何从接收缓冲区中区分不同TCP数据包的问题,我们会采用 包头+包体 的方式 。


在这里插入图片描述


正如上图所演示:在每个数据包前添加一个固定长度的包头,并在包头中记录数据包的总长度。包体部分则承载实际要传输的数据。

处理接收缓冲区数据的步骤如下:

  • Step 1: 首先从接收缓冲区读取固定大小的包头(例如20字节)。

  • Step 2: 解析包头,从中获取数据包的总长度,这里假设包头中包含的数据长度字段名为Header.Length

  • Step 3 : 根据Header.Length的值,确定接下来需要从接收缓冲区读取的数据量。

    例如,如果包头之后的数据总长度为1048字节,减去已读取的20字节包头,还需读取1028字节的数据。


通过这种方式,即使数据在接收缓冲区中是连续存放的,服务器也能够根据每个数据包的包头信息,正确地拆分和识别出独立的数据包。


二、recv()函数详解


recv() 函数是专用于
scoket 上的
read 操作,用于接收 TCP 套接字数据的系统调用。它在服务器端或客户端程序中用来从连接的对端读取数据,本质上是从接收缓冲区读取数据 ,该系统调用将返回实际读取的字节数,其值可能会小于传入的
length 参数。


以下是 recv() 函数的详细说明:

(1)、函数原型

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

(2)、参数

  • sockfd:套接字描述符,表示要从中读取数据的 TCP 套接字。

  • buf:指向一个缓冲区的指针,用于存储接收到的数据。

  • len:缓冲区的大小,即 buf 可以存储的最大字节数。

  • flags:用来修改recv()行为的选项。常用的值包括:

    • 0:正常接收数据。

    • MSG_PEEK:窥视接收的数据,不从接收缓冲区中移除数据。

    • MSG_WAITALL:等待直到接收到 len 个字节的数据,或者出现错误。


(3)、返回值

  • 成功时,返回接收到的字节数,该值通常小于或等于 len

  • 失败时,返回 -1,并设置全局变量 errno 以指示错误类型。


(4)、错误处理

  • EAGAINEWOULDBLOCK:在非阻塞模式下,如果操作会阻塞,则返回此错误。
  • ECONNRESET:远程主机强行关闭了一个现有的连接。
  • ENOTCONN:套接字未连接,即未调用 connect()accept()
  • ENOTSOCKsockfd 不是一个套接字。

(5)、使用场景

  • 主要用于 TCP 套接字上的数据接收。对于 UDP 套接字,通常使用 recvfrom() 函数。

(6)、阻塞和非阻塞行为

  • 默认情况下,recv() 是阻塞的,它会等待直到至少接收到一个字节的数据。
  • 对于非阻塞套接字,如果接收缓冲区中没有数据,recv() 会立即返回,返回值为 0。

(7)、与 read() 的区别

  • read() 是一个通用的系统调用,用于读取文件描述符,而 recv() 专门用于套接字。

  • recv() 可以处理套接字选项和状态,而 read() 不能。


(8)、示例代码

接下来我们通过一个简单的客户端示例,演示如何使用recv()接收数据:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");servaddr.sin_port = htons(8000);connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));char buffer[1024];ssize_t nbytes = recv(sockfd, buffer, sizeof(buffer), 0);buffer[nbytes] = '\0';std::cout << "Received: " << buffer << std::endl;close(sockfd);return 0;
}

在这个例子中,客户端连接到本机的8000端口,然后使用recv()接收数据并输出到控制台。你可以自行运行并在服务器端发送数据,以验证recv()的效果。


(9)、注意事项:

  • 接收到的数据可能小于请求的长度,因此应用程序应该总是检查返回值。
  • recv() 可以与 select()poll() 结合使用,以实现更复杂的非阻塞操作。

recv() 函数是 TCP 网络编程中接收数据的基本工具。正确使用它需要考虑套接字的状态、缓冲区的大小以及可能的错误情况。


三、send()函数解析


send() 函数专用于
scoket 上的
write
操作,在套接字上发送数据的系统调用。

本质上是向发送缓冲区中写入数据,内核在发送
TCP 数据时,通常会使用
Nagle
算法把多个小的数据包合并成一个发送给另一端,以提高效率。

它是 TCP 网络编程中发送数据的基本工具之一。

以下是 send() 函数的详细解析:


(1)、函数原型

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

(2)、参数

  • sockfd:套接字描述符,表示要从中发送数据的套接字。
  • buf:指向要发送数据缓冲区的指针。
  • len:要发送数据的长度,单位为字节。
  • flags:用来修改发送行为的选项。常用的值包括:
    • 0:正常发送数据。
    • MSG_DONTWAIT:使 send() 调用非阻塞。
    • MSG_MORE:暗示更多的数据要发送,可以用于优化传输效率。

(3)、返回值

  • 成功时,返回已发送的字节数,该值通常小于或等于 len
  • 失败时,返回 -1,并设置全局变量 errno 以指示错误类型。

(4)、错误处理

  • EAGAINEWOULDBLOCK:在非阻塞模式下,如果操作会阻塞,则返回此错误。

  • ECONNRESET:远程主机强行关闭了连接。

  • ENOTCONN:套接字未连接到任何远程主机。

  • ENOTSOCKsockfd 不是一个套接字。


(5)、使用场景

  • 主要用于已连接的 TCP 套接字上的数据发送。对于 UDP 套接字,通常使用 sendto() 函数。


(6)、阻塞和非阻塞行为

  • 默认情况下,send() 是阻塞的,它会等待直到数据被发送。

  • 对于非阻塞套接字,如果数据不能立即发送,send() 会返回 -1 并设置 errnoEAGAINEWOULDBLOCK


(7)、与 write() 的区别

  • write() 是一个通用的系统调用,用于写文件描述符,而 send() 专门用于套接字。

  • send() 可以处理套接字选项和状态,而 write() 不能。


(8)、示例代码

const char* message = "Hello, World!";
ssize_t nbytes = send(sockfd, message, strlen(message), 0);
if (nbytes < 0) {perror("send failed");exit(EXIT_FAILURE);
}

(9)、注意事项

  • 发送的数据可能因为网络或其他原因没有立即发送出去,send() 只是将数据交给了内核。
  • 对于非阻塞套接字,可以使用 select()poll() 来等待套接字变得可写。

send() 函数是 TCP 网络编程中发送数据的基础。正确使用它需要考虑套接字的状态、数据的长度以及可能的错误情况。此外,send()recv() 配合使用,可以实现完整的数据发送和接收流程。

需要注意的是,recv()和send()都可能出现中断或部分接收/发送的情况,因此在实际应用中需要循环调用,确保所有数据都被完整传输。此外,它们还有一些特殊的flags可以控制阻塞行为等,具体用法视实际情况而定。


四、权威之作待续


恭喜你已经完整学习了TCP 流式协议,什么是粘包?粘包如何处理?以及recv()和send()函数,这标志着你正在向Socket编程高手的行列迈进。当然,Socket编程远不止如此,还有select()、poll()等I/O复用函数、Unix域Socket编程、原始套接字编程等更高阶的技巧等待你去发掘。我们期待在不久的将来,为你呈现一部Socket编程的不朽权威之作。敬请期待!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/24576.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用R包“Phenotype”对表型值进行检查

首先&#xff0c;你需要确保你已经安装了R和RStudio&#xff08;如果你想用RStudio的话&#xff09;。然后&#xff0c;你可以按照以下步骤进行操作&#xff1a; 加载数据&#xff1a;首先&#xff0c;你需要加载你的表型数据。如果你的数据是以CSV、Excel等格式保存的&#x…

地面站Mission planner

官方教程; Mission Planner地面站介绍 | Autopilot (gitbook.io) Mission Planner 功能/屏幕 — Mission Planner 文档 (ardupilot.org) 安卓或者windows软件下载地址&#xff1a; 地面站连接及使用 plane (cuav.net) 在完全装机后再进行各干器件的校准&#xff0c;没有组…

【LeetCode 前缀和 + 哈希表】LC_560_和为K的子数组

文章目录 1. 和为K的子数组&#x1f197; 1. 和为K的子数组&#x1f197; 题目链接&#x1f517; &#x1f427;解题思路&#xff1a; 前缀和 哈希表 &#x1f34e; 设i为数组中的任意位置&#xff0c;⽤ sum[i] 表⽰ [0, i] 区间内所有元素的和。 &#x1f34e; 想知道有…

【设计模式深度剖析】【5】【行为型】【迭代器模式】

&#x1f448;️上一篇:策略模式 设计模式-专栏&#x1f448;️ 文章目录 迭代器模式定义英文原话直译如何理解呢&#xff1f; 迭代器模式的角色1. Iterator&#xff08;迭代器&#xff09;2. ConcreteIterator&#xff08;具体迭代器&#xff09;3. Aggregate&#xff08;聚…

用 DataGridView 控件显示数据

使用DataGridView&#xff0c;可以很方便显示数据。 &#xff08;1&#xff09;Visual Studio版本&#xff1a;Visual Studio 2022 &#xff08;2&#xff09;应用程序类型&#xff1a;windows form &#xff08;3&#xff09;编程语言&#xff1a;C# 一、目标框架 .NET Fra…

K-BAT01,K-CU01和利时卡件

K-BAT01,K-CU01和利时卡件。现场控制站下装与在线调试。9二、组态流程&#xff1a;操作站组态控制站组态新建工程控制站用户组态历史站组态下装现场控制站下装历史站下装操作员站10三、组态详解&#xff1a;1、K-BAT01,K-CU01和利时卡件。新建工程&#xff1a;打开工程总控&…

【Unity】Kafka、Mqtt、Wesocket通信

1 前言 最近研究了下kafka、mqtt、webocket插件在Unity网络通信中的应用&#xff0c;做下小总结吧。&#xff08;不想写笔记&#xff0c;但不写又会忘&#xff0c;痛苦&#xff09; 2 Kafka 先说结果&#xff1a;Kafka实现失败。 我会使用的方法是在VS里安装了Confluent.Kafka…

项目3:从0开始的RPC框架(扩展版)

一. 全局配置加载 1. 需求分析 通常情况下&#xff0c;在RPC框架运行的会涉及到多种配置信息&#xff0c;比如注册中心的地址、序列化方式、网络服务端接口号等。 在简易版框架中&#xff0c;硬编码了这些配置&#xff0c;也就是都写死了&#xff0c;在真实的应用环境中是不…

【启程Golang之旅】让文件操作变得简单

欢迎来到Golang的世界&#xff01;在当今快节奏的软件开发领域&#xff0c;选择一种高效、简洁的编程语言至关重要。而在这方面&#xff0c;Golang&#xff08;又称Go&#xff09;无疑是一个备受瞩目的选择。在本文中&#xff0c;带领您探索Golang的世界&#xff0c;一步步地了…

MySQL 与 PostgreSQL 关键对比二(SQL语法)

目录 1 详细示例 1.1自动增量列 1.2 字符串连接 1.3 JSON 支持 2 总结 MySQL 和 PostgreSQL 是两种流行的开源关系数据库管理系统&#xff08;RDBMS&#xff09;。尽管它们在许多方面相似&#xff0c;但在 SQL 语法和功能上存在一些显著差异。 以下SQL语句的执行如果需要开…

1. lvs负载均衡

lvs负载均衡 一、集群技术概述1、集群技术类型2、负载均衡技术3、高可用技术 二、负载均衡 LVS1、LVS介绍2、负载均衡策略/算法3、LVS设计模式3.1 NAT模式的注意事项3.2 DR 直接路由模式的注意事项 三、LVS nat模式的实现1、确认后端服务器网关正确2、安装ipvsadm软件3、开启路…

[AIGC] SpringBoot的自动配置解析

下面是一篇关于SpringBoot自动配置的文章&#xff0c;里面包含了一个简单的示例来解释自动配置的原理。 SpringBoot的自动配置解析 Spring Boot是Spring的一个子项目&#xff0c;用于快速开发应用程序。它主要是简化新Spring应用的初始建立以及开发过程。其中&#xff0c;自动…

java第二十一课 —— 快捷键,包,访问修饰符

IDEA 快捷键 删除行&#xff1a;Ctrl Y复制行&#xff1a;Ctrl D补全代码&#xff1a;Alt /添加取消注释&#xff1a;Ctrl /导入该行需要的类&#xff1a;Alt Enter快速格式化代码&#xff1a;Ctrl Shift L快速运行程序&#xff1a;Ctrl Shift F10生成构造器&#xf…

直播商城源码-PC+APP+H5+小程序现成源码

随着电商行业的不断演进&#xff0c;直播商城已成为连接消费者和商品的新兴桥梁。直播商城源码提供了一个完整的解决方案&#xff0c;使得企业能够迅速搭建起一个覆盖PC、APP、H5和小程序的全渠道电商平台。本文将探讨直播商城源码的优势、关键功能以及如何选择适合的现成源码。…

如何搭建一个高效的帮助网站?

在当今数字化时代&#xff0c;拥有一个高效的帮助网站对于企业来说至关重要。无论是为了提供产品支持、解答常见问题&#xff0c;还是为了增强用户体验和品牌形象&#xff0c;一个贴心、专业的帮助网站都能为企业赢得更多的用户和客户。那么&#xff0c;如何才能搭建一个高效的…

设置电脑定时关机

1.使用快捷键winR 打开运行界面 2.输入cmd &#xff0c;点击确认&#xff0c;打开命令行窗口&#xff0c;输入 shutdown -s -t 100&#xff0c;回车执行命令&#xff0c;自动关机设置成功 shutdown: 这是主命令&#xff0c;用于执行关闭或重启操作。-s: 这个参数用于指定执行关…

华为端云一体化开发 初始化云db表结构和表数据(实践2.0)(HarmonyOS学习第七课)

实例介绍&#xff1a;黑马鸿蒙刷题学习过程 1. 静态页面准备 借用黑马完成的页面&#xff0c;已经提供给大家一套写好的基本模板&#xff0c;大家直接将这套模板覆盖原有entry/src/main目录就可以 &#x1f4ce;main.ziphttps://www.yuque.com/attachments/yuque/0/2024/zip…

IngsollRang伺服拧紧轴控制器维修故障排查

【IngsollRang控制器故障排查】 在开始维修之前&#xff0c;请确保拧紧机已关闭并断开电源。然后&#xff0c;按照以下步骤进行故障排查&#xff1a; 1. 检查电源连接&#xff1a;确保拧紧机的电源线牢固连接&#xff0c;且电源插座正常工作。 2. 检查保险丝&#xff1a;如果电…

贪心算法-加油站

一、题目描述 二、解题思路 1.运动过程分析 这里需要一个油箱剩余油量的变量resGas&#xff0c;初始化resGas0&#xff1b;还需要一个标记从什么位置当做初始位置的startIdx&#xff0c;初始化startIdx0。 我们从数组下标idx0处开始向后遍历&#xff0c;初始时startIdx0&#…

java的核心机制:JVM

JVM&#xff08;java virtual machine&#xff0c;java虚拟机&#xff09;&#xff1a;是一个虚拟的计算机&#xff0c;是java程序的运行环境。JVM具有指令集并使用不同的存储区域&#xff0c;负责执行指令&#xff0c;管理数据、内存、寄存器。 JVM功能1&#xff1a;实现java程…