回到顶部 暗色模式

TCP、UDP和DNS简介

1. TCP

        TCP是一种面向连接的 ( $connection-oriented$ ) 协议,在一个应用进程向另一个应用进程发送数据之前,这两个进程必须先相互握手。TCP建立的连接是一条逻辑链接,其共同状态仅保留在两个通信端系统的TCP程序中。由于TCP协议只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机)中运行,所以中间的网络元素不会维持TCP连接状态。事实上,中间路由器看到的是数据报,而非TCP连接。
        一旦建立起一条TCP连接,两个应用进程之间就可以相互发送数据了。TCP接收到数据后,会将数据引导到连接的发送缓存 ( $send\ \ buffer$ ) 里,发送缓存是发起三次握手期间设置的缓存之一。接下来TCP就会时不时从发送缓存里取出一块数据,并将数据传递到网络层。TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度 ( $Maximum\ \ Segment\ \ Size$ ,$MSS$ )。MSS通常根据最初确定的由本地发送主机发送的最大链路层帧长度( 即所谓的最大传输单元 ( $Maximum\ \ Transmission\ \ Unit$ ,$MTU$ )) 来设置。以太网和PPP链路层协议都具有 $1500$ 字节的MTU,再加上TCP/IP首部长度 ( 通常为 $40$ 字节 ) ,因此MSS的典型值为 $1460$ 字节。
        TCP为每块客户端数据配上一个TCP首部,从而形成多个TCP报文段 ( $TCP\ \ segment$ )。这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中。然后这些IP数据报被发送到网络中。当TCP在另一端接收到一个报文段之后,该报文段的数据就被放入该TCP连接的接收缓存中。

TCP报文段结构

1.1 序号和确认号

        TCP报文段首部中两个最重要的字段是序号字段和确认号字段。TCP把数据看成一个无结构的、有序的字节流。序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。一个报文段的序号 ( $sequence\ \ number\ \ for\ \ a\ \ segment$ ) 是该报文段首字节的字节流编号。
        TCP是全双工协议,因此主机 $A$ 在向主机 $B$ 发送数据的同时,也许也接收来自主机 $B$ 的数据。从主机 $B$ 到达的每个报文段中都有一个序号用于从 $B$ 流向 $A$ 的数据。主机 $A$ 填充进报文段的确认号是主机 $A$ 期望从主机 $B$ 收到的下一字节的序号。假设主机 $A$ 已收到一个来自主机 $B$ 的包含字节 $0 \sim 535$ 的报文段,以及另一个包含字节 $900 \sim 1000$ 的报文段。由于某种原因,主机 $A$ 还没有收到字节 $536 \sim 899$ 的报文段。在这个例子中,主机 $A$ 为了重新构建主机 $B$ 的数据流,仍在等待字节 $536$ 。因此 $A$ 到 $B$ 的下一个报文段将在确认号字段中包含 $536$ 。因为TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供累计确认 ( $cumulative\ \ acknowledge$ )。当主机 $A$ 在收到第二个报文段 ( $536 \sim 899$ ) 之前收到第三个报文段 ( $900 \sim 1000$ ),这时候的处理方式TCP RFC并没有明确规定任何规则,编程人员有两种处理方式:丢弃或者缓存。显然,后一种选择对网络带宽而言更为有效,也是实践中采用的方法。
        主机 $A$ 和主机 $B$ 之间数据的确认可以承载在数据的报文段中,而不需要重新发一个独立的确认报文段,这种确认称为捎带 ( $piggybacked$ )。主机之间也可以发送空报文段,通常用于确认已经收到数据。对于空报文段,虽然没有数据,但仍然需要分配一个序号,因为TCP中存在序号字段,报文段需要填入序号。

