Linux软件编程学习笔记(十一):深入浅出 TCP 协议与 Socket 编程实践

一、TCP 协议概述

TCP(Transmission Control Protocol,传输控制协议)是 TCP/IP 协议族中的核心协议之一,位于传输层。它的设计目标是在不可靠的 IP 网络之上,提供一种面向连接的、可靠的、基于字节流的通信服务。

1.1 TCP 的核心特性

特性 说明
面向连接 通信前需建立连接(三次握手),结束后释放连接(四次挥手)
可靠传输 通过确认与重传机制,保证数据完整、无差错、不丢失、不重复
全双工通信 连接双方可同时发送和接收数据
字节流服务 应用层数据被视为连续的字节流,无固定边界,由应用层自行解析
流量控制 使用滑动窗口,防止发送方发送过快导致接收方缓冲区溢出
拥塞控制 动态调整发送速率,避免网络过载

二、TCP 报文段头部格式

理解 TCP 的工作机制,离不开对报文段头部的认识。TCP 头部长度通常为 20~60 字节,各字段含义如下:

字段名 长度(bit) 说明
源端口号 16 发送方应用程序端口
目标端口号 16 接收方应用程序端口
序列号(SEQ) 32 数据段的第一个字节序号
确认号(ACK) 32 期望收到的下一个序号,确认号之前的数据已收到
数据偏移 4 头部长度(单位 4 字节)
控制位 6+2 包含 SYN、ACK、FIN、RST、PSH、URG 等标志
窗口大小 16 接收窗口大小,用于流量控制
校验和 16 头部和数据的校验和
紧急指针 16 紧急数据的位置

其中,控制位(flags) 是管理连接状态的关键:

  • SYN:同步序号,用于建立连接
  • ACK:确认字段有效
  • FIN:发送方完成数据发送,请求关闭
  • RST:异常重置连接
  • PSH:接收方应尽快将数据交给应用层
  • URG:紧急数据

三、TCP 的建立连接------三次握手

TCP 在传输数据前必须通过三次握手建立连接,其根本目的是确保双方都具备发送和接收数据的能力,并同步初始序列号。

3.1 握手过程

text 复制代码
客户端              服务器
  |                  |
  |  SYN, SEQ=x      |
  |----------------->|  (第一次:客户端请求连接)
  |                  |
  |  SYN, ACK=x+1,   |
  |  SEQ=y           |
  |<-----------------|  (第二次:服务器同意,并携带自己的SYN)
  |                  |
  |  ACK=y+1         |
  |----------------->|  (第三次:客户端确认,连接建立)
  |                  |
  |  数据传输...     |
  • 第一次 :客户端发送 SYN 报文段,指明自己的初始序列号 x,表示希望建立连接。
  • 第二次 :服务器回复 SYN+ACK,确认号是 x+1,同时告知自己的初始序列号 y
  • 第三次 :客户端发送 ACK,确认号为 y+1。此包可携带应用层数据。

3.2 为什么是三次,而不是两次或四次?

  • 两次:服务器无法确认客户端已收到自己的 SYN-ACK,可能造成半连接或重复连接。
  • 三次:最小的握手次数,足以同步双方的序列号并确认双方向通信能力。
  • 四次或更多:增加不必要的开销。

四、TCP 的断开连接------四次挥手

当一方完成数据发送后,可以发起关闭连接。由于 TCP 是全双工的,关闭需要分别关闭两个方向的传输。

4.1 挥手过程

text 复制代码
客户端              服务器
  |                  |
  |  FIN, SEQ=u      |
  |----------------->|  (第一次:客户端不再发送数据)
  |                  |
  |  ACK=u+1         |
  |<-----------------|  (第二次:服务器确认,此时若还有数据可继续发送)
  |                  |
  |  FIN, SEQ=v      |
  |<-----------------|  (第三次:服务器数据发送完毕,请求关闭)
  |                  |
  |  ACK=v+1         |
  |----------------->|  (第四次:客户端确认,进入TIME_WAIT)
  |                  |
  |  连接彻底关闭    |
  • 第一次:主动关闭方(如客户端)发送 FIN 报文,表示不再发送数据。
  • 第二次:被动关闭方回复 ACK,表示已收到 FIN,但此时若还有数据可继续发送。
  • 第三次:被动关闭方数据发完后,发送自己的 FIN。
  • 第四次:主动关闭方回复 ACK,随后等待 2MSL 后关闭。

4.2 为什么是四次而不是三次?

因为关闭连接时,被动关闭方可能仍有数据要发送,不能像握手那样合并 ACK 和 FIN,因此必须分两步(先 ACK,再 FIN)。


五、TCP 的可靠传输机制

TCP 通过以下机制保证数据传输的可靠性:

5.1 序列号与确认应答

每个报文段都带有唯一的 序列号 (SEQ),接收方回复 确认号 (ACK = 已收到的最后一个字节序号 + 1),告知发送方所有数据已收到。发送方若在超时时间内未收到确认,则重传该报文段。

5.2 滑动窗口

