当前位置: 首页 > news >正文

计算机网络知识点总结(四)Linux C++ Socket实现“伪”半双工聊天室程序

上一篇文章讲了Socket的基本函数,并用一个简单的示例实现了单工通信——客户端发消息,服务端接收并返回。这一篇我打算在此基础上改进,实现服务端和客户端之间的相互通信,也就是半双工聊天。

如果想更深入理解,推荐大家阅读《UNIX网络编程卷1套接字联网API》这本书。

1. 补充知识点

1.1 Socket处于网络协议的哪个层次?

套接字编程接口位于应用层和传输层之间,是从应用层进入传输层的入口。应用层、表示层、会话层这三层处理具体的网络应用细节,而传输层、网络层、数据链路层、物理层处理通信细节。

1.2 为什么Socket提供的是从OSI模型顶上三层进入传输层的接口?

这样设计有两个原因:

  1. 职责分离:顶上三层处理具体网络应用(如FTP、Telnet、HTTP)的所有细节,但对通信细节了解很少;底下四层对具体网络应用了解不多,却处理所有的通信细节——发送数据、等待确认、给无序到达的数据排序、计算并验证校验和等等。

  2. 进程隔离:顶上三层通常构成用户进程,底下四层作为操作系统内核的一部分提供。现代操作系统都提供分隔用户进程与内核的机制,因此第4层和第5层之间的接口是构建API的自然位置。

2. TCP实现框架

上图展示了TCP通信的完整流程。服务端需要经过socket创建、bind绑定、listen监听、accept接受连接这四个步骤,之后才能进入数据收发阶段。客户端则相对简单,创建socket后直接connect连接即可。

连接建立后,双方可以通过send/recv进行数据交互。需要注意的是,TCP是面向连接的可靠传输协议,数据会按顺序到达,但需要应用层自己处理数据边界问题。

3. 代码实现

3.1 什么是"伪"半双工

这里需要解释一下为什么叫"伪"半双工。真正的半双工(Half Duplex)通信是指数据可以随时发送,只是不能同时传输。而我们这个实现是"一问一答"式的——客户端先发一条消息,服务端收到后回复一条消息,然后客户端再发下一条。虽然实现了双向通信,但通信是交替进行的,不是真正意义上的半双工,所以称之为"伪"半双工。

3.2 服务端代码 Server.cpp

