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

📅 2026/7/5 1:34:18 👁️ 阅读次数 📝 编程学习
计算机网络知识点总结(四)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多路复用技术,后续文章会详细介绍。