1.2 可靠数据传输

        因特网的网络层服务 ( IP服务是不可靠的 )。对于IP服务,数据报能够溢出路由器缓存而永远不能到达目的地,数据报也可能是乱序到达的,而且数据报中的比特可能损坏。
        TCPIP不可靠的尽力而为服务之上创建了一种可靠数据传输服务 ( $reliable\ \ data\ \ transfer\ \ service$ )。TCP的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、无间隙、非冗余和按序的数据流。TCP在每次发送报文的时候都会设置一个定时器,当定时器超时时就会重传报文。每当有超时事件发生,TCP会重传具有最小序号的还未被确认的报文段,但是每次重传时都会将下一次的超时间隔设为先前值的两倍。每当重新收到ACK时,超时间隔就又会被重新计算。
        超时触发重传存在的问题之一是超时周期可能相对较长,因而增加了端到端时延。发送方通常可在超时事件发生之前通过注意所谓的冗余ACK ( $duplicate\ \ ACK$ ) 来检测丢包。冗余ACK就是再次确认某个报文段的ACK。因为发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余ACK。如果TCP发送方接收到对相同数据的 $3$ 个冗余ACK,就会执行快速重传 ( $fast\ \ retransmit$ ),即在该报文段的定时器过期之前重传丢失的报文段。

1.3 流量控制

        TCP为它的应用程序提供了流量控制服务 ( $flow-control\ \ service$ ) 以消除发送方使接收方缓存溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。TCP发送方也可能因为IP网络的拥塞而被遏制,这种形式的发送方的控制被称为拥塞控制 ( $congestion\ \ control$ )。
        TCP让发送方维护一个接收窗口 ( $revive\ \ window$ ) 的变量来提供流量控制。通俗地说,接收窗口用于给发送方一个指示——该接收方还有多少可用的缓存空间。为了避免缓存溢出,缓存大小应该不小于应用程序从缓存读出的数据流的最后一个字节的编号与放入缓存中的数据流的最后一个字节编号的差。主机 $A$ 根据主机 $B$ 提供的剩余缓存窗口大小,计算将要发送的数量的大小,如果不大于则发送,否则阻塞。当主机 $B$ 的接收窗口为 $0$ 时,主机 $A$ 会定时发送一个只有一个字节数据的报文段,用于判断主机 $B$ 的剩余窗口大小是否改变。

1.4 连接建立和拆除

        连接可以通过以下方式建立:

  1. 客户端TCP首先向服务端TCP发送一个特殊的TCP报文段。报文段首部SYN标志位置 $1$ ,同时客户端会随机选择一个初始序号 ( $client_-isn$ ) 作为报文段序号;
  2. 服务器接收到报文段,为连接分配缓存和变量,并向该客户TCP发送报文段。报文段的SYN标志位和ACK标志位置 $1$ ,确认号置为 ( $client_-isn + 1$ ),服务器随机选择自己的初始序号 ( $server_-isn$ ) 作为报文段序号;
  3. 客户端为TCP连接分配缓存和变量,并向服务器发送报文段。最后一个报文段的SYN标志位置 $0$ ,ACK标志位置 $1$ 。

        完成三次握手之后,每个报文段的SYN标志位都会置 $0$ 。
        当主机 $A$ 想要关闭连接时,会先发出一个FIN标志位置 $1$ 的报文段,主机 $B$ 收到之后会发送一个ACK报文段,然后再发送FIN报文段。最后,主机 $A$ 再发送ACK报文段,表示连接释放。

1.5 拥塞控制

        运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,即拥塞窗口 ( $congestion\ \ window$ ),限制发送方能向网络中发送流量的速率。
        当一条TCP连接开始时,拥塞窗口大小通常初始置为一个MSS的较小值。在慢启动 ( $slow-start$ ) 状态,每当传输的报文段首次被确认就增加一个MSS。在慢启动的过程中,发送方还会维护一个变量称为慢启动阈值,每当发生超时事件 ( 即拥塞 ) 时,这个慢启动阈值就会设为当前拥塞窗口的一半,同时拥塞窗口会重新设为初始值。当拥塞窗口大小增长为慢启动阈值的大小时,TCP不会再使用慢启动,而是每接收到等同于拥塞窗口大小的数据时增加一个MSS,这时TCP进入拥塞避免状态。
        另一种结束慢启动的方式是接收到三个冗余ACK的情况,这种情况下会执行快速重传,并进入快速恢复状态。在快速恢复中,拥塞窗口大小和慢启动阈值会设为原拥塞窗口大小的一半,并且对于引起TCP进入快速恢复状态的缺失报文段,每收到一个冗余ACK,就增加一个MSS。最终,当对丢失报文段的一个ACK到达时进入拥塞避免状态。如果出现超时,则会迁移到慢启动状态。

1.6 状态

        在一个TCP连接的生命周期内,运行在每台主机中的TCP协议在各种TCP状态 ( $TCP\ \ state$ ) 之间变迁。客户TCP开始时处于 $CLOSED$ 状态。

客户TCP经历的典型的TCP状态序列

        假设客户应用程序决定要关闭连接,将会发送一个FIN报文段并进入 $FIN_-WAIT_-1$ 状态,收到服务器的ACK报文段后进入 $FIN_-WAIT_-2$ 状态。再次收到服务器的FIN报文段后,客户端会对服务器的报文段进行确认,并进入 $TIME_-WAIT$ 状态。在这个状态中,如果ACK丢失,客户端可以重传。经过一段时间的等待后,连接正式关闭,客户端的所有资源将被释放。
        与客户端一样,服务器也有一系列状态:

服务器端TCP经历的典型的TCP状态序列

        当服务器正在监听客户发送其SYN报文段端口时,如果其收到的报文段端口号或源IP地址与该主机上进行中的套接字都不匹配的情况下,主机将向源发送一个特殊重置报文段,即RST标志位置 $1$ ( 如果是UDP,将会发送ICMP数据报 ) 。

1.7 标志位

        除了SYNACKFIN这些用于建立和关闭连接的标志位外,TCP还提供了其他标志位:

2. UDP

        UDP只是做了运输协议能做的最少工作,除了复用/分解功能以及少量的差错检测外,它几乎没有对IP增加别的东西。实际上,如果应用程序开发人员选择UDP而不是TCP,则该应用程序差不多就是直接与IP打交道。在使用UDP发送报文段之前,发送方和接收方的运输层实体之间没有握手,因此,UDP被称为是无连接的DNS是一个通常使用UDP应用层协议的例子,当主机想要进行一次DNS查询时,将会构造一个查询报文并通过UDP发送。
        虽然TCP提供了可靠的数据传输服务,但是有些应用更适合UDP,原因如下:

应用 应用层协议 传输层协议
电子邮件 SMTP TCP
远程终端访问 Telnet TCP
Web HTTP TCP
文件传输 FTP TCP
远程文件服务器 NFS 通常UDP
流式多媒体 通常专用 UDPTCP
域名服务 DNS 通常UDP

3. Java套接字

        TCP

public class Server {
    public Server(int port) throws IOException {
        ServerSocket serverSocket = new ServerSocket(port);
        while (true) {
            Socket socket = serverSocket.accept();
            new Thread(new HandleAClient(socket)).start();
        }
    }

    private static class HandleAClient implements Runnable {
        private final Socket socket;

        public HandleAClient(Socket socket) { this.socket = socket; }

