1.tcp协议

TCP(Transmission Control Protocol,传输控制协议) 是一种面向连接、可靠的、基于字节流的传输层协议,在 OSI 七层模型中处于传输层,是互联网中最核心的协议之一,为应用层(如 HTTP、FTP、SMTP 等)提供稳定的数据传输服务。

2.tcp协议的实现流程

在这里插入图片描述

2.1服务器端相关函数

  1. socket —— 创建套接字
    用于创建一个网络套接字,作为通信的 “端点”。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数说明:

  • domain:指定协议族,TCP 使用 AF_INET(IPv4)或 AF_INET6(IPv6)
  • type:设置套接字类型,TCP 应采用 SOCK_STREAM(流式套接字,提供可靠连接)
  • protocol:选择协议类型,TCP 通常设为 0(系统自动选择 IPPROTO_TCP)

返回值:

  • 成功:返回套接字描述符(非负整数值)
  • 失败:返回 -1参数说明:
  1. bind :绑定地址(套接字是具有ip+po
    将套接字与服务器的 IP 地址和端口号绑定,让客户端能找到服务器。
    运行
#include <sys/socket.h>
#include <netinet/in.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:由 socket 调用返回的套接字描述符
addr:指向 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)结构的指针,包含目标服务器的 IP 地址和端口信息
addrlen:地址结构体的实际长度(例如 sizeof(struct sockaddr_in)sockfd:由 socket 调用返回的套接字描述符
addr:指向 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)结构的指针,包含目标服务器的 IP 地址和端口信息
返回值:成功返回 0,失败返回 -1。

  1. listen —— 监听连接
    将套接字设置为监听模式,开始监听来自客户端的请求,并维护一个 “待处理连接队列”。
    这个监听队列里面有已完成三次握手和未完成三次握手的客户端
#include <sys/socket.h>
int listen(int sockfd, int backlog);

参数说明:

  • sockfd:已绑定的套接字描述符。
  • backlog:等待处理连接队列的最大容量(超出时新连接将被拒绝)。

返回值:

  • 成功:返回 0
  • 失败:返回 -1参数说明:
  1. accept —— 接受客户端连接
    从 listen 的待处理队列中取出一个已完成握手的连接,创建新的套接字描述符用于与该客户端通信(原监听套接字仍继续监听新连接)。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:监听套接字描述符。
  • addr:用于存储客户端IP和端口信息(可传入NULL忽略)。服务器接收客户端连接后,会将客户端信息存入该结构体数组。
  • addrlen:addr结构体的长度(调用前需初始化,例如sizeof(struct sockaddr_in))。
    返回值
    成功时返回新的套接字描述符(用于与客户端通信),失败时返回-1。sockfd:监听套接字描述符。
  1. send / recv:数据收发函数(与客户端通信)。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  1. close ——关闭套接字:关闭套接字,释放资源。
#include <unistd.h>
int close(int fd);

2.2代码(客户端和服务器端)

客户端

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
    exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);//杞负澶х
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
     int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
     if(res==-1)
     {
     printf("connect error\n");
     exit(1);
     }
     while(1)
     {
     printf("input:\n");
     char buff[128]={0};
     fgets(buff,128,stdin);
     if(strncmp(buff,"end",3)==0)
     {
        break;
     }
     send(sockfd,buff,strlen(buff)-1,0);
     memset(buff,127,sizeof(buff));
     recv(sockfd,buff,127,0);
     printf("buff=%s\n",buff);
    }
     close(sockfd);
     }

服务器端

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
    exit(1);
    }
    struct sockaddr_in saddr,caddr;//ipv4濂楁帴瀛楀湴鍧€
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);//杞负澶х
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        printf("bind err\n");
        exit(1);
    }
    res=listen(sockfd,5);//5鏄洃鍚槦鍒楃殑澶у皬
    if(res==-1)
    {
        exit(1);
    }
    while(1)
    {
    int len=sizeof(caddr);
    int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
     if(c<0)
     {
        continue;
     }
     printf("accept c=%d\n",c);
     while(1)
     {
     char buff[128]={0};
     int n=recv(c,buff,127,0);
     if(n<=0)
     {
     break;
     }
     printf("n=%d,recv:%s\n",n,buff);
     send(c,"ok",3,0);
     }
     close(c);
     printf("client close\n");
}
}

