网络通信与协议栈 -- TCP协议与编程

发布于:2025-09-04 ⋅ 阅读:(20) ⋅ 点赞:(0)

TCP 协议与编程:面向连接的可靠传输

TCP(Transmission Control Protocol,传输控制协议)是传输层 “面向连接、可靠传输” 的核心协议,通过三次握手建立连接、四次挥手关闭连接,配合确认重传、流量控制、拥塞控制机制,保障数据有序、不丢失、不重复传输,适用于对可靠性要求高的场景(如网页浏览、文件传输、即时通信)。以下从协议特性、通信框架、编程流程到代码示例,系统梳理 TCP 编程核心内容。

1. TCP 的核心特性(与 UDP 对比)

TCP 作为 “可靠传输协议”,核心特性围绕 “连接管理” 和 “可靠性保障” 设计,与 UDP 的无连接、不可靠形成显著差异:

特性维度 TCP 协议 UDP 协议
连接模式 面向连接:通信前需通过 “三次握手” 建立专用连接,通信后需 “四次挥手” 关闭连接 无连接:直接发送数据,无需建立 / 关闭连接
传输可靠性 可靠:通过 “确认重传”(收到 ACK 才继续发)、“序号 / 确认号”(保证有序)、“校验和”(检测错误)实现数据不丢失、不重复、有序 不可靠:仅校验头部,无重传 / 确认机制,可能丢包、乱序
数据边界 无边界:数据以 “字节流” 形式传输,发送端多次 send (),接收端可一次 recv () 读取 有边界:数据以 “数据报” 为单位,发送次数与接收次数需匹配
流量控制 支持:通过 “滑动窗口” 机制,根据接收端缓冲区大小调整发送速率,避免接收端溢出 不支持:按发送端速率传输,可能导致接收端缓冲区满丢包
拥塞控制 支持:通过 “慢启动”“拥塞避免”“快速重传”“快速恢复” 机制,避免网络拥堵 不支持:无拥塞控制,可能加剧网络拥堵
头部大小 20-60 字节(含序号、确认号、窗口大小等字段) 8 字节(仅含源端口、目标端口、长度、校验和)
适用场景 对可靠性要求高的场景(网页 HTTP/HTTPS、文件传输 FTP、即时通信) 对延迟敏感的场景(视频通话、游戏、DNS 查询)

2. TCP 连接管理:三次握手与四次挥手

TCP 的 “面向连接” 核心是通过 “三次握手建立连接” 和 “四次挥手关闭连接”,确保通信双方状态同步,避免无效数据传输。

(1)三次握手(建立连接,确保双方收发能力正常)

目的:确认客户端和服务器的 “发送能力” 和 “接收能力” 均正常,分配连接所需资源(如缓冲区、序号)。
流程(客户端主动发起,服务器被动监听):

  1. 客户端 → 服务器:发送 SYN(同步)报文,携带初始序号(如 seq=100),表示 “我要建立连接,我的初始序号是 100”;此时客户端进入SYN_SENT状态。
  2. 服务器 → 客户端:发送 SYN+ACK(同步 + 确认)报文,携带服务器初始序号(如 seq=200)和对客户端 SYN 的确认号(ack=101,即 “我收到你的 100,下次请发 101”);此时服务器进入SYN_RCVD状态。
  3. 客户端 → 服务器:发送 ACK(确认)报文,携带对服务器 SYN 的确认号(ack=201,即 “我收到你的 200,下次请发 201”);此时客户端进入ESTABLISHED(连接建立)状态,服务器收到后也进入ESTABLISHED状态,双方可开始传输数据。

(2)四次挥手(关闭连接,确保数据传输完成)

目的:确保双方已传输完所有数据,释放连接占用的资源(如缓冲区、端口)。
流程(通常客户端主动发起关闭,也可服务器发起):

  1. 客户端 → 服务器:发送 FIN(结束)报文,携带序号(如 seq=300),表示 “我已无数据要发,请求关闭连接”;此时客户端进入FIN_WAIT_1状态。
  2. 服务器 → 客户端:发送 ACK(确认)报文,携带确认号(ack=301,即 “我收到你的关闭请求,已停止接收你的数据”);此时服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态(等待服务器发送剩余数据)。
  3. 服务器 → 客户端:服务器发送完所有剩余数据后,发送 FIN 报文,携带序号(如 seq=400),表示 “我也无数据要发,请求关闭连接”;此时服务器进入LAST_ACK状态。
  4. 客户端 → 服务器:发送 ACK(确认)报文,携带确认号(ack=401,即 “我收到你的关闭请求”);此时客户端进入TIME_WAIT状态(等待 2MSL,确保服务器收到 ACK,避免服务器重发 FIN),服务器收到后进入CLOSED状态;客户端等待 2MSL 后也进入CLOSED状态,连接完全关闭。

