网络编程-TCP 协议学习笔记

一、网络通信模式

1. C/S 模式(客户端 / 服务器模式)

  • 核心思想:客户端主动发起请求,服务器被动接收并响应请求,是 TCP 通信的典型模式。
  • 特点:服务器需要长期运行,客户端按需启动;资源集中在服务器,客户端仅负责交互。
  • **交互逻辑:**客户端与服务器点对点专属通信。例如:QQ连接QQ服务器、打游戏

2. B/S 模式(浏览器 / 服务器模式)

  • 核心思想 :基于 HTTP 协议(底层仍为 TCP),以浏览器作为通用客户端,无需开发专用客户端。
  • 特点:跨平台性强,开发成本低;依赖网络和浏览器环境。
  • **交互逻辑:**浏览器向服务器发起请求,服务器响应后返回页面。例如淘宝、京东网页

3. P2P 模式(对等网络模式)

  • 核心思想:无中心服务器,每个节点既是客户端也是服务器,节点间直接通信。
  • 特点:去中心化,资源利用率高;节点管理和连接稳定性较难控制。

二、TCP 核心特征

  1. 面向连接 :通信前必须通过 "三次握手" 建立连接,通信结束需 "四次挥手" 释放连接。
  2. 可靠传输 :通过序列号、确认应答、重传机制、流量控制、拥塞控制保证数据不丢失、不重复、按序到达。
  3. 面向字节流 :以字节为单位传输数据,无数据边界(易引发黏包问题)。
  4. 全双工通信:同一连接中,双方可同时收发数据。
  5. 拥塞控制:通过慢启动、拥塞避免等算法,适配网络拥塞状态。

三、TCP 会话过程

1. 三次握手(建立连接)

SYN:同步标志、ACK:确认标志、FIN:结束标志、seq:序列号、ack:确认号

握手阶段 发起方 核心参数(TCP 报文段) 参数意义
第一次 客户端 SYN=1,seq=x 客户端向服务器发起连接请求,SYN 置 1 表示请求同步,seq 为客户端初始序列号
第二次 服务器 SYN=1,ACK=1,seq=y,ack=x+1 服务器确认客户端请求(ACK=1 表示确认,ack=x+1 表示期望接收下一字节),同时向客户端发起连接请求(SYN=1,seq=y)
第三次 客户端 ACK=1,seq=x+1,ack=y+1 客户端确认服务器的连接请求,连接正式建立

2. 四次挥手(释放连接)

挥手阶段 发起方 核心参数 参数意义
第一次 客户端 FIN=1,seq=u 客户端向服务器发送关闭连接请求(FIN=1 表示无数据要发送),seq 为当前序列号
第二次 服务器 ACK=1,seq=v,ack=u+1 服务器确认客户端的关闭请求,此时服务器仍可向客户端发数据
第三次 服务器 FIN=1,ACK=1,seq=w,ack=u+1 服务器无数据发送,向客户端发送关闭请求
第四次 客户端 ACK=1,seq=u+1,ack=w+1 客户端确认服务器的关闭请求,等待 2MSL 后释放连接

四、C/S 模式编程流程(以 Linux 为例)

通用前置:头文件

复制代码
#include <sys/socket.h>   // 套接字核心函数
#include <netinet/in.h>   // 地址结构体定义
#include <arpa/inet.h>    // IP 地址转换函数
#include <unistd.h>       // close 函数
#include <string.h>       // 内存操作(如 memset)

1. 服务器端流程

(1)创建 TCP 监听套接字:socket ()
  • 函数原型int socket(int domain, int type, int protocol);
  • 功能:创建一个套接字文件描述符,用于后续网络通信。
  • 参数
    • domain:地址族,TCP 用 AF_INET(IPv4)/AF_INET6(IPv6);
    • type:套接字类型,TCP 用 SOCK_STREAM(流式套接字),UDP 用 SOCK_DGRAM
    • protocol:协议类型,填 0 表示自动匹配 type 对应的默认协议(TCP 为 IPPROTO_TCP)。
  • 返回值:成功返回非负套接字描述符;失败返回 -1,设置 errno。