现在有一个问题就是如何实现多个客户端连接同一个服务器?
方法:
①多线程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
void *fun(void *arg)
{
    int *p=(int *)arg;
    int c=*p;
    free(p);
    while(1)
     {
     char buff[128]={0};
     int n=recv(c,buff,127,0);
     if(n<=0)
     {
     break;
     }
     printf("n=%d,recv:%s\n",n,buff);
     send(c,"ok",3,0);
     }
     close(c);
     printf("client close");
}


int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
    exit(1);
    }
    struct sockaddr_in saddr,caddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        printf("bind err\n");
        exit(1);
    }
    res=listen(sockfd,5);//5鏄洃鍚槦鍒楃殑澶у皬
    if(res==-1)
    {
        exit(1);
    }
    while(1)
    {
    int len=sizeof(caddr);
    int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
     if(c<0)
     {
        continue;
     }
     printf("accept c=%d\n",c);
     //创建线程,来处理客户端发送过来的数据
     int *p=malloc(sizeof(int));
     *p=c;
     pthread_t id;
     //创建线程,此时需要把新的已连接客户端的套接字传给这个线程
     pthread_create(&id,NULL,fun,(void *)p);
   }
}
②多进程
int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        exit(1);
    }
    struct sockaddr_in saddr,caddr;//ipv4濂楁帴瀛楀湴鍧€
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);//杞负澶х
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//灏嗗瓧绗︿覆褰㈠紡鐨刬pv4鍦板潃杞崲涓?2浣嶇殑缃戠粶瀛楄妭搴忔暣鏁?
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        printf("bind err\n");
        exit(1);
    }
    res=listen(sockfd,5);//5鏄洃鍚槦鍒楃殑澶у皬
    if(res==-1)
    {
        exit(1);
    }
    while(1)
    {
        int len=sizeof(caddr);
    int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//绗簩涓鎺ュ瓙鍦拌川锛氫负浜嗗瓨鍌ㄥ鎴风鐨勫鎺ュ瓧鍦拌川淇℃伅
     if(c<0)
     {
        continue;
     }
     pid_t pid=fork();
     if(pid==-1)//创建失败
     {
        close(c);
        continue;
     }
     if(pid==0)//子进程
     {
        while(1)
        {
 t
        char buff[128]={0};
        int n=recv(c,buff,127,0);
        if(n<=0)
        {
            break;
        }
        printf("recv:%s",buff);
        send(c,"ok,2,0");
    }
    printf("client close\n");
    close(c);
    exit(0);//作用:处理完这个客户端就直接退出
    }
     close(c);
    }
}