3. TCP 通信框架(C/S 模式)

TCP 采用 “客户端 - 服务器(C/S)” 模型,服务器需提前启动并监听指定端口,客户端主动发起连接请求,连接建立后双方通过 “字节流” 传输数据,流程如下:

  • 服务器端:启动 → 创建套接字 → 绑定 IP + 端口 → 监听端口 → 接受客户端连接 → 收发数据 → 关闭连接 ;
  • 客户端:启动 → 创建套接字 → 发起连接(连接服务器) → 收发数据 → 关闭连接;

核心差异(与 UDP 对比)

  • 服务器需多一步 “监听(listen)” 和 “接受连接(accept)” 操作,UDP 无需;
  • 连接建立后,双方通过read()/write()recv()/send()收发数据(基于已连接套接字),无需每次指定目标地址(UDP 需每次用sendto()/recvfrom()指定地址);
  • 数据传输为 “字节流”,接收端需根据应用层协议(如 HTTP 的Content-Length)判断数据是否接收完整,UDP 无需(数据报有边界)。

4. TCP 编程流程(以 C 语言 Socket API 为例)

TCP 编程依赖标准 Socket API,核心函数包括socket()(创建套接字)、bind()(绑定地址)、listen()(监听端口)、accept()(接受连接)、connect()(发起连接)、send()/recv()(收发数据)、close()(关闭连接)。

(1)核心函数说明

函数原型 功能描述 关键参数与返回值
int socket(int domain, int type, int protocol); 创建 TCP 套接字(向内核申请网络通信端点) - domain:AF_INET(IPv4);
- type:SOCK_STREAM(TCP 流式套接字);
- protocol:0(自动适配 TCP);
- 返回值:成功返回套接字 ID(int),失败返回 - 1。
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen); 服务器端:将套接字与指定 IP + 端口绑定(确定监听地址) - sockfd:socket () 返回的套接字 ID;
- my_addr:sockaddr_in 结构体(含 IP、端口、协议族);
- addrlen:结构体长度(sizeof (struct sockaddr_in));
- 返回值:成功返回 0,失败返回 - 1。
int listen(int sockfd, int backlog); 服务器端:将套接字设为 “监听状态”,等待客户端连接 - sockfd:绑定后的套接字 ID;
- backlog:监听队列大小(未完成连接队列 + 已完成连接队列的最大长度,通常设 5-10);
- 返回值:成功返回 0,失败返回 - 1。
int accept(int sockfd, struct sockaddr *cli_addr, socklen_t *addrlen); 服务器端:从监听队列中接受一个客户端连接,返回 “已连接套接字”(用于与该客户端通信) - sockfd:监听状态的套接字 ID(仅用于接受连接,不直接收发数据);
- cli_addr:存储客户端地址(可选,NULL 表示不关心);
- addrlen:客户端地址长度指针;
- 返回值:成功返回 “已连接套接字 ID”,失败返回 - 1。
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen); 客户端:向服务器发起 TCP 连接请求(触发三次握手) - sockfd:socket () 返回的套接字 ID;
- serv_addr:服务器地址结构体(含服务器 IP、端口);
- addrlen:结构体长度;
- 返回值:成功返回 0(连接建立),失败返回 - 1。
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 收发双方:通过已连接套接字发送数据(TCP 字节流) - sockfd:accept ()(服务器)或 connect ()(客户端)返回的 “已连接套接字”;
- buf:发送数据缓冲区;
- len:数据长度;
- flags:0(阻塞发送);
- 返回值:成功返回发送字节数,失败返回 - 1。
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 收发双方:通过已连接套接字接收数据(TCP 字节流) - 参数同 send (),buf 为接收缓冲区;
- 返回值:成功返回接收字节数,0 表示对方关闭连接,失败返回 - 1。
int close(int sockfd); 关闭套接字,释放资源(触发四次挥手) - sockfd:已连接套接字或监听套接字;
- 返回值:成功返回 0,失败返回 - 1。

(2)TCP 编程流程对比(服务器端 vs 客户端)