TCP 采用 滑动窗口 实现流量控制和高效传输。发送方维护一个窗口,窗口内的数据可以连续发送而不必等待逐个确认。窗口大小由接收方的接收窗口(rwnd)和拥塞窗口(cwnd)共同决定。

  • 发送窗口:允许发送但尚未确认的数据范围。
  • 接收窗口:接收方可用的缓冲区大小,在 ACK 中通告。

5.3 校验和

TCP 头和数据都参与校验和计算,接收方校验失败则丢弃报文段,相当于触发重传。

5.4 分片与排序

TCP 会根据网络 MTU 对数据进行合理分片,接收方将乱序到达的数据包缓存并按序重组,再交给应用层。


六、流量控制

流量控制 是端到端的机制,解决"收发速度不匹配"的问题。

TCP 通过滑动窗口实现流量控制:

  • 接收方在每次 ACK 中捎带 窗口大小(win) 字段,告知发送方自己还能接收多少数据。
  • 发送方根据该窗口值调整发送速率。
  • 如果接收方处理不及时,可以缩小窗口,甚至通告 win=0,让发送方停止发送。

七、拥塞控制

拥塞控制 是全局性的,防止过多的数据注入网络导致网络拥堵。TCP 使用以下四大算法:

7.1 慢启动(Slow Start)

连接刚建立时,拥塞窗口(cwnd)初始为 1 个 MSS(最大报文段长度),每收到一个 ACK,cwnd 加倍(指数增长),快速探测网络容量。

7.2 拥塞避免(Congestion Avoidance)

当 cwnd 达到慢启动阈值(ssthresh)后,进入拥塞避免阶段,cwnd 每经过一个 RTT(往返时间)线性增加 1 个 MSS(加法增大)。

7.3 快重传(Fast Retransmit)

当发送方连续收到 3 个重复的 ACK 时,立即重传丢失的报文段,而不必等待超时。

7.4 快恢复(Fast Recovery)

发生快重传后,TCP 将 ssthresh 设为当前 cwnd 的一半,cwnd 设为 ssthresh + 3 个 MSS,然后线性增长。


八、TCP Socket 编程接口

Linux 的 Socket API 是 POSIX 标准的一部分,几乎所有操作系统都支持。下面逐一介绍核心函数,包含原型、参数、返回值和使用示例。

8.1 socket() ------ 创建套接字

c 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 参数
    • domain:协议族,常用 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地)。
    • type:套接字类型,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。
    • protocol:通常设为 0,自动选择协议(TCP 为 IPPROTO_TCP)。
  • 返回值 :成功返回文件描述符(非负整数),失败返回 -1
c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket");
    exit(1);
}

8.2 bind() ------ 绑定本地地址

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 用途:将套接字与指定 IP 和端口绑定。服务器端通常需要绑定固定端口。
  • 参数
    • sockfd:socket 返回的描述符。
    • addr:指向 struct sockaddr_insockaddr_in6 的指针。
    • addrlen:地址结构大小。
  • 返回值 :成功 0,失败 -1
c 复制代码
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 所有本地 IP
servaddr.sin_port = htons(8888);

if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
    perror("bind");
    exit(1);
}

建议 :在 bind 前设置 SO_REUSEADDR,避免端口被占用(TIME_WAIT 导致)。

c 复制代码
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

8.3 listen() ------ 监听连接

c 复制代码
int listen(int sockfd, int backlog);
  • 用途:将主动套接字变为被动(监听)套接字,并设置已完成连接队列的最大长度。
  • 参数
    • sockfd:已绑定的套接字。
    • backlog:排队等待 accept 的连接数上限(通常设为 10~128)。
  • 返回值 :成功 0,失败 -1
c 复制代码
if (listen(sockfd, 10) < 0) {
    perror("listen");
    exit(1);
}

8.4 accept() ------ 接受连接

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 用途 :从监听套接字的完成队列中取出第一个连接,并生成一个新的已连接套接字,用于与该客户端通信。
  • 参数
    • sockfd:监听套接字。
    • addr:输出客户端地址信息(可为 NULL)。
    • addrlen:输入输出,传入大小,返回实际长度。
  • 返回值 :成功返回新套接字描述符,失败返回 -1
c 复制代码
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int connfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (connfd < 0) {
    perror("accept");
    exit(1);
}
printf("客户端 %s:%d 已连接\n",
       inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

8.5 connect() ------ 发起连接(客户端)

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 用途:客户端发起 TCP 三次握手,连接到指定服务器。
  • 参数 :与 bind 类似,地址为目标服务器。
  • 返回值 :成功 0,失败 -1
c 复制代码
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);

if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
    perror("connect");
    exit(1);
}

8.6 send() ------ 发送数据(TCP)

c 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 用途:通过已连接的套接字发送数据。
  • 参数
    • sockfd:已连接套接字。
    • buf:发送缓冲区。
    • len:要发送的字节数。
    • flags:通常 0,可用 MSG_DONTWAIT 等。
  • 返回值 :成功返回实际发送的字节数(可能小于 len),失败 -1
c 复制代码
char msg[] = "Hello TCP!";
ssize_t sent = send(connfd, msg, strlen(msg), 0);
if (sent < 0) {
    perror("send");
    exit(1);
}