3.udp协议实现

  1. 创建 UDP 套接字socket()
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 功能:创建网络套接字(文件描述符)用于 UDP 通信
  • 参数
    • domain:地址族,UDP 使用 AF_INET(IPv4)或 AF_INET6(IPv6)
    • type:套接字类型,UDP 必须设为 SOCK_DGRAM
    • protocol:协议类型,UDP 设为 0(系统自动选择 IPPROTO_UDP
  • 返回值:成功返回套接字描述符(≥0),失败返回 -1(需检查 errno
  1. 绑定本地地址bind()
#include <sys/socket.h> 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将套接字与本地 IP 和端口绑定(服务器必须绑定,客户端可选)
  • 参数
    • sockfd:套接字描述符
    • addr:指向本地地址结构(sockaddr_insockaddr_in6
    • addrlen:地址结构长度(如 sizeof(struct sockaddr_in)
  • 返回值:成功返回 0,失败返回 -1
  1. 发送 UDP 数据sendto()
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • 功能:向指定目标发送 UDP 数据报(无需建立连接)
  • 参数
    • sockfd:UDP 套接字描述符
    • buf:待发送数据缓冲区
    • len:数据长度(字节)
    • flags:发送标志(通常为 0)
    • dest_addr:目标地址结构(含 IP 和端口)
    • addrlen:目标地址结构长度
  • 返回值:成功返回发送字节数,失败返回 -1
  1. 接收 UDP 数据recvfrom()
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • 功能:接收 UDP 数据报并获取发送方地址(可选)
  • 参数
    • sockfd:UDP 套接字描述符
    • buf:接收数据缓冲区
    • len:缓冲区最大容量
    • flags:接收标志(通常为 0)
    • src_addr:存储发送方地址的结构(NULL 表示不需要)
    • addrlen:输入为缓冲区长度,输出为实际地址长度(需传指针)
  • 返回值:成功返回接收字节数,失败返回 -1(UDP 通常不返回 0)
  1. 关闭套接字close()
#include <unistd.h>
int close(int fd);
  • 功能:释放套接字资源
  • 参数:套接字描述符
  • 返回值:成功返回 0,失败返回 -1

辅助函数(地址处理)

  • inet_pton():推荐使用,支持 IPv4/IPv6,将字符串 IP 转为二进制格式
  • htons()/htonl():主机字节序 → 网络字节序(端口/IP)
  • ntohs()/ntohl():网络字节序 → 主机字节序(端口/IP)(1)创建 UDP 套接字:socket()

3.1函数实现

3.2代码

服务器端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
int main()
{
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd==-1)
    {
        exit(1);
    }
   
    struct sockaddr_in saddr,caddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    saddr.sin_port=htons(6000);

    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        printf("bind err\n");
        exit(1);
    }
    while(1)
    {
        int len=sizeof(caddr);
        char buff[128]={0};
        recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len);
        printf("buff=%s",buff);
        sendto(sockfd,"ok",2,0,(struct sockaddr *)&caddr,sizeof(caddr));
    }
    close(sockfd);
}

客户端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
int main()
{
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd==-1)
    {
        exit(1);
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    saddr.sin_port=htons(6000);
    while(1)
    {
        char buff[128]={0};
        printf("input:\n");
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
        sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&saddr,sizeof(saddr));
        memset(buff,0,128);
        int len=sizeof(saddr);
        recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len);
        printf("buff=%s\n",buff);

    }
    close(sockfd);
}

4.tcp协议的应用

4.1http协议

HTTP(Hypertext Transfer Protocol)是一种应用层协议,专门用于在计算机网络上传输超文本内容(如HTML文档及其包含的图片、视频等资源)。它作为Web浏览器与服务器之间通信的基础规范,定义了数据传输的规则和标准。

该协议基于TCP传输层协议工作,采用经典的客户端-服务器模型:由客户端主动发起连接请求,经过TCP三次握手建立可靠连接后,才能发送HTTP请求报文。

HTTP服务的默认监听端口为80(服务器端),而客户端端口则由系统动态分配。在用户未显式指定端口时,浏览器会自动附加80端口进行访问。值得注意的是,当前主流的HTTPS协议使用443作为默认端口,若尝试通过80端口访问HTTPS服务通常会被服务器拒绝。

4.2http协议实现的流程

在这里插入图片描述

4.2myhttp协议

请求报文:浏览器向服务器在这里插入图片描述
http的请求方法:
HTTP/1.1协议中共定义了八种方法(有时也叫“动作”),来表明Request-URL指定的资源不同的操作方式
在这里插入图片描述

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法

应答报文:
在这里插入图片描述
应答状态码
在这里插入图片描述

5.tcp协议和udp协议的特点