(2)绑定 IP + 端口:bind ()
  • 函数原型int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将套接字与指定的 IP 地址和端口号绑定。
  • 参数
    • sockfd:socket () 返回的套接字描述符;
    • addr:通用地址结构体指针,需强转为 struct sockaddr_in(IPv4)类型,包含 IP 和端口;
    • addrlen:地址结构体的长度,sizeof(struct sockaddr_in)
  • 返回值:成功返回 0;失败返回 -1,设置 errno。
(3)进入监听状态:listen ()
  • 函数原型int listen(int sockfd, int backlog);
  • 功能:将套接字转为监听状态,等待客户端连接请求。
  • 参数
    • sockfd:绑定后的套接字描述符;
    • backlog:半连接队列(未完成三次握手)的最大长度(如 5、10,具体值受系统限制)。
  • 返回值:成功返回 0;失败返回 -1,设置 errno。
(4)建立连接:accept ()
  • 函数原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能:阻塞等待客户端连接,建立后返回新的套接字用于与该客户端通信。
  • 参数
    • sockfd:监听套接字描述符;
    • addr:输出参数,存储客户端的 IP 和端口信息;
    • addrlen:输入输出参数,传入结构体长度,传出实际长度。
  • 返回值:成功返回新的通信套接字描述符;失败返回 -1,设置 errno。
(5)接收数据:recv ()
  • 函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 功能:从已连接的套接字接收数据。
  • 参数
    • sockfd:accept () 返回的通信套接字;
    • buf:存储接收数据的缓冲区;
    • len:缓冲区最大长度;
    • flags:接收方式,0 表示阻塞接收,MSG_DONTWAIT 表示非阻塞。
  • 返回值:成功返回实际接收的字节数;0 表示客户端关闭连接;-1 表示失败,设置 errno。
(6)发送数据:send ()
  • 函数原型ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 功能:向已连接的套接字发送数据。
  • 参数
    • sockfd:通信套接字描述符;
    • buf:待发送数据的缓冲区;
    • len:待发送数据的长度;
    • flags:发送方式,0 表示阻塞发送。
  • 返回值:成功返回实际发送的字节数;失败返回 -1,设置 errno。
(7)关闭连接:close ()
  • 函数原型int close(int fd);
  • 功能:关闭套接字描述符,释放资源。
  • 参数fd:待关闭的套接字描述符(监听 / 通信套接字)。
  • 返回值:成功返回 0;失败返回 -1,设置 errno。

代码:

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>

typedef  struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    //创建监听套接字
    int listfd = socket(AF_INET,SOCK_STREAM,0);
    if(listfd == -1)
    {
        perror("socket");
        return 1;
    }

    //创建服务器、客户端地址结构体
    struct sockaddr_in ser,cli;
    //清空结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));

    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;

    //给套接字绑定ip+port
    int ret = bind(listfd,(SA)&ser,sizeof(ser));
    if(ret == -1)
    {
        perror("bind");
        return 1;
    }

    //进入监听状态
    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    //建立连接-接收通信套接字
    int conn = accept(listfd,(SA)&cli,&len);
    if(conn == -1)
    {
        perror("accept");
        return 1;
    }

    while(1)
    {
        char buf[521] = {0};

        //接收数据
        int rd_ret = recv(conn,buf,sizeof(buf),0);
        if(rd_ret <= 0)
        {
            break;
        }

        //处理数据
        printf("cli:%s\n",buf);
        time_t tm;
        time(&tm);
        sprintf(buf, "%s %s",buf,ctime(&tm));

        //发送数据
        send(conn,buf,strlen(buf),0);
    }

    //断开连接
    close(listfd);
    close(conn);
    return 0;
}

2. 客户端流程

(1)创建套接字:socket ()
  • 同服务器端,参数一致(AF_INET + SOCK_STREAM + 0)。
(2)构造服务器地址结构体
复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;                // 地址族
server_addr.sin_port = htons(8080);              // 服务器端口(网络字节序)
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 IP
(3)主动连接服务器:connect ()
  • 函数原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:向服务器发起 TCP 连接请求(三次握手)。
  • 参数
    • sockfd:客户端套接字描述符;
    • addr:服务器地址结构体指针;
    • addrlen:地址结构体长度。
  • 返回值:成功返回 0;失败返回 -1,设置 errno。