// 系统类型定义#include<sys/types.h>// Socket核心函数和数据结构#include<sys/socket.h>// 标准输入输出#include<stdio.h>// sockaddr_in结构和IP地址定义#include<netinet/in.h>// IP地址转换函数(inet_pton)#include<arpa/inet.h>// close()函数#include<unistd.h>// 字符串操作(memset、strlen、strcmp)#include<string.h>// 标准库函数#include<stdlib.h>// 错误码定义#include<errno.h>// 断言宏(用于调试检查)#include<assert.h>// 默认端口号(实际使用时从命令行参数传入)#definePORT7000// 缓冲区大小,用于存储收发数据#defineBUFFER_SIZE1024intmain(intargc,char*argv[]){// 检查命令行参数是否完整,需要传入IP地址和端口号if(argc<=2){printf("Usage: %s ip_address port_number\n",argv[0]);return1;}// 从命令行参数获取IP地址constchar*ip=argv[1];// 将端口号从字符串转换为整数intport=atoi(argv[2]);// 创建socket描述符// AF_INET: IPv4协议族// SOCK_STREAM: 面向连接的TCP套接字// 0: 自动选择对应协议intsockSer=socket(AF_INET,SOCK_STREAM,0);// 检查socket创建是否成功assert(sockSer>=0);// 定义服务端地址结构structsockaddr_inserver_sockaddr;// 清空地址结构,避免垃圾数据memset(&server_sockaddr,0,sizeof(server_sockaddr));// 设置协议族为IPv4server_sockaddr.sin_family=AF_INET;// 设置端口号,htons()将主机字节序转换为网络字节序(大端序)server_sockaddr.sin_port=htons(port);// 设置IP地址,inet_pton()将点分十进制IP转换为二进制网络字节序inet_pton(AF_INET,ip,&server_sockaddr.sin_addr);// 将socket绑定到指定的IP和端口intret=bind(sockSer,(structsockaddr*)&server_sockaddr,sizeof(server_sockaddr));// 检查绑定是否成功assert(ret!=-1);// 开始监听连接请求// 第二个参数5表示等待队列的最大长度(未被accept的连接数)ret=listen(sockSer,5);// 检查监听是否成功assert(ret!=-1);printf("Server listening on %s:%d...\n",ip,port);// 定义客户端地址结构,用于存储客户端信息structsockaddr_inclient_addr;// 客户端地址长度socklen_t client_addrlength=sizeof(client_addr);// 接受客户端连接,阻塞等待直到有客户端连接// 返回新的socket描述符(connfd),用于与该客户端通信intconnfd=accept(sockSer,(structsockaddr*)&client_addr,&client_addrlength);if(connfd<0){printf("Accept failed, errno is: %d\n",errno);return1;}printf("Client connected\n");// 定义接收和发送缓冲区charbuffer_recv[BUFFER_SIZE]={0};charbuffer_send[BUFFER_SIZE]={0};// 主循环:持续与客户端通信while(1){// 清空缓冲区,避免上次数据残留memset(buffer_recv,0,BUFFER_SIZE);memset(buffer_send,0,BUFFER_SIZE);// 接收客户端消息// connfd: 已连接的socket描述符// buffer_recv: 接收缓冲区// BUFFER_SIZE - 1: 预留一个字节给字符串结束符// 0: 默认接收方式ret=recv(connfd,buffer_recv,BUFFER_SIZE-1,0);// 接收失败或客户端断开连接if(ret<=0){printf("Client disconnected or error occurred\n");break;}// 检查是否收到退出指令if(strcmp(buffer_recv,"quit\n")==0){printf("Communication is over!\n");break;}// 打印客户端发送的消息printf("client: %s",buffer_recv);// 提示服务端输入消息printf("server: ");// 从标准输入读取服务端消息fgets(buffer_send,BUFFER_SIZE,stdin);// 发送消息给客户端send(connfd,buffer_send,strlen(buffer_send),0);// 检查服务端是否输入退出指令if(strcmp(buffer_send,"quit\n")==0){printf("Communication is over!\n");break;}}// 关闭与客户端的连接close(connfd);// 关闭监听socketclose(sockSer);return0;}

3.3 客户端代码 Client.cpp

// 系统类型定义#include<sys/types.h>// Socket核心函数和数据结构#include<sys/socket.h>// 标准输入输出#include<stdio.h>// sockaddr_in结构和IP地址定义#include<netinet/in.h>// IP地址转换函数(inet_pton)#include<arpa/inet.h>// close()函数#include<unistd.h>// 字符串操作(memset、strlen、strcmp)#include<string.h>// 标准库函数#include<stdlib.h>// 断言宏(用于调试检查)#include<assert.h>// 缓冲区大小,用于存储收发数据#defineBUFFER_SIZE1024intmain(intargc,char*argv[]){// 检查命令行参数是否完整,需要传入服务端IP地址和端口号if(argc<=2){printf("Usage: %s ip_address port_number\n",argv[0]);return1;}// 从命令行参数获取服务端IP地址constchar*ip=argv[1];// 将端口号从字符串转换为整数intport=atoi(argv[2]);// 创建socket描述符// AF_INET: IPv4协议族// SOCK_STREAM: 面向连接的TCP套接字// 0: 自动选择对应协议intsockfd=socket(AF_INET,SOCK_STREAM,0);// 检查socket创建是否成功assert(sockfd>=0);// 定义服务端地址结构structsockaddr_inservaddr;// 清空地址结构,避免垃圾数据memset(&servaddr,0,sizeof(servaddr));// 设置协议族为IPv4servaddr.sin_family=AF_INET;// 设置服务端端口号,htons()转换为网络字节序servaddr.sin_port=htons(port);// 设置服务端IP地址,inet_pton()转换为二进制网络字节序inet_pton(AF_INET,ip,&servaddr.sin_addr);// 连接到服务端// sockfd: 客户端socket描述符// servaddr: 服务端地址结构// sizeof(servaddr): 地址结构长度intret=connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));if(ret<0){printf("Connection failed\n");return1;}printf("Connected to server %s:%d\n",ip,port);// 定义发送和接收缓冲区charsendbuf[BUFFER_SIZE];charrecvbuf[BUFFER_SIZE];// 主循环:持续与服务端通信while(1){// 清空缓冲区,避免上次数据残留memset(sendbuf,0,BUFFER_SIZE);memset(recvbuf,0,BUFFER_SIZE);// 提示客户端输入消息printf("client: ");// 从标准输入读取客户端消息fgets(sendbuf,BUFFER_SIZE,stdin);// 发送消息给服务端send(sockfd,sendbuf,strlen(sendbuf),0);// 检查客户端是否输入退出指令if(strcmp(sendbuf,"quit\n")==0){printf("Communication is over!\n");break;}// 接收服务端回复的消息// sockfd: 已连接的socket描述符// recvbuf: 接收缓冲区// BUFFER_SIZE - 1: 预留一个字节给字符串结束符// 0: 默认接收方式ret=recv(sockfd,recvbuf,BUFFER_SIZE-1,0);// 接收失败或服务端断开连接if(ret<=0){printf("Server disconnected or error occurred\n");break;}// 检查是否收到服务端的退出指令if(strcmp(recvbuf,"quit\n")==0){printf("Communication is over!\n");break;}// 打印服务端发送的消息printf("server: %s",recvbuf);}// 关闭socket连接close(sockfd);return0;}

