C++ TCP网络编程实战:客户端与服务端全流程解析
TCP是面向连接的可靠传输协议,广泛应用于互联网数据交互。在C++开发中,理解Socket API的正确调用顺序是构建稳定网络应用的基础。本文将深入解析TCP Client/Server架构,从套接字创建到数据传输,再到连接关闭,提供标准且可运行的代码框架,帮助您规避常见的阻塞与错误处理陷阱。
TCP协议特性与基础架构
TCP(Transmission Control Protocol)是一种面向连接的流式协议。其核心特征包括:
- 可靠性:通过校验和、确认应答(ACK)和超时重传机制保证数据无差错到达。
- 面向连接:通信前必须通过“三次握手”建立虚拟连接,通信结束后需通过“四次挥手”断开。
- 全双工通信:支持数据同时在双向通道传输。

TCP遵循典型的Client/Server(C/S)架构模式。服务器端被动等待请求,客户端主动发起连接。这种非对称性决定了双方在使用系统调用时的角色差异:服务器需要绑定固定端口并监听,而客户端则由系统自动分配临时端口。

C++ TCP客户端编程指南
TCP客户端的主要职责是发现服务器并发起连接。完整的操作生命周期包含以下五个步骤:
第1步:创建TCP套接字
首先需要使用 socket() 创建一个文件描述符。对于TCP协议,第二个参数必须指定为 SOCK_STREAM(流式套接字),以区别于UDP使用的 SOCK_DGRAM。
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
第2步:连接服务器(Connect)
使用 connect() 函数向指定IP和端口的服务器发起连接请求。若未显式调用 bind(),内核会自动为该套接字分配一个可用的本地随机端口(Ephemeral Port)并完成必要的地址结构填充。
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
参数说明:
socket:上一步创建的套接字描述符。address:服务器端地址结构指针(通常为struct sockaddr_in)。address_len:地址结构体的大小。
第3步:发送数据(Send)
连接建立后,通过 send() 将请求报文发送至服务端。该函数返回实际发送的字节数,失败时返回-1。
ssize_t send(int socket, const void *buffer, size_t length, int flags);
注意:flags 通常设为0。在TCP协议中,禁止发送长度为0的报文,因为接收方无法区分这是数据还是连接终止信号。
第4步:接收响应(Recv)
阻塞模式下,recv() 会等待直至收到数据或连接断开。若成功接收,返回实际读入缓冲区的数据长度;若远端正常关闭连接,返回0;若发生错误,返回-1。
ssize_t recv(int socket, void *buffer, size_t length, int flags);
第5步:关闭连接(Close)
通信完成后,务必调用 close() 释放资源,这将触发TCP的四次挥手过程,通知对方连接已终结。
客户端完整代码示例
以下代码展示了连接至 IP 10.9.21.211 端口 8000 的服务器并回显响应的标准流程:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
// 1. 创建TCP套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket failed");
return 1;
}
// 2. 配置服务器地址信息
struct sockaddr_in ser_addr;
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8000);
// 假设服务器IP为 10.9.21.211
inet_pton(AF_INET, "10.9.21.211", &ser_addr.sin_addr.s_addr);
// 3. 发起连接
if (connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr)) < 0) {
perror("connect failed");
close(sockfd);
return 1;
}
// 4. 发送请求
const char *msg = "hello tcp";
send(sockfd, msg, strlen(msg), 0);
// 5. 接收应答
unsigned char buf[1500] = {0};
ssize_t len = recv(sockfd, buf, sizeof(buf), 0);
if (len > 0) {
printf("服务器的应答:%s\n", buf);
} else if (len == 0) {
printf("服务器已关闭连接\n");
}
// 6. 清理资源
close(sockfd);
return 0;
}
C++ TCP服务端编程指南
服务端程序通常需要同时处理多个客户端的连接请求。其核心区别在于需要进行 bind 和 listen,并通过 accept 提取已完成的连接。
第1步:创建套接字
与客户端相同,创建 TCP 类型的流式套接字。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
第2步:绑定地址与端口(Bind)
服务器必须明确指定监听的 IP 和端口。INADDR_ANY 表示监听本机所有可用网卡接口。
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000); // 监听端口
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有本地地址
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror("bind failed");
return 1;
}
第3步:设置监听队列(Listen)
listen() 将套接字从主动模式切换为被动模式,并为尚未被 accept 处理的半连接/全连接创建内核队列。backlog 参数定义队列最大长度。
if (listen(sockfd, 10) < 0) {
perror("listen failed");
return 1;
}
第4步:接受新连接(Accept)
这是一个阻塞调用。当有新连接进入已连接队列时,accept 从中取出一个连接,并返回一个新的文件描述符(cfd)用于后续与该特定客户端的通信。
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int cfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);
重要提示:sockfd 仅用于 accept 获取新连接,不再直接参与数据传输。真正的数据收发应在新返回的 cfd 上进行。

第5步:数据收发循环(Send/Recv)
服务端通常在独立循环中处理每个 cfd:
- 读取判断:
recv()返回 0 表示对端优雅关闭(FIN包),返回 -1 表示错误,其余为正整数表示数据长度。 - 发送判断:
send()返回 0 在 TCP 中是不允许的,若发生则视为异常或立即断开。
第6步:资源回收
单个客户端通信结束后,关闭该客户端的连接描述符(cfd);服务结束时,关闭监听描述符(sockfd)。
服务端完整代码示例
以下代码实现了一个简单的Echo Server(回显服务器),接收客户端消息并将其原样发回:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9000
#define BACKLOG 10
int main() {
// 1. 创建套接字 (修正原稿中的SOCK_DGRAM错误,应为SOCK_STREAM)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定地址
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(PORT);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
perror("bind");
close(listen_fd);
exit(1);
}
// 3. 监听
if (listen(listen_fd, BACKLOG) < 0) {
perror("listen");
close(listen_fd);
exit(1);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// 4. 接受连接
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len);
if (conn_fd < 0) {
perror("accept");
continue;
}
// 打印客户端信息
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr, ip, INET_ADDRSTRLEN);
uint16_t port = ntohs(cli_addr.sin_port);
printf("New client connected: %s:%u\n", ip, port);
// 5. 数据交换循环
char buf[1500] = {0};
while (1) {
ssize_t n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
if (n > 0) {
buf[n] = '\0'; // 确保字符串结束
printf("Received: %s\n", buf);
// Echo response
send(conn_fd, buf, n, 0);
} else if (n == 0) {
printf("Client closed connection.\n");
break;
} else {
perror("recv error");
break;
}
}
// 6. 关闭当前客户端连接
close(conn_fd);
}
// 7. 关闭监听套接字
close(listen_fd);
return 0;
}
常见问题与注意事项
- 字节序转换:在设置
sin_port时必须使用htons(),IP地址建议由inet_pton()转换,以确保跨平台兼容性。 - 粘包问题:TCP是字节流协议,不保留消息边界。如果应用层需要固定长度或带分隔符的消息,必须在业务逻辑层实现封包与拆包,或使用变长字段长度解析。
- 非阻塞模式:上述代码均为阻塞I/O模型。在高并发场景下,建议使用
epoll配合select/poll等IO多路复用技术来管理成千上万个并发连接。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













