在网络编程领域,TCP协议就像一位可靠的邮差,确保你的每一封信件都能准确无误地送达目的地。作为传输层协议,TCP通过三次握手建立连接、数据确认与重传、流量控制、拥塞控制以及四次挥手断开连接等机制,为应用层提供了可靠的字节流传输服务。
TCP的核心特性可以概括为三点:
在实际编程中,TCP的这些特性直接影响了我们的接口使用方式。比如,由于TCP是面向连接的,所以我们需要显式地调用connect()和accept()来建立连接;由于它是可靠的,所以我们不需要在应用层处理丢包问题;又因为它是流式的,所以会出现"粘包"现象,需要我们在应用层处理消息边界。
注意:TCP的可靠性不是绝对的,它只能保证数据在网络层的可靠传输。如果应用程序崩溃或机器断电,仍然可能丢失数据。对于金融交易等场景,还需要应用层的确认机制。
服务端的TCP通信就像开设一家餐厅,需要先准备场地、招聘员工,然后等待客户光临:
创建socket:相当于租下店面
c复制int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket创建失败");
exit(EXIT_FAILURE);
}
这里AF_INET表示IPv4协议,SOCK_STREAM表示使用TCP协议。第三个参数0表示自动选择默认协议。
bind绑定地址:相当于给餐厅挂上门牌号
c复制struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
address.sin_port = htons(8080); // 端口号
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind失败");
exit(EXIT_FAILURE);
}
htons()函数将主机字节序转换为网络字节序(大端序),这是网络编程中常见的坑点。
listen监听连接:相当于开门营业,设置等待区大小
c复制if (listen(server_fd, 5) < 0) { // backlog=5
perror("listen失败");
exit(EXIT_FAILURE);
}
backlog参数决定了等待连接队列的最大长度。注意这个值只是建议值,实际值可能由系统决定。
accept接受连接:迎接具体客户
c复制int new_socket;
int addrlen = sizeof(address);
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("accept失败");
exit(EXIT_FAILURE);
}
关键点:accept返回的是一个新的socket文件描述符,专门用于与这个客户端通信。
客户端的流程就像去餐厅吃饭的顾客:
创建socket:准备去吃饭
c复制int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket创建失败");
exit(EXIT_FAILURE);
}
connect连接服务器:选择餐厅并建立联系
c复制struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
// 将IP地址从字符串转换为网络格式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("地址转换失败");
exit(EXIT_FAILURE);
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("连接失败");
exit(EXIT_FAILURE);
}
数据收发是TCP通信的核心功能,就像餐厅里的点餐和上菜:
send发送数据:
c复制char *hello = "Hello from server";
int bytes_sent = send(new_socket, hello, strlen(hello), 0);
if (bytes_sent < 0) {
perror("发送失败");
}
注意:send()的返回值表示实际发送的字节数,可能小于请求发送的长度。这是TCP流量控制和拥塞控制的结果。
recv接收数据:
c复制char buffer[1024] = {0};
int bytes_read = recv(new_socket, buffer, 1024, 0);
if (bytes_read < 0) {
perror("接收失败");
} else if (bytes_read == 0) {
printf("连接已关闭\n");
} else {
printf("收到消息: %s\n", buffer);
}
关键点:recv()返回0表示对方已关闭连接(收到FIN包),负数表示出错。
TCP的流式特性导致多个数据包可能被粘在一起传输,就像把多封信装进同一个信封。解决方法主要有三种:
固定长度法:每个消息都使用固定长度
c复制// 发送方
char msg[100] = {0};
strncpy(msg, "Hello", sizeof(msg));
send(sock, msg, sizeof(msg), 0);
// 接收方
char buffer[100];
recv(sock, buffer, sizeof(buffer), 0);
分隔符法:使用特殊字符作为消息边界
c复制// 发送方
send(sock, "Hello\n", 6, 0);
// 接收方 - 需要逐个字符读取直到遇到\n
长度前缀法:在数据前添加长度信息
c复制// 发送方
uint32_t len = htonl(strlen("Hello"));
send(sock, &len, sizeof(len), 0);
send(sock, "Hello", strlen("Hello"), 0);
// 接收方
uint32_t msg_len;
recv(sock, &msg_len, sizeof(msg_len), 0);
msg_len = ntohl(msg_len);
char *buffer = malloc(msg_len + 1);
recv(sock, buffer, msg_len, 0);
buffer[msg_len] = '\0';
实战经验:在真实项目中,推荐使用长度前缀法,它既高效又可靠。HTTP协议的Content-Length头就是这种思想的体现。
对于高性能服务器,阻塞式IO会严重限制并发能力。解决方案有:
设置非阻塞模式:
c复制int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
使用select多路复用:
c复制fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int activity = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (activity > 0 && FD_ISSET(sockfd, &readfds)) {
// 可读事件发生
}
更高效的epoll(Linux特有):
c复制int epoll_fd = epoll_create1(0);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event);
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == sockfd) {
// 可读事件发生
}
}
理解TCP状态机对调试网络问题至关重要。常见状态包括:
使用netstat命令查看连接状态:
bash复制netstat -antp | grep 8080
常见问题排查:
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 可靠 | 不可靠 |
| 传输方式 | 字节流 | 数据报 |
| 流量控制 | 有 | 无 |
| 拥塞控制 | 有 | 无 |
| 传输效率 | 较低 | 较高 |
| 头部大小 | 最小20字节 | 8字节 |
| 适用场景 | 文件传输、Web等 | 视频流、DNS等 |
TCP编程模型:
UDP编程模型:
UDP示例:
c复制// 服务端
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in servaddr;
bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&cliaddr, &len);
sendto(sockfd, "response", 8, 0, (struct sockaddr*)&cliaddr, len);
选择TCP当:
选择UDP当:
混合使用案例:很多实时游戏同时使用TCP和UDP,TCP传输关键状态信息,UDP传输实时位置更新。
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 聊天循环
while (1) {
memset(buffer, 0, BUFFER_SIZE);
int bytes_read = read(new_socket, buffer, BUFFER_SIZE);
if (bytes_read <= 0) break;
printf("Client: %s", buffer);
printf("Server: ");
fgets(buffer, BUFFER_SIZE, stdin);
send(new_socket, buffer, strlen(buffer), 0);
}
close(new_socket);
close(server_fd);
return 0;
}
c复制#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 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation error");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 转换IP地址
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("invalid address");
return -1;
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
return -1;
}
// 聊天循环
while (1) {
printf("Client: ");
fgets(buffer, BUFFER_SIZE, stdin);
send(sock, buffer, strlen(buffer), 0);
if (strcmp(buffer, "exit\n") == 0) break;
memset(buffer, 0, BUFFER_SIZE);
int bytes_read = read(sock, buffer, BUFFER_SIZE);
if (bytes_read <= 0) break;
printf("Server: %s", buffer);
}
close(sock);
return 0;
}
在实际项目中,TCP通信的这些基础原理和接口使用是构建更复杂网络应用的基石。理解每个系统调用背后的TCP状态变化,能够帮助开发者编写出更健壮、高效的网络程序。