5.1TCP特点:

  1. 面向连接
    通信前必须通过 “三次握手” 建立连接,通信结束后通过 “四次挥手” 释放连接,双方需维护连接状态(如序号、窗口大小等)。
    三次握手:确保双方收发能力正常,同步初始序号(ISN),避免历史连接干扰。
    四次挥手:确保双方数据都已传输完毕,优雅释放资源,避免数据丢失。
  2. 可靠传输
    TCP 通过一系列机制保证数据可靠送达,核心手段包括:
    确认与重传:接收方收到数据后发送 ACK 确认,发送方若超时未收到 ACK,则重传数据(基于超时重传或快速重传机制)。
    序号与确认号:每个字节都有唯一序号,接收方通过确认号告知发送方 “已正确接收至某序号”,确保数据不丢失、不重复。
    校验和:首部和数据均计算校验和,接收方校验出错误则丢弃数据,触发发送方重传。
    流量控制:通过滑动窗口机制,接收方根据自身缓冲区大小(接收窗口)限制发送方的发送速率,避免接收方溢出。
    拥塞控制:发送方根据网络拥塞状态(如丢包)动态调整发送速率(慢启动、拥塞避免、快速恢复等算法),避免网络过载。
  3. 面向字节流
    数据被视为连续的字节流,而非独立的数据报。发送方可以分多次写入数据,TCP 会将其合并或拆分(基于 MSS,最大段大小)为合适的报文段传输。
    接收方会将乱序的报文段重新排序(基于序号),拼接成完整的字节流提交给应用层,确保数据顺序与发送一致。

5.2 UDP特点:

  1. 无连接
    通信前不需要建立连接(无需像 TCP 那样三次握手),发送方直接通过 sendto() 向目标地址发送数据报,接收方通过 recvfrom() 接收,双方无需维护连接状态。
    优势:减少了连接建立 / 释放的开销,通信延迟更低。
    劣势:无法保证数据报的到达顺序、完整性,也不知道对方是否在线。
  2. 不可靠传输
    不保证送达:数据报发送后,UDP 不提供确认机制(无 ACK),发送方无法知道接收方是否收到,可能因网络拥塞、链路故障等导致丢失。
    不保证顺序:多个数据报可能通过不同路径传输,到达接收方的顺序可能与发送顺序不一致(TCP 靠序号保证顺序,UDP 无此机制)。
    无重传机制:数据报丢失后,UDP 不会自动重传,需依赖应用层实现重传逻辑(如实时视频中可丢弃旧帧,不重传)。
    无流量控制和拥塞控制:发送方不会根据网络状况调整发送速率,可能导致接收方缓冲区溢出(丢包)或网络拥塞加剧。
  3. 面向数据报
    数据以 “数据报” 为单位传输,每个数据报包含完整的源地址和目标地址,独立处理。
    发送方调用一次 sendto() 发送的数据,接收方必须通过一次 recvfrom() 完整接收(若缓冲区不足,可能截断数据,剩余部分丢弃)。
    数据报大小有限制(受 IP 层 MTU 限制,通常最大为 65507 字节),超过则会被分片,增加丢失风险。
  4. 开销小、效率高
    UDP 首部仅 8 字节(包含源端口、目标端口、长度、校验和),远小于 TCP 的 20~60 字节首部,协议开销低。
    无需维护连接状态(如序号、窗口、重传计时器等),对系统资源(CPU、内存)消耗少,适合高频次、小数据量传输。
  5. 支持广播和多播
    UDP 允许向同一网络中的所有主机(广播)或特定组主机(多播)发送数据,而 TCP 仅支持点对点通信。
    典型场景:局域网内的设备发现(如 DHCP)、多媒体广播(如直播、组播会议)。

5.3TCP 和 UDP 区别:

  1. 连接

TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。
2. 服务对象

TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信
3. 可靠性

TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议
4. 拥塞控制、流量控制

TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
5. 首部开销

TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 传输方式

TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7. 分片不同

TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

6.tcp/ip协议中常见的问题

6.1透彻理解三次握手和四次挥手

三次握手
在这里插入图片描述
一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN状态
客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
四次挥手
在这里插入图片描述
服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
在这里插入图片描述
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
在这里插入图片描述
注意:第三次握手是可以携带数据的,前两次握手是不可以携带数据的

5.2为什么采用三次握手而非两次握手?主要原因如下:

① 三次握手阻止历史连接初始化,从而避免资源浪费(核心原因):如果因为网络阻塞而存在历史连接,俩次握手的情况下,(被动发起方)没有中间状态给(主动发起方)来阻止历史连接,导致可能建立一个历史连接,导致被动发起方可能建立一个历史连接,造成资源浪费(俩次握手,第一次被动方就已经进入了ESTABLISHED状态,所以就直接分配资源了,可以进行发送数据了,但是主动方并没有进入ESTABLISHED状态,所以它收到这个历史连接会判断是否为自己期望的,如果不是,就会发会发送一个RST报文终止连接,所以被动方只有收到这个RST报文才知道断开连接)

