为什么要实现UDP可靠传输以及过程

一、TCP 协议有什么缺陷?

TCP 通过序列号、确认应答、超时重传、流量控制、拥塞控制等方式实现了可靠传输,看起来它很完美,实际上TCP 协议的缺陷主要有四个方面:

  1. 升级 TCP 的工作很困难;
  2. TCP 建立连接的延迟;
  3. TCP 存在队头阻塞问题;
  4. 网络迁移需要重新建立 TCP 连接;

升级 TCP 的工作很困难

TCP 协议是诞生在 1973 年,至今 TCP 协议依然还在实现更多的新特性。

但是 TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。

而升级内核这个工作是很麻烦的事情,麻烦的事情不是说升级内核这个操作很麻烦,而是由于内核升级涉及到底层软件和运行库的更新,我们的服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级也比较保守和缓慢。

很多 TCP 协议的新特性,都是需要客户端和服务端同时支持才能生效的,比如 TCP Fast Open 这个特性,虽然在2013 年就被提出了,但是 Windows 很多系统版本依然不支持它,这是因为 PC 端的系统升级滞后很严重,W indows Xp 现在还有大量用户在使用,尽管它已经存在快 20 年。

所以,即使 TCP 有比较好的特性更新,也很难快速推广,用户往往要几年或者十年才能体验到。

TCP 建立连接的延迟

基于 TCP 实现的应用协议,都是需要先建立三次握手才能进行数据传输,比如 HTTP 1.0/1.1、HTTP/2、HTTPS。

现在大多数网站都是使用 HTTPS 的,这意味着在 TCP 三次握手之后,还需要经过 TLS 四次握手后,才能进行 HTTP 数据的传输,这在一定程序上增加了数据传输的延迟。

TCP 三次握手和 TLS 握手延迟,如图:

请添加图片描述
TCP 三次握手的延迟被 TCP Fast Open (快速打开)这个特性解决了,这个特性可以在「第二次建立连接」时减少 TCP 连接建立的时延。
请添加图片描述
常规 HTTP 请求 与 Fast Open HTTP 请求

过程如下:

  • 在第一次建立连接的时候,服务端在第二次握手产生一个 Cookie (已加密)并通过 SYN、ACK包一起发给客户端,于是客户端就会缓存这个 Cookie,所以第一次发起 HTTP Get 请求的时候,还是需要 2 个 RTT 的时延;
  • 在下次请求的时候,客户端在 SYN 包带上 Cookie 发给服务端,就提前可以跳过三次握手的过程,因为 Cookie中维护了一些信息,服务端可以从 Cookie 获取 TCP 相关的信息,这时发起的 HTTP GET 请求就只需要 1 个 RTT的时延;

TCP Fast Open 这个特性是不错,但是它需要服务端和客户端的操作系统同时支持才能体验到,而 TCP Fast Open 是在 2013 年提出的,所以市面上依然有很多老式的操作系统不支持,而升级操作系统是很麻烦的事情,因此 TCP Fast Open 很难被普及开来。

还有一点,针对 HTTPS 来说,TLS 是在应用层实现的握手,而 TCP 是在内核实现的握手,这两个握手过程是无法结合在一起的,总是得先完成 TCP 握手,才能进行 TLS 握手。

也正是 TCP 是在内核实现的,所以 TLS 是无法对 TCP 头部加密的,这意味着 TCP 的序列号都是明文传输,所以就存安全的问题。

一个典型的例子就是攻击者伪造一个的 RST 报文强制关闭一条 TCP 连接,而攻击成功的关键则是 TCP 字段里的序列号位于接收方的滑动窗口内,该报文就是合法的。

为此 TCP 也不得不进行三次握手来同步各自的序列号,而且初始化序列号时是采用随机的方式(不完全随机,而是随着时间流逝而线性增长,到了 2^32 尽头再回滚)来提升攻击者猜测序列号的难度,以增加安全性。

但是这种方式只能避免攻击者预测出合法的 RST 报文,而无法避免攻击者截获客户端的报文,然后中途伪造出合法 RST 报文的攻击的方式。
请添加图片描述
大胆想一下,如果 TCP 的序列号也能被加密,或许真的不需要三次握手了,客户端和服务端的初始序列号都从 0 开始,也就不用做同步序列号的工作了,但是要实现这个要改造整个协议栈,太过于麻烦,即使实现出来了,很多老的网络设备未必能兼容。

