14.8 Socket 一收一发通信

news2025/4/5 20:17:52

通常情况下我们在编写套接字通信程序时都会实现一收一发的通信模式,当客户端发送数据到服务端后,我们希望服务端处理请求后同样返回给我们一个状态值,并以此判断我们的请求是否被执行成功了,另外增加收发同步有助于避免数据包粘包问题的产生,在多数开发场景中我们都会实现该功能。

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 许可协议。转载请注明出处!

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

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

相关文章

docker基础认知(镜像+容器+仓库+客户端与服务器)

镜像&#xff08;Image&#xff09; 静态文件&#xff0c;提供了容器运行时所需的程序、库、资源、配置等文件&#xff0c;另外包含了一些为运行时准备的一些配置参数&#xff08;如匿名卷、环境变量、用户等&#xff09;。&#xff08;相比文件的说法&#xff0c;文件系统的说…

Mac电脑空间不足怎么办?如何优化系统

随着使用时间的增长&#xff0c;我们会发现Mac电脑的存储空间越来越少&#xff0c;这时候我们就需要对Mac电脑进行清理&#xff0c;以释放更多的存储空间。那么&#xff0c;Mac空间不足怎么解决呢&#xff1f; 1.清理垃圾文件 Mac空间不足怎么解决&#xff1f;首先要做的就是…

Go语言入门心法(七): 并发与通道

Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 Go语言入门心法(四): 异常体系 Go语言入门心法(五): 函数 一: go语言并发与通道

身份证照片怎么弄成200k以内?三个方法轻松搞定!

在日常生活中&#xff0c;为了方便上传和保存、便于传输和处理以及符合相关规定等方面的考虑&#xff0c;身份证照片大小常常被要求控制在200k以内&#xff0c;可是手机随便一拍&#xff0c;任凭如何截图都在超过1M&#xff0c;这个时候就需要借助一些图片压缩工具&#xff0c;…

汽车电子 -- CAN报文接收解析和发送组包

之前已经讲过如何生成DBC文件了&#xff0c;程序中该如何解析DBC呢&#xff1f; 其中包括接收CAN报文解析和发送CAN报文组包&#xff1f;&#xff1f; 一、Motorola和Intel格式 dbc里的信号Signals&#xff0c;其中里面有两种数据格式 Motorola和Intel格式。 之前C语言里&…

win11点击任务栏固定的应用:该文件没有与之关联的应用来执行该操作

点击固定在任务栏的任何图标提示 1、直接点击“开始”旁边的搜索按钮&#xff0c;搜索“注册表编辑器”。 2、找到HKEY_CLASSES_ROOT\lnkfile&#xff08;注意“lnkfile”的l是小写L不是大写i&#xff09;&#xff0c;在右侧新建一个字符串值“IsShortcut”的文件&#xff1…

大模型的背景与现状问题

一、大模型的发展背景 谈起大模型&#xff0c;第一时间想到的是什么&#xff1f;是主流的ChatGPT&#xff1f;或者GPT4&#xff1f;还是DALL-E3&#xff1f;亦或者Midjourney&#xff1f;以及Stablediffusion&#xff1f;还是层出不穷的其他各类AI Agent应用工具&#xff1f;大…

攻防千层饼

近年来&#xff0c;网络安全领域正在经历一场不断升级的攻防对抗&#xff0c;这场攻防已经不再局限于传统的攻击与防御模式&#xff0c;攻击者和防守者都已经越发熟练&#xff0c;对于传统攻防手法了如指掌。 在这个背景下&#xff0c;攻击者必须不断寻求创新的途径&#xff0…

【C++】头文件chrono

2023年10月16日&#xff0c;周一晚上 当前我只是简单的了解了一下chrono 以后可能会深入了解chrono并更新文章 目录 功能原理头文件chrono中的一些类头文件chrono中的数据类型一个简单的示例程序小实验&#xff1a;证明a的效率比a高 功能 这个chrono头文件是用来处理时间的…

git log 美化配置

