详解网络协议栈--TCP协议

TCP 协议

TCP协议是一种可靠的、面向连接的字节流服务,源主机在传送数据前需要先和目标主机建立连接。然后在此连接上被编号的数据段按序收发。同时要求对每个数据段进行确认,保证了可靠性。如果在指定的时间内没有收到目标主机对所发数据段的确认,源主机会重传发送该数据段

TCP头部

image

  • 头部封装
// TCP头 20字节
struct tcphdr {
    unsigned short srcport;     //源端口号
    unsigned short dstport;     //目的端口号
    unsigned int   seqnum;      //序列号32bit
    unsigned int   acknum;      //确认号
    unsigned short hdrlen:4,    //TCP头长度
                   resv:6;      //保留位
                   urg:1,       //紧急指针有效(urgent pointer)
                   ack:1,       //确认序号有效
                   psh:1,       //接收方应该尽快将这个报文段交给应用层
                   rst:1,       //复位链接
                   syn:1,       //同步序号用来发起连接
                   fin:1;       //发端完成发送任务,断开连接
    unsigned short winsize;     //窗口大小
    unsigned short checksum;    //检验和
    unsigned short urgpointer;  //紧急数据偏移量
};
  • 字段解释
    • 源端口和目的端口:TCP协议通过使用"端口"来标识源端和目标端的应用进程。端口号可以使用0到65535之间的任何数字。在收到服务请求时,操作系统动态地为客户端的应用程序分配端口号
    • 序列号:用来标识从源端向目的端发送的数据字节流,它表示在这个报文段中的第一个数据字节
    • 确认序号:只有ACK标志位为1时,确认号字段才有效,它包含目的端所期望收到源端的下一个数据字节
    • 头部长度:给出头部占32比特的数目,没有任何选项字段的TCP头部长度为20字节,最多可以有60字节的TCP头部
    • 标志位字段(U、A、P、R、S、F):各比特的含义如下:
      • URG:紧急指针(urgent pointer)有效
      • ACK:确认序号有效
      • PSH:接收方应该尽快将这个报文段交给应用层
      • RST:重建连接
      • SYN:发起一个连接
      • FIN:释放一个连接
    • 滑动窗口大小:用来进行流量制,单位为字节数,这个值是本机期望一次接收的字节数,最大为65535字节
    • TCP校验和:对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证
    • 紧急指针:它是一个偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号
    • 可选项字段:占32比特,可能包括"窗口扩大因子"、"时间戳"等选项。
TCP包结构
  • 代码表示
struct tcppacket {
    struct ethhdr eth;
    struct iphdr  ip;
    struct tcphdr tcp;
    unsigned char body[0]; 
};

三次握手
  • 流程
    • 源主机调用connect向目标主机发起连接,发送一个同步标志位(SYN)置1的TCP数据段,此段中同时标明初始序号ISN为一个随时间变化的随机值
    • 目标主机发回确认数据段,此段中的同步标志位(SYN)同样被置1,且确认标志位(ACK)也置1,同时在确认序号字段表明目标主机期待收源主机下一个数据段的序号(即表明前一个数据段已收到并且没有错误)此外此段中还包含目标主机的TCP数据段初始序号。
    • 源主机再回送一个数据段,同样带有递增的发送序号和确认序号,至此三次握手完成,连接建立成功,两端可进进一步进行数据收发

image

三次握手的思考
  • 第一个包即源主机发给目标主机的SYN中途被丢,没有到达目标主机,怎么办?

源主机会周期性超时重传,直到收到目标主机的确认

  • 第二个包即目标主机发给源主机的SYN+ACK中途被丢,没有到达源主机,怎么办?

目标主机会周期性超时重传,直到收到源主机的确认

  • 第三个包即源主机发给目标主机的ACK中途被丢,没有到达目标主机,怎么办?

源主机发完ACK,单方面认为TCP处于Established状态,而目标主机显然认为TCP为Active状态,此时可能有以下几种情况发生:

  • 假定此时双方都没有数据发送,目标主机会周期性超时重传,直到收到源主机的确认,收到之后目标主机的的TCP连接也变成Established状态,双向可以发包
  • 假定此时有源主机数据发送,目标主机收到源主机的 Data + ACK,自然会切换为Established 状态,并接受源主机的 Data
  • 假定目标主机有数据发送,数据发送不了,会一直周期性超时重传SYN + ACK,直到收到源主机的确认才可以发送数据
  • TCP连接为什么是三次握手,而不是2次或者4次?

模拟四次握手的过程:

  1. 源主机发送同步信号SYN + 源主机初始序列号给目标主机
  2. 目标主机发送确认数据段给源主机
  3. 目标主机发送同步信号SYN + 目标主机初始序列号给原主机主机
  4. 源主机发送确认数据段给目标主机

很显然2和3这两个步骤可以合并,只需要三次握手,提高连接的速度与效率。

模拟两次握手的过程:

  1. 源主机发送同步信号SYN + 源主机初始序列号给目标主机
  2. 目标主机发送同步信号SYN + 目标主机初始序列号 + ACK 给源主机