TCP 存在队头阻塞问题

TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据。如下图:

请添加图片描述

图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 packet #3 在网络中丢失了,即使 packet #4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 packet #3 重传后,接收方的应用层才可以从内核中读取到数据。

这就是 TCP 队头阻塞问题,但这也不能怪 TCP ,因为只有这样做才能保证数据的有序性。

HTTP/2 多个请求是跑在一个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求,所以 HTTP/2 队头阻塞问题就是因为 TCP 协议导致的。
请添加图片描述

网络迁移需要重新建立 TCP 连接

基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。
请添加图片描述

TCP 四元组

那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。

而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

二、Java实现UDP可靠传输

TCP具有建立连接的延迟、存在队头阻塞问题、网络迁移需要重新建立 TCP 连接的痛点,所以为了保证通信的时延和质量的条件下尽量降低成本人们研究了在 UDP 之上做可靠保证,下面使用Java socket进行网络编程简单实现了UDP可靠传输。

思路:

  • 发送方的主要操作流程: 创建发送方的套接字,采用…端口号 ——> 构造数据报包,用来将长度为 length的包发送到指定主机上的指定端口号 ——> 从此套接字发送数据报包 ——> 接收者接收并返回数据 ——> 关闭此数据报套接字。
  • 接收方的主要操作流程: 接收操作并存储数据,并告诉发送者是否接收到。

代码:

package net;
 
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
 
public class demo {
    // 发送者--->客户端 客户端--->发送者
    // 发送者发给客户端数据,客户端返回数据给发送者
    public static void send() {
        // 发送端
        try {
            // 创建发送方的套接字 对象 采用9004默认端口号
            DatagramSocket socket = new DatagramSocket(9004);
            // 发送的内容
            String text = "hi xx!";
            byte[] buf = text.getBytes();
            // 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
            DatagramPacket packet = new DatagramPacket(buf, buf.length,
                    InetAddress.getByName("172.22.67.6"), 9001);
            // 从此套接字发送数据报包
            socket.send(packet);
            // 接收,接收者返回的数据
            displayReciveInfo(socket);
            // 关闭此数据报套接字。
            socket.close();
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
 
    public static void recive() {
        System.out.println("---recive---");
        // 接收端
        try {
            //创建接收方的套接字 对象  并与send方法中DatagramPacket的ip地址与端口号一致
            DatagramSocket socket = new DatagramSocket(9001,
                    InetAddress.getByName("172.22.67.6"));
            //接收数据的buf数组并指定大小
            byte[] buf = new byte[1024];
            //创建接收数据包,存储在buf中
            DatagramPacket packet = new DatagramPacket(buf, buf.length);
            //接收操作
            socket.receive(packet);
            byte data[] = packet.getData();// 接收的数据
            InetAddress address = packet.getAddress();// 接收的地址
            System.out.println("接收的文本:::" + new String(data));
            System.out.println("接收的ip地址:::" + address.toString());
            System.out.println("接收的端口::" + packet.getPort()); // 9004
 
            // 告诉发送者 我接收完毕了
            String temp = "我接收完毕了";
            byte buffer[] = temp.getBytes();
            //创建数据报,指定发送给 发送者的socket地址
            DatagramPacket packet2 = new DatagramPacket(buffer, buffer.length,
                    packet.getSocketAddress());
            //发送
            socket.send(packet2);
            //关闭
            socket.close();
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
 
    /**
     * 接收数据并打印出来
     */
    public static void displayReciveInfo(DatagramSocket socket)
            throws IOException {
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        socket.receive(packet);
 
        byte data[] = packet.getData();// 接收的数据
        InetAddress address = packet.getAddress();// 接收的地址
        System.out.println("接收的文本:::" + new String(data));
        System.out.println("接收的ip地址:::" + address.toString());
        System.out.println("接收的端口::" + packet.getPort()); // 9004
    }
 
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                recive();
            }
        }.start();
 
        new Thread() {
            @Override
            public void run() {
                send();
            }
        }.start();
    }
}

三、现有解决技术

现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了,那就是 QUIC 协议,已经应用在了 HTTP/3。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值