角色 核心步骤(按执行顺序) 关键说明
服务器端 1. socket():创建 TCP 套接字
2. bind():绑定服务器 IP + 端口
3. listen():将套接字设为监听状态
4. accept():阻塞等待并接受客户端连接(返回通信套接字)
5. recv()/send():通过通信套接字与客户端收发数据
6. close():关闭通信套接字(与该客户端断开),可循环执行 4-6 接受新客户端
- 监听套接字(socket()返回)仅用于接受连接,不直接收发数据;
- 每个客户端连接对应一个 “通信套接字”,服务器需通过多线程 / 多进程处理多客户端(避免单客户端阻塞其他客户端)。
客户端 1. socket():创建 TCP 套接字
2. connect():发起连接(连接服务器 IP + 端口)
3. send()/recv():与服务器收发数据
4. close():关闭套接字(断开连接)
- 客户端无需bind():系统自动分配临时端口;
connect()失败表示三次握手未完成(如服务器未启动、端口错误)。

5. TCP服务器/客户端 代码示例

服务器端代码

#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
typedef struct sockaddr *(SA);  // 定义sockaddr指针别名

int main(int argc, char **argv)
{
  // 创建监听套接字
  int listfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == listfd)
  {
    perror("scoket error\n");
    return 1;
  }
  
  // 初始化服务器地址结构
  struct sockaddr_in ser, cli;
  bzero(&ser, sizeof(ser));  // 清零结构体
  bzero(&cli, sizeof(cli));

  ser.sin_family = AF_INET;          // IPv4协议
  ser.sin_port = htons(50000);       // 端口号转换为网络字节序
  ser.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口

  // 绑定地址和端口
  int ret = bind(listfd, (SA)&ser, sizeof(ser));
  if (-1 == ret)
  {
    perror("bind");
    return 1;
  }
  
  // 开始监听(队列长度3)
  listen(listfd, 3);
  
  socklen_t len = sizeof(cli);
  // 接受客户端连接
  int conn = accept(listfd, (SA)&cli, &len);
  if (-1 == conn)
  {
    perror("accept");
    return 1;
  }
  
  time_t tm;
  while (1)
  {
    char buf[1024] = {0};  // 初始化接收缓冲区
    // 接收客户端数据
    int ret = recv(conn, buf, sizeof(buf), 0);
    if (ret <= 0)  // 连接关闭或出错
    {
        break;
    }
    time(&tm);  // 获取当前时间
    // 在消息后附加时间戳
    sprintf(buf, "%s %s", buf, ctime(&tm));
    // 发送响应数据
    send(conn, buf, strlen(buf), 0);
  }
  
  // 关闭所有套接字
  close(listfd);
  close(conn);

  return 0;
}

运行结果

  • 服务器启动后阻塞在accept()等待连接
  • 客户端连接后,服务器接收消息并添加时间戳返回
  • 示例输出:hello,this is tcp test Wed Jun 12 10:30:45 2024

客户端代码 

#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr *(SA);  // 定义sockaddr指针别名

int main(int argc, char **argv)
{
  // 创建通信套接字
  int conn = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == conn)
  {
    perror("socket");
    return 1;
  }
  
  // 初始化服务器地址结构
  struct sockaddr_in ser;
  bzero(&ser, sizeof(ser));

  ser.sin_family = AF_INET;        // IPv4协议
  ser.sin_port = htons(50000);     // 服务器端口
  ser.sin_addr.s_addr = INADDR_ANY; // 本地回环地址

  // 连接服务器
  int ret = connect(conn, (SA)&ser, sizeof(ser));
  if (-1 == ret)
  {
    perror("connect error\n");
    return 1;
  }
  
  int i = 10;
  while (i--)
  {
    char buf[1024] = "hello,this is tcp test";  // 发送消息
    send(conn, buf, strlen(buf), 0);            // 发送数据
    
    bzero(buf, sizeof(buf));                   // 清空缓冲区
    recv(conn, buf, sizeof(buf), 0);           // 接收响应
    printf("from ser:%s\n", buf);              // 打印服务器响应
    sleep(1);                                  // 间隔1秒
  }
  
  close(conn);  // 关闭连接
  return 0;
}

运行结果

  • 客户端连接服务器成功
  • 每秒发送"hello,this is tcp test"并接收带时间戳的响应
  • 示例输出:from ser:hello,this is tcp test Wed Jun 12 10:30:45 2024

6.TCP数据粘包问题

问题描述

  • TCP粘包问题是指由于TCP是面向流的传输协议,数据在传输过程中没有明确的消息边界,导致:

  • 多条消息合并:接收方可能一次性收到发送方多次发送的数据

  • 消息被拆分:一个完整的消息可能被拆分成多个数据包接收

  • 数据混乱:无法准确区分每条消息的起始和结束位置

解决方案

  1. 设置边界:使用特殊字符(如\0)作为消息结束标志
  2. 固定大小:每次发送固定长度的数据块
  3. 自定义协议:在消息头部添加长度字段

网站公告

今日签到

点亮在社区的每一天
去签到