编辑 vim ~/.gitconfig 添加配置 [alias]lg log --graph --abbrev-commit --decorate --dateformat:%m-%d %H:%M:%S --formatformat:%C(bold blue)%h%C(reset) - %s %C(bold yellow)% d%C(reset) %n %C(dim white) (%ad) - %an%C(reset) --allgit lg 效果

Ansible脚本进阶---playbook

目录 一、playbooks的组成 二、案例 2.1 在webservers主机组中执行一系列任务&#xff0c;包括禁用SELinux、停止防火墙服务、安装httpd软件包、复制配置文件和启动httpd服务。 2.2 在名为dbservers的主机组中创建一个用户组&#xff08;mysql&#xff09;和一个用户&#x…

Cesium Vue(三)— 相机配置

1. 坐标系转换 1.1 cesium使用到的坐标系 屏幕坐标系&#xff0c;二维的笛卡尔坐标系&#xff0c;API > Cartesian2地理空间坐标系&#xff0c;WGS-84坐标系&#xff0c; API > Cartographic(经度&#xff0c;维度&#xff0c;高度)三维笛卡尔空间直角坐标系&#xff0…

华为交换机S200, S1700系列产品命名规则

华为交换机的全系列产品命名规则如下&#xff1a; S系列&#xff1a;代表固定端口交换机。例如&#xff0c;S5720系列、S6720系列。CE系列&#xff1a;代表企业级交换机。例如&#xff0c;CE5800系列、CE6800系列。CloudEngine系列&#xff1a;代表华为云引擎交换机&#xff0c…

如何实现 Es 全文检索、高亮文本略缩处理(封装工具接口极致解耦)

如何实现 Es 全文检索、高亮文本略缩处理 前言技术选型JAVA 常用语法说明全文检索开发高亮开发Es Map 转对象使用核心代码 Trans 接口&#xff08;支持父类属性的复杂映射&#xff09;Trans 接口可优化的点高亮全局配置类如下真实项目落地效果为什么不用 numOfFragments、fragm…

数据结构与算法课后题-第五章(哈夫曼树和哈夫曼编码)

文章目录 选择题1选择题2选择题3选择题4选择题5选择题6选择题7应用题7 选择题1 选择题2 选择题3 需要深究 选择题4 选择题5 选择题6 选择题7 应用题7

【LeetCode刷题(数组and排序)】:存在重复元素

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 &#xff0c;返回 true &#xff1b;如果数组中每个元素互不相同&#xff0c;返回 false 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3,1] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;nums [1,2…

framework通信机制—LiveData使用方法及原理

LiveData是一种可观察的数据存储器类。与常规的可观察类不同&#xff0c;LiveData 具有生命周期感知能力&#xff0c;意指它遵循其他应用组件&#xff08;如 activity、fragment 或 service&#xff09;的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应…

自我介绍思考

1.引导面试官有重点的看你简历 2.在引导部分暗示他我是最适合这个岗位的 面试官在考察什么&#xff1f; a.你的表述是否一致b.考察你的语言表达能力&#xff0c;逻辑思维能力&#xff0c;总结概括能力c.考察你对现场的把控能力d.对时间的把控能力 怎么做&#xff1f; 1.写逐…

uniapp中全局页面挂载组件(H5)

前言 我们已经学习了 uniapp中全局页面挂载组件&#xff08;小程序&#xff09; 有些小伙伴问在H5怎么做那让我们试一试 直接上代码 //引用组件 import dialog from ./index.vue; //我这里要把小程序的方法和h5方法写一起所以用了混入 import mixins from ./mixins.js //使用…

OJ项目【登录】——验证码、失败登录多次账户冻结、用户密码加密,我是如何实现的?

目录 前言 1、验证码 1.1、引入pom 1.2、前端核心代码 1.3、后端核心代码 2、账户冻结 2.1、思路&#xff1a; 2.2、核心代码示例&#xff1a; 3、密码加密——加盐算法 3.1、思路&#xff1a; 3.2、代码实现示例&#xff1a; 4、小结&#xff1a;展示我的项目 4…