3.4 编译与运行

# 编译服务端g++ Server.cpp-oserver# 编译客户端g++ Client.cpp-oclient# 启动服务端(绑定到本机地址)./server127.0.0.17000# 打开另一个终端启动客户端./client127.0.0.17000

运行后,客户端先发送消息,服务端收到后回复,双方交替发送直到一方输入"quit"结束通信。

这个实现虽然简单,但展示了TCP半双工通信的基本原理。如果需要支持真正的全双工通信(双方同时发送),可以使用多线程或I/O多路复用技术,后续文章会详细介绍。

http://www.gsyq.cn/news/1637187.html

相关文章:

  • 大疆 M3508 电机速度 PID 调参实战:从振荡到稳定,3 组参数对比分析
  • 【全网大测评】有没有降AI率的靠谱软件推荐?2026年亲测15款降AI率工具,帮你避坑省钱!
  • Go 微服务限流:别把所有请求都堵在入口
  • 2026建筑合同管理系统怎么选才不踩坑:房建企业合同、签证、产值与付款闭环指南
  • AI 创业假设验证:先证明有人痛,再证明模型强
  • 2026年暑假学习规划排名:这样安排让孩子高效又充实
  • 椭偏仪—介质膜的首选方法
  • OWTB 3PL 核心主流程与行业落地方案
  • ChromaControl:实现跨品牌RGB设备统一控制的终极解决方案
  • 剪映专业版教程:制作推拉平移相册效果
  • 【OpenHarmony/HarmonyOs 】数学视界实战:悬浮导航栏、沉浸光感与全新交互体验
  • 基于51单片机的气象站环境检测系统 风速风向温湿度 气象监测仪24(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 微服务合同测试:创业团队也别只靠联调
  • 2026美妆行业会员管理系统怎么选?跨店互通、复购提升、成本对比
  • 2026年一键生成论文工具实测:5款AI神器闭眼选不翻车
  • VRoid Studio中文汉化完整攻略:5步摆脱英文界面困扰
  • 5分钟快速上手:Mi-Create可视化小米手表表盘设计终极指南
  • YOLO26 全网独家改进创新:ECCV2026 S2-FracMix 颈部网络,引入形状-尺度分形混合 Neck,独家创新!
  • Three.js 粒子星空教程
  • 数学公式编辑革命:为什么MathLive成为现代Web开发者的首选方案?
  • 分享一个好用的免费远程工具APP
  • 机器人高算力平台上车前,整机评审要检查哪些工程约束?
  • 终极指南:如何用ViGEmBus驱动在Windows上轻松创建虚拟游戏控制器
  • Python 自动化任务:Cron 之外还要有状态机
  • Windows Cleaner:告别C盘爆红,让你的电脑重获新生!
  • 第44篇:网络抖动、接口偶发卡顿?抓包看懂TCP丢包重传真相
  • 前端工程化-01:前端工程化技术栈
  • 蓝速科技 RISC-V 鸿蒙信创终端全场景落地方案
  • Chrome DevTools 3步定位 Blob 视频源:从 Network 面板到 m3u8 链接实战
  • 显卡驱动彻底清理指南:3分钟掌握DDU专业工具