这里有一个问题,源主机与目标主机就源主机的初始序列号达成了一致,但是目标主机无法知道源主机是否已经接收到自己的同步信号,如果这个同步信号丢失了,
源主机和目标主机就目标主机的初始序列号将无法达成一致。于是TCP的设计者将SYN这个同步标志位SYN设计成占用一个字节的编号(FIN标志位也是),
既然是一个字节的数据,按照TCP协议对有数据的TCP segment 必须确认的原则,所以在这里源主机必须给目标主机一个确认,以确认源主机已经接收到目标主机的同步信号。

四次挥手
  • 流程
    • 源主机发送一个释放连接的标志位(FIN)为1的数据段发出结束会话请求
    • 目标主机回送一个数据段,并带有相应的发送序号和确认序号
    • 目标主机发送释放连接标志位(FIN)为1的数据段发出结束会话请求
    • 源主机回送一个数据段,并带有相应的发送序号和确认序号
    • 至此该次连接释放

image

四次挥手的思考
  • 假如server端一直不发送close,那么客户端的fin_wait_2状态如何消除?

server端会一直处于fin_wait_2, 无法终止,唯一能终止fin_wait_2的方法只能是kill掉server端进程;另外fin_wait_2没有终止的必要且不能终止,因为必须要按tcp的状态迁移流程来

  • time_wait状态存在的理由是?

time_wait状态存在有两个理由:

  • 为了保证主动关闭端发送的最后一个ACK能够到达对端。即假如最后一个ACK丢失,被动关闭端会超时重传一个FIN过来,然后主动关闭端收到在此发送ACK确认,同时启动2MSL定时器,如此下去。
    若没有2MSL等待时间,主动关闭端发送完ACK确认后就立即释放连接的话,则被动关闭端就无法重传(连接已断开),因而也就收不到确认,就无法按照步骤进入CLOSE状态
  • 防止已失效的连接请求报文段出现在后续的新连接中,避免对新连接造成错误。因为连接释放的过程中会有一些无效的报文段滞留在网络结点中,
    但是经过2MSL时间这些无效报文段都可以从网络中消失,这样的话在下一个新连接中就不会出现上一个连接遗留的报文段
  • MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失,2MSL时间一般维持在1-4min
  • 出现过多time_wait状态的原因是? 如何解决?

time_wait只会出现在主动关闭端,出现过多time_wait状态的原因是 timewait 状态时间设置过长
解决方法:

  • 将time_wait状态的时间设置短些
  • 将socket设置成可重用

可重用的作用是主动关闭端在timewait状态的2msl时间内,如果想重新与对端建立连接,可以直接重用上一连接的tcb结构体直接与对端重建连接,
不需要重新申请创建tcb结构体 如果不设置可重用,那么主动关闭端想要与对端重新建立连接 就需要等2msl时间走完后,进程释放原连接的tcb结构体,
然后在重新申请tcb,创建连接所以出现大量timewait,将socket设置成可重用,可以一定程度减少timewait的出现,因为断开重连可以直接重建连接,不需要等2msl时间走完再创建

  • 出现过多close_wait状态的原因是? 如何解决?

close_wait只会出现在被动关闭端,出现过多close_wait状态的原因是调用close的时机不对 或 当 recv == 0 时没有及时调用close
解决方法:当recv == 0 时及时调用close

  • 两端同时关闭时,状态如何变迁?

TCP协议允许连接双方同时执行主动关闭,这样当应用层发出关闭命令时,两端都从ESTABLISHED 变为 FIN_WAIT_1,这将导致两端各发送一个FIN,
两个FIN经过网络传输后分别到达另一端,收到FIN后,状态由FIN_WAIT_1变迁到CLOSING,并发送最后的ACK,当收到最后的ACK时,状态变化为TIME_WAIT,状态迁移如下图:

image

TCP状态迁移

image

延迟确认

TCP 通过延迟确认来保证传输数据有序
- 如何实现:
- 接收端接收到一个数据包后启动一个定时器(200ms),定时器时间内每接收到一个新包就重置定时器,直到定时器超时后,将接收到的数据进行排序,此时 acknum = 第一段连续数据包的最后一个包序号+1,接收端将acknum返回给发送端, 发送端将acknum之后的包全部进行重传,这样就可以保证顺序

滑动窗口

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

  • 实现:
    • 窗口合拢:窗口左边沿向右边沿靠近,这种现象发生在数据别发送和确认时
    • 窗口张开:窗口右边向右移动时将允许发送更多数据,这种现象发生在对端接收进程读取已经确认的数据并释放了TCP的接收缓存
    • 窗口收缩:窗口右边向左移动,一般不建议使用这方式

image

  • 分类

    • 发送端:拥塞窗口
    • 接收端:通告窗口
  • 滑动窗口大小计算

    • 根据RTT(一次请求的往返时间)确定,cwnd = 0.9 * old_rtt + 0.1 * cur_rtt (消抖的过程)
  • 滑动窗口左右边界计算

    • 窗口左边界 = acknum
    • 窗口右边界 = acknum + 窗口大小