② 确保双方初始序列号同步:发送必须要ack确认回复,当一端发送携带SEQ(序列号)的报文时,另一方通常需要通过ACK(确认应答号)来进行确认,这是 TCP 实现可靠传输的核心机制之一。
序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

看看三次握手是如何阻止历史连接的:

在这里插入图片描述
是如何避免资源浪费的?
俩次握手流程中,服务端尽在接收到客户端的SYN后,就会直接进入ESTABLISHED状态并发送数据,如果此时存在历史连接(因网络哦阻塞的连接)延迟到达,服务端会将它当作新连接处理,然后客户端发现这恶鬼ack并不是自己期望得到的,那么就会发送RST中断连接。结果就是白建立连接,白发送数据,浪费了资源。

但是三次握手也会有发送RST报文中断历史连接,那为什么不说浪费资源呢?
首先俩次握手流程中,服务器端接受到SYN报文,服务器端就会进入ESTABLISHED状态,同时分配资源、发送业务数据。发送业务数据,因为此时时历史连接,会被客户端的RST报文中断而无效,这俩个操作都是实质性的资源消耗。

三次握手的资源节约:“仅验证连接,不提前消耗业务资源”

三次握手的关键改进是把“资源分配和业务数据发送”推迟到客户端的 ACK 之后。
旧 SYN 到达后,服务端仅会:回复 SYN,ACK (这一步只是“验证连接请求”,消耗的资源极少); 不会分配业务资源,也不会发送业务数据。当客户端回复 RST 中断历史连接时,服务端仅损失了一个 SYN,ACK 报文的微小开销,避免了“业务资源和数据”的大额浪费。
两次握手是**“服务端单方面确认后就投入业务资源”,而三次握手是“服务端先发起验证,等客户端确认后再投入业务资源”**。
前者在历史连接场景下会导致“业务级资源浪费”,后者仅产生“控制级报文开销”,从而解决了历史连接的资源浪费问题。

5.3四次握手为什么可以变为三次?

在这里插入图片描述
三次挥手触发的条件:
如果服务器此时也恰好没有剩余数据需要发送,并且也准备关闭连接,那么服务器可以将 “对客户端 FIN 的确认(ACK)” 和 “自己的关闭请求(FIN)” 合并到同一个 TCP 报文中发送。

5.4time_wait问题

在这里插入图片描述

5.3粘包问题和解决方法

粘包:客户端多次发送的数据服务器端一次性接收到。
解决方法:给数据包添加明确的边界,让接收方能够正确拆分出独立的数据包。
①固定长度数据包
原理:约定每个数据包的长度固定(如 1024 字节)。
实现:
发送方:不足固定长度时用空字符填充,确保每次发送固定字节数。
接收方:每次从缓冲区读取固定长度的数据,作为一个完整数据包。
缺点:灵活性差,不适合数据长度差异大的场景(浪费带宽)。
②自定义分隔符
原理:在每个数据包末尾添加一个特殊分隔符(如 \r\n、### 等)。
实现:
发送方:每个数据包后附加分隔符。
接收方:读取数据时,以分隔符为标志拆分数据包。
缺点:若数据本身包含分隔符(如文本中出现 \r\n),会导致误拆分,需额外处理转义。
③长度前缀法(最常用)
原理:在每个数据包前添加一个 “长度字段”(如 4 字节整数),表示后续数据的字节数。
实现:
发送方:先发送数据长度(如 len=5),再发送实际数据(如 hello)。
接收方:
先读取长度字段(得到 5)。
再精确读取 5 字节数据,作为一个完整数据包。
优点:无歧义,适合任意类型数据(文本、二进制),是工业级常用方案。
4. 协议格式定义
原理:通过更复杂的协议规范(如 HTTP、Protobuf)定义数据包结构,包含头部(含长度、类型等信息)和数据体。
示例:HTTP 协议通过 Content-Length 字段指定 Body 长度,或用 Transfer-Encoding: chunked 分块传输。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