计算机网络知识点总结(四)Linux C++ Socket实现“伪”半双工聊天室程序 上一篇文章讲了Socket的基本函数并用一个简单的示例实现了单工通信——客户端发消息服务端接收并返回。这一篇我打算在此基础上改进实现服务端和客户端之间的相互通信也就是半双工聊天。如果想更深入理解推荐大家阅读《UNIX网络编程卷1套接字联网API》这本书。1. 补充知识点1.1 Socket处于网络协议的哪个层次套接字编程接口位于应用层和传输层之间是从应用层进入传输层的入口。应用层、表示层、会话层这三层处理具体的网络应用细节而传输层、网络层、数据链路层、物理层处理通信细节。1.2 为什么Socket提供的是从OSI模型顶上三层进入传输层的接口这样设计有两个原因职责分离顶上三层处理具体网络应用如FTP、Telnet、HTTP的所有细节但对通信细节了解很少底下四层对具体网络应用了解不多却处理所有的通信细节——发送数据、等待确认、给无序到达的数据排序、计算并验证校验和等等。进程隔离顶上三层通常构成用户进程底下四层作为操作系统内核的一部分提供。现代操作系统都提供分隔用户进程与内核的机制因此第4层和第5层之间的接口是构建API的自然位置。2. TCP实现框架上图展示了TCP通信的完整流程。服务端需要经过socket创建、bind绑定、listen监听、accept接受连接这四个步骤之后才能进入数据收发阶段。客户端则相对简单创建socket后直接connect连接即可。连接建立后双方可以通过send/recv进行数据交互。需要注意的是TCP是面向连接的可靠传输协议数据会按顺序到达但需要应用层自己处理数据边界问题。3. 代码实现3.1 什么是伪半双工这里需要解释一下为什么叫伪半双工。真正的半双工Half Duplex通信是指数据可以随时发送只是不能同时传输。而我们这个实现是一问一答式的——客户端先发一条消息服务端收到后回复一条消息然后客户端再发下一条。虽然实现了双向通信但通信是交替进行的不是真正意义上的半双工所以称之为伪半双工。3.2 服务端代码 Server.cpp// 系统类型定义#includesys/types.h// Socket核心函数和数据结构#includesys/socket.h// 标准输入输出#includestdio.h// sockaddr_in结构和IP地址定义#includenetinet/in.h// IP地址转换函数inet_pton#includearpa/inet.h// close()函数#includeunistd.h// 字符串操作memset、strlen、strcmp#includestring.h// 标准库函数#includestdlib.h// 错误码定义#includeerrno.h// 断言宏用于调试检查#includeassert.h// 默认端口号实际使用时从命令行参数传入#definePORT7000// 缓冲区大小用于存储收发数据#defineBUFFER_SIZE1024intmain(intargc,char*argv[]){// 检查命令行参数是否完整需要传入IP地址和端口号if(argc2){printf(Usage: %s ip_address port_number\n,argv[0]);return1;}// 从命令行参数获取IP地址constchar*ipargv[1];// 将端口号从字符串转换为整数intportatoi(argv[2]);// 创建socket描述符// AF_INET: IPv4协议族// SOCK_STREAM: 面向连接的TCP套接字// 0: 自动选择对应协议intsockSersocket(AF_INET,SOCK_STREAM,0);// 检查socket创建是否成功assert(sockSer0);// 定义服务端地址结构structsockaddr_inserver_sockaddr;// 清空地址结构避免垃圾数据memset(server_sockaddr,0,sizeof(server_sockaddr));// 设置协议族为IPv4server_sockaddr.sin_familyAF_INET;// 设置端口号htons()将主机字节序转换为网络字节序大端序server_sockaddr.sin_porthtons(port);// 设置IP地址inet_pton()将点分十进制IP转换为二进制网络字节序inet_pton(AF_INET,ip,server_sockaddr.sin_addr);// 将socket绑定到指定的IP和端口intretbind(sockSer,(structsockaddr*)server_sockaddr,sizeof(server_sockaddr));// 检查绑定是否成功assert(ret!-1);// 开始监听连接请求// 第二个参数5表示等待队列的最大长度未被accept的连接数retlisten(sockSer,5);// 检查监听是否成功assert(ret!-1);printf(Server listening on %s:%d...\n,ip,port);// 定义客户端地址结构用于存储客户端信息structsockaddr_inclient_addr;// 客户端地址长度socklen_t client_addrlengthsizeof(client_addr);// 接受客户端连接阻塞等待直到有客户端连接// 返回新的socket描述符connfd用于与该客户端通信intconnfdaccept(sockSer,(structsockaddr*)client_addr,client_addrlength);if(connfd0){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: 默认接收方式retrecv(connfd,buffer_recv,BUFFER_SIZE-1,0);// 接收失败或客户端断开连接if(ret0){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// 系统类型定义#includesys/types.h// Socket核心函数和数据结构#includesys/socket.h// 标准输入输出#includestdio.h// sockaddr_in结构和IP地址定义#includenetinet/in.h// IP地址转换函数inet_pton#includearpa/inet.h// close()函数#includeunistd.h// 字符串操作memset、strlen、strcmp#includestring.h// 标准库函数#includestdlib.h// 断言宏用于调试检查#includeassert.h// 缓冲区大小用于存储收发数据#defineBUFFER_SIZE1024intmain(intargc,char*argv[]){// 检查命令行参数是否完整需要传入服务端IP地址和端口号if(argc2){printf(Usage: %s ip_address port_number\n,argv[0]);return1;}// 从命令行参数获取服务端IP地址constchar*ipargv[1];// 将端口号从字符串转换为整数intportatoi(argv[2]);// 创建socket描述符// AF_INET: IPv4协议族// SOCK_STREAM: 面向连接的TCP套接字// 0: 自动选择对应协议intsockfdsocket(AF_INET,SOCK_STREAM,0);// 检查socket创建是否成功assert(sockfd0);// 定义服务端地址结构structsockaddr_inservaddr;// 清空地址结构避免垃圾数据memset(servaddr,0,sizeof(servaddr));// 设置协议族为IPv4servaddr.sin_familyAF_INET;// 设置服务端端口号htons()转换为网络字节序servaddr.sin_porthtons(port);// 设置服务端IP地址inet_pton()转换为二进制网络字节序inet_pton(AF_INET,ip,servaddr.sin_addr);// 连接到服务端// sockfd: 客户端socket描述符// servaddr: 服务端地址结构// sizeof(servaddr): 地址结构长度intretconnect(sockfd,(structsockaddr*)servaddr,sizeof(servaddr));if(ret0){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: 默认接收方式retrecv(sockfd,recvbuf,BUFFER_SIZE-1,0);// 接收失败或服务端断开连接if(ret0){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多路复用技术后续文章会详细介绍。