        @Override
        public void run() {
            try {
                DataInputStream in = new DataInputStream(socket.getInputStream());
                System.out.println(in.readDouble());
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Client {
    public Client(String host, int port) throws IOException {
        Socket socket = new Socket(host, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        Scanner scanner = new Scanner(System.in);
        out.writeDouble(scanner.nextDouble());
        socket.close();
    }
}

        UDP

public class Server {
    public Server(int port) throws IOException {
        DatagramSocket datagramSocket = new DatagramSocket(port);
        while (true) {
            byte[] receiveData = new byte[1024];
            DatagramPacket packet = new DatagramPacket(receiveData, receiveData.length);
            datagramSocket.receive(packet);
            System.out.println(new String(receiveData));
            new Thread(new HandleAClient(packet)).start();
        }
    }

    private static class HandleAClient implements Runnable {
        private DatagramPacket receivePacket;
        private DatagramSocket socket;

        public HandleAClient(DatagramPacket receivePacket) {
            this.receivePacket = receivePacket;
        }

        @Override
        public void run() {
            try {
                byte[] sendData = "服务端".getBytes();
                SocketAddress address = receivePacket.getSocketAddress();
                DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address);
                socket = new DatagramSocket();
                socket.send(sendPacket);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (socket != null) socket.close();
            }
        }
    }
}
public class Client {
    public Client(String host, int port) throws IOException {
        byte[] sendData = "客户端".getBytes();
        InetSocketAddress address = new InetSocketAddress(host, port);
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address);
        DatagramSocket socket = new DatagramSocket;
        socket.send(sendPacket);
        byte[] receiveData = new byte[1024];
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
        socket.receive(receivePacket);
        System.out.println(new String(receiveData));
        socket.close();
    }
}

4. DNS

        主机的一种标识方法是用它的主机名 ( $hostname$ ),如www.google.com。然而,主机名几乎没有提供关于主机在因特网中的位置的信息。因此,主机也可以使用IP进行标识。
        人们通常会记忆主机名,但路由器不能使用主机名,而是使用IP。为了能在主机名和IP之间转换,我们需要域名系统 ( $Domain\ \ Name\ \ System$ ,$DNS$ ) 的服务。DNS是一个由分层的DNS服务器实现的分布式数据库,使得主机能够查询分布式数据库的应用层协议。除了进行主机名到IP地址的转换外,DNS还提供了一些重要的服务:主机别名 ( $host\ \ aliasing$ )、邮件服务器别名 ( $mail\ \ server\ \ aliasing$ ) 和负载分配 ( $load\ \ distribution$ )。
        DNS采用了分布式的设计方案,全世界范围内存在着大量的DNS服务器。大致来说,有 $3$ 种类型的DNS服务器:DNS服务器 ( 提供TLD服务器的IP地址 )、顶级域服务器 ( 对于每个顶级域和所有的国家的顶级域都有的TLD服务器 ) 和权威DNS服务器 ( 在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的DNS记录 )。除此之外,还有一种本地DNS服务器 ( $local\ \ DNS\ \ server$ ),每个ISP都有一台本地DNS服务器。
        如果想要知道一个主机名对应的IP地址,主机会先向本地DNS服务器发送一个DNS查询报文,本地DNS服务器会将报文转发到根DNS服务器,根DNS服务器返回负责域名后缀的TLDIP地址列表。本地DNS服务器会再次向这些TLD服务器之一发送查询报文,TLD会返回权威DNS服务器的IP地址,最后本地DNS服务器直接向权威DNS服务器发送查询报文。权威DNS服务器会返回主机名对应的IP地址。一般情况下,TLD服务器并不知道权威DNS服务器的IP地址,而是中间某个DNS服务器,这时就需要本地DNS服务器再向中间DNS服务器发送查询报文。
        上述查询过程中使用了递归查询 ( $recursive\ \ query$ ) 和迭代查询 ( $iterative\ \ query$ )。在实践中,通常从请求主机到本地DNS服务器的查询是递归的,而其余的查询是迭代的。
        为了改善时延性能并减少在因特网上到处传输的DNS报文数量,DNS广泛使用了缓存技术,缓存包括浏览器缓存、本地 $hosts$ 文件和本地DNS解析器缓存等。本地DNS服务器也能够缓存TLD服务器的IP地址,从而绕过根DNS服务器。事实上,因为缓存,除了少数DNS查询以外,根服务器都会被绕过。

TCP、UDP和DNS简介