注意:TCP 是流式协议,send 可能只发送部分数据,需循环发送直到全部发送完毕。


8.7 recv() ------ 接收数据(TCP)

c 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 用途:从已连接套接字接收数据。
  • 参数 :同 sendbuf 为接收缓存,len 为缓存大小。
  • 返回值 :成功返回实际接收的字节数(可能小于 len),返回 0 表示对方已关闭,失败返回 -1
c 复制代码
char buffer[1024];
ssize_t n = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (n < 0) {
    perror("recv");
    exit(1);
} else if (n == 0) {
    printf("连接关闭\n");
    close(connfd);
    return;
}
buffer[n] = '\0';
printf("收到: %s\n", buffer);

8.8 close()shutdown()

c 复制代码
int close(int fd);
int shutdown(int sockfd, int how);
  • close:关闭套接字,释放资源,若为最后一个引用则触发关闭流程。
  • shutdown :更精细地控制关闭方向:
    • how = SHUT_RD:不再接收
    • how = SHUT_WR:不再发送
    • how = SHUT_RDWR:两者都禁止

shutdown 不会释放套接字,需配合 close 使用。常用于优雅关闭。


8.9 地址转换辅助函数

  • inet_pton:将 IP 字符串转为网络字节序二进制(推荐)。
c 复制代码
int inet_pton(int af, const char *src, void *dst);
  • inet_ntop:将二进制 IP 转回字符串。
  • htons / ntohs:端口号转换(主机 ↔ 网络)。
  • htonl / ntohl:IP 地址转换。

九、完整示例:TCP 回射服务器与客户端

9.1 服务器端代码

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>

int main() {
    int listenfd, connfd;
    struct sockaddr_in servaddr, clientaddr;
    socklen_t len = sizeof(clientaddr);
    char buffer[1024];

    // 1. 创建 socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 设置端口重用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8888);
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind");
        exit(1);
    }

    // 4. 监听
    if (listen(listenfd, 10) < 0) {
        perror("listen");
        exit(1);
    }
    printf("服务器启动,监听端口 8888\n");

    // 5. 循环接受连接
    while (1) {
        connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
        if (connfd < 0) {
            perror("accept");
            continue;
        }
        printf("客户端 %s:%d 连接\n",
               inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

        // 回显所有收到的数据
        ssize_t n;
        while ((n = recv(connfd, buffer, sizeof(buffer)-1, 0)) > 0) {
            buffer[n] = '\0';
            printf("收到: %s\n", buffer);
            send(connfd, buffer, n, 0);   // 回显
        }
        close(connfd);
        printf("客户端断开\n");
    }
    close(listenfd);
    return 0;
}

9.2 客户端代码

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>

int main() {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[1024];

    // 1. 创建 socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);  // 本机测试

    // 3. 连接服务器
    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect");
        exit(1);
    }
    printf("连接服务器成功\n");

    // 4. 发送消息并接收回显
    printf("请输入消息: ");
    fgets(buffer, sizeof(buffer), stdin);
    send(sockfd, buffer, strlen(buffer), 0);

    ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);
    if (n > 0) {
        buffer[n] = '\0';
        printf("回显: %s\n", buffer);
    }

    // 5. 关闭连接
    close(sockfd);
    return 0;
}

十、总结

TCP 作为互联网最基础的传输协议之一,通过三次握手 建立连接、可靠传输 (序列号、确认重传、滑动窗口)保障数据完整性、流量控制拥塞控制平衡网络性能。理解这些原理,是网络编程和网络问题排查的基石。

在编程层面,Linux 提供的 Socket API(socket/bind/listen/accept/connect/send/recv/close 等)简洁而强大,掌握它们即可开发出稳定高效的 TCP 应用。实际项目中,还需进一步学习多线程/多进程并发、select/poll/epoll 事件驱动模型,以及超时处理等高级主题。

希望本文能帮助读者建立起从协议原理到代码实践的完整认知。若有疑问或建议,欢迎在评论区交流!


文章首发于 CSDN,转载请联系作者。

相关推荐
清钟沁桐25 分钟前
mlir 编译器学习笔记之十四 -- cpu算子自动生成
笔记·学习·mlir
speop29 分钟前
hell-gpu| TASK01-2
linux·运维·算法
minglie132 分钟前
多项式分裂域的自同构群的求法
学习
byte轻骑兵42 分钟前
【BlueZ 】用户态入口:main.c 核心函数与程序启动流程
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
唐古乌梁海1 小时前
传输协议https-TCP
网络协议·tcp/ip·https
每天题库1 小时前
机修钳工题库计算题专练,尺寸公差公式熟练应用
学习·安全·考试·题库·考证
Seraphina361 小时前
记一次实验:利用缓存服务器规范化技术进行网页缓存欺骗
经验分享·笔记·网络安全·缓存
dear_bi_MyOnly1 小时前
数组字符串深度解析:从入门到卡牌实战
开发语言·c++·学习
Navigator_Z1 小时前
LeetCode //C - 1223. Dice Roll Simulation
c语言·算法·leetcode