拥塞控制算法
  • 慢启动
  • 拥塞控制
  • 快速重传
  • 快速恢复
粘包和分包
  • 解决方法:
    • 为TCP头加上长度
    • 在tcp数据包中加入分割符
常用结构体
// 通用地址结构
struct sockaddr {
    unsigned short sa_family;       // 地址族, AF_xxx
    char           sa_data[14];     // 14字节协议地址

};

// Internet协议地址结构
struct sockaddr_in {          
    unsigned short sin_family;      // 地址族, AF_INET,2 bytes
    unsigned short sin_port;        // 端口,2 bytes
    struct in_addr sin_addr;        // IPV4地址,4 bytes         
    char           sin_zero[8];     // 8 bytes unused,作为填充
};
常用接口
  • socket:建立一个socket通信(向系统注册通知系统建立一个通信端口)
函数原型:int socket(int domain, int type, int protocol);
函数参数:
    domain:地址族
        AF_INET     // internet 协议———广域网,互联网,外联网,局域网,内联网
        PF_UNIX     // unix internal协议———AF_LOCAL本地进程间通信
        PF_NS       // Xerox NS协议
        PF_IMPLINK  // Interface Message协议
    type:套接字类型
        SOCK_STREAM // 流式套接字
        SOCK_DGRAM  // 数据报套接字
        SOCK_RAW    // 原始套接字,直接和网络层通信
    protocol:具体协议, 通常置为0
返回值:成功则返回socket套接字,失败返回-1

  • connect:向服务器发起连接
函数原型:int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
函数参数:
    sockfd : socket返回的文件描述符
    serv_addr: 服务器端的地址信息 
    addrlen: serv_addr的长度
返回值:成功返回0, 失败则返回-1
  • bind:将socket绑定到本机的地址结构信息上,其中有IP地址和端口,绑定端口是为指引数据包发给对应哪个进程
函数原型:int bind(int sockfd, (struct sockaddr *)&my_addr, int addrlen);
函数参数:
    sockfd: socket调用返回的文件描述符
    my_addr:是指向 sockaddr_in 结构的指针,包含本机IP 地址和端口号
    addrlen: sockaddr地址结构的长度 = sizeof(struct sockaddr_in)
返回值:成功返回0;失败返回-1
  • listen:监听等待连接
函数原型:int listen(int sockfd, int backlog);
函数参数:
    sockfd:监听连接的套接字
    backlog:指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求。一般为5-10
    - backlog队列长度计算:
    - 半连接队列长度(mac系统 或 linux2.2之前)
    - 半连接队列长度 + 全连接队列长度(linux2.2之后)
    - 一般取值为 每秒连接接入量 / 20  (经验值)

返回值:成功返回0;失败返回-1
  • listen的底层实现细节:

    • 服务端处于监听状态,当接收到客户端发送的SYN 请求建立连接时,会创建一个连接节点,同时将该节点放入半连接队列中
  • DDOS攻击

    • DoS(拒绝服务)攻击即利用listen接口中backlog这个原理,通过客户端连接一直只发SYN非法的连接占用了全部的连接数,造成正常的连接请求被拒绝
    • 防御措施:在业务服务器外层加上物理防火墙,对连接进行预检测
  • accept:接受socket连接

函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数参数:
    sockfd: 监听的套接字
    addr: 客户端地址, 若不关心则填NULL
    addrlen:地址长度, 若不关心则填NULL
返回值:成功返回:已建立好连接的套接字,失败返回:-
  • accept的底层实现细节:
    • 当服务器接收到客户端返回的ACK后,会将半连接队列中对应的连接节点取出放入全连接队列中
    • 当全连接队列为空时,accept会一直阻塞等待,直到队列中有结点可取(队列非空)
    • accept接口返回,将从全连接队列中取出对应的节点,并为该节点分配一个fd,与节点一一对应

image

  • send:发送数据, 作用只是将数据从应用层拷贝到协议栈
函数原型:ssize_t send(int socket, const void *buffer, size_t length, int flags);
函数参数:
    socket:对端套接字
    buffer:发送缓冲区首地址
    length:发送的字节数
    flags: 发送方式(通常为0)
返回值:成功返回实际发送的字节数,失败则返回-1, 并设置errno
  • recv:接收数据 对端内核协议栈收到数据后通知应用程序, 应用程序调用recv将数据从内核协议栈拷贝到应用层
函数原型:ssize_t recv(int socket, const void *buffer, size_t length, int flags);
函数参数:
    buffer:接收缓冲区首地址
    length:接收的字节数
    flags:接收方式(通常为0)
返回值:成功返回实际接收的字节数,失败则返回-1, 并设置errno
  • close/shutdown:关闭套接字
函数原型: 
int close(int sockfd);    //读写通道都关闭
int shutdown(int sockfd, int howto);   //终止socket通信
函数参数:
    sockfd:套接字
    howto:
        0:关闭读通道,但是可以继续往套接字写数据。
        1:关闭写通道。只能从套接字读取数据。
        2:关闭读写通道,和close()一样
返回值:成功返回0, 失败返回-1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值