(4)发送 / 接收数据:send ()/recv ()
  • 同服务器端,参数一致(使用客户端套接字)。
(5)关闭连接:close ()
  • 同服务器端。

代码:

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <arpa/inet.h>

typedef  struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    //创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    //创建服务器地址结构体
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("192.168.31.136");

    //主动连接服务器
    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if(ret == -1)
    {
        perror("connect");
        return 1;
    }

    int i = 30;
    while(i--)
    {
        char buf[512] = "hello,this is why";

        //发送数据
        send(sockfd,buf,strlen(buf),0);

        bzero(buf, sizeof(buf));

        //接收数据
        int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
        if(rd_ret <= 0)
        {
            break;
        }
        printf("from ser:%s\n",buf);
        sleep(1);
    }

    //断开连接
    close(sockfd);

    return 0;
}

五、TCP 黏包问题

1. 原因

  • TCP 是面向字节流,无数据边界,操作系统会根据缓冲区情况合并 / 拆分数据;
  • 发送方:操作系统会为了效率,把多次小数据合并成一个数据包发送(Nagle算法);
  • 接收方:缓冲区未及时读取,多个数据包被一次性读取。
  • 黏包 = TCP 字节流特性 + 发送方缓冲区攒包 + 接收方缓冲区累积

2. 现象

  • 接收方一次读取到多个发送方的数据包(如发送 "hello" + "world",接收 "helloworld");
  • 接收方一次读取到不完整的数据包(如发送 100 字节,只读取到 50 字节)。

3. 解决方案

  1. 固定长度包:双方约定数据包长度(如每次发 1024 字节),不足补空,接收方按固定长度读取;
  2. 添加分隔符:数据包末尾加特殊分隔符(如 \r\n),接收方按分隔符拆分;
  3. 自定义协议头 :包头包含数据长度(如 4 字节表示包体长度),接收方先读包头,再按长度读包体(最常用)。

六、TCP 与 UDP 对比

特性 TCP UDP
连接性 面向连接(三次握手) 无连接
可靠性 可靠(确认、重传、有序) 不可靠(无确认,易丢包)
数据边界 无(字节流),易黏包 有(数据报),无黏包
通信方式 全双工 单工 / 半双工(按需)
速度 慢(有拥塞 / 流量控制) 快(无额外控制)
适用场景 文件传输、网页加载、聊天 音视频直播、游戏、广播
核心函数 socket/bind/listen/accept socket/bind/sendto/recvfrom

总结

  1. TCP 核心是面向连接、可靠传输,C/S 编程需遵循 "创建套接字 - 绑定 - 监听 - 连接 - 收发 - 关闭" 流程,核心函数(socket/bind/listen/accept/connect/recv/send)需掌握原型、参数和返回值;
  2. 黏包问题源于 TCP 字节流特性,解决方案核心是给数据加边界(固定长度、分隔符、自定义协议头);
  3. TCP 与 UDP 核心差异在 "连接性" 和 "可靠性",需根据场景选择(可靠选 TCP,高速选 UDP)。
相关推荐
z10_142 小时前
动态住宅IP:数据爬虫与社媒营销的效率引擎
爬虫·网络协议·tcp/ip
FreeBuf_2 小时前
为何安全验证正迈向Agentic时代
网络·安全
蒸蒸yyyyzwd2 小时前
设计模式之美学习笔记
笔记·学习·设计模式
非凡ghost2 小时前
Smart Launcher安卓版(安卓桌面启动器)
android·windows·学习·音视频·软件需求
芯盾时代2 小时前
医疗行业网络安全的需求
网络·安全·web安全
智慧化智能化数字化方案2 小时前
向华为学习——解读华为工业与AI融合应用指南【】
人工智能·学习·华为工业与ai融合应用指南
乾元2 小时前
职业进阶: 传统安全工程师如何转型为 AI 安全专家?
网络·人工智能·安全·web安全·机器学习·网络安全·安全架构
yhdata2 小时前
OT安全工具软件发展提速:2032年市场规模锁定27.66亿元,赛道潜力加速释放
大数据·网络·人工智能·安全
盐水冰2 小时前
【烘焙坊项目】后端搭建(11)- 用户&商家订单板块
java·后端·学习