在计算机网络中,TCP(传输控制协议)和UDP(用户数据报协议)是两个常用的传输层协议。它们分别提供了可靠的数据传输和快速的数据传送,成为互联网世界中的双子星。本文将探讨TCP和UDP的特点、优势和应用场景,以及如何选择合适的协议来满足不同的需求。
英文名:Transmission Control Protocol
中文名:传输控制协议
协议说明:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。
举例:打电话,需要双方都接通,才能进行对话
特点:效率低,数据传输比较安全
英文名:User Datagram Protocol
中文名:数据报协议
协议说明:UDP是一种面向无连接的传输层通信协议。
举例:发短信,不需要双方建立连接,But,数据报的大小应限制在64k以内
特点:效率高,数据传输不安全,容易丢包
TCP是一种面向连接的、可靠的传输控制协议,是互联网通信中最常用的协议之一。TCP的设计目标是提供可靠的数据传输、流量控制和拥塞控制机制,以确保数据在网络中准确无误地传输。
TCP协议总结起来主要有以下几个特点
面向连接:在TCP通信中,发送端和接收端之间需要建立一个可靠的连接,这个连接在数据传输结束后再关闭。连接的建立和断开是通过三次握手和四次挥手来完成的。
可靠性:TCP提供可靠的数据传输机制。它通过序列号、确认应答和重传机制来确保数据的准确传输。发送端将数据划分为称为TCP段的小块,并在每个段上添加一个序列号。接收端通过确认应答来确认已收到的数据,并可以要求发送端重新传输丢失的数据。
流量控制:TCP具有流量控制机制,用于控制数据发送的速率,以避免接收端无法处理过多的数据而导致的数据丢失。接收端可以通过窗口大小来通知发送端自己的接收能力,从而实现动态调整数据传输的速率。
拥塞控制:TCP通过拥塞控制机制来避免网络拥塞和数据丢失。它使用一种称为拥塞窗口的机制来控制数据发送的速率。当网络拥塞时,TCP会降低发送速率,从而减轻网络负载,保证网络的稳定性。
面向字节流:TCP是一种面向字节流的协议,将应用层传输的数据视为一连串的字节流。TCP会将数据划分为适当大小的数据段,并通过TCP头部中的序列号来对每个数据段进行标记,以便接收端正确地重组数据。
可靠性校验:TCP使用校验和机制来检测数据在传输过程中是否发生了错误。接收端会根据校验和的值判断数据是否被篡改,如果检测到错误,接收端会要求发送端重新传输数据。
有序性:TCP保证数据的有序传输。每个TCP段都有一个序列号,接收端根据序列号对接收到的数据进行排序,以确保数据按正确的顺序重新组装。
全双工通信:TCP支持全双工通信,即发送端和接收端可以同时进行发送和接收操作,实现双向的数据传输。
UDP是一种无连接的传输协议,相对于TCP而言,它具有以下特点:
TCP协议是作用是用来进行端对端数据传送的,那么就会有发送端和接收端,在操作系统有两个空间即user space和kernal space。
每个Tcp socket连接在内核中都有一个发送缓冲区和接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。
单工:只允许甲方向乙方传送信息,而乙方不能向甲方传送 ,如汽车单行道。
半双工:半双工就是指一个时间段内只有一个动作发生,甲方可以向乙方传送数据,乙方也可以向甲方传送数据,但不能同时进行,如一条窄马路同一时间只能允许一个车通行。
全双工:同时允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。
一个socket的两端,都会有send和recv两个方法,如client发送数据到server,那么就是客户端进程调用send发送数据,而send的作用是将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。也就是说send()方法返回之时,数据不一定会发送到对端即服务器上去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。
接收缓冲区把数据缓存入内核,等待recv()读取,recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回。若应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
查看socket发送缓冲区大小,cat /proc/sys/net/ipv4/tcp_wmem
TCP粘包问题是在TCP协议中常见的一个数据传输问题,指的是发送方连续发送的多个数据包在接收方接收时被粘在一起,导致接收方无法正确解析和处理数据包的边界。这种情况可能会对应用层造成困扰,因为接收方无法准确判断每个数据包的起始和结束位置,从而导致数据解析错误或数据丢失。
因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。
对于粘包和拆包问题,常见的解决方案有四种:
Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:
LineBasedFrameDecoder:以行为单位进行数据包的解码;
DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码;
FixedLengthFrameDecoder:以固定长度进行数据包的解码;
LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用);
基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode更适合这样的场景。
UDP协议不会发生粘包问题的主要原因是UDP是面向数据报的协议,每个UDP数据包都是独立的、自包含的。在UDP中,每个数据包被视为一个独立的实体,没有像TCP那样的数据流概念,因此不会发生粘包现象。
UDP协议在传输数据时,每个数据包都有自己的报文头,其中包含了目标端口、源端口、数据长度等信息。在发送端,每个UDP数据包独立发送,并且每个数据包都有固定的大小。在接收端,每次接收到一个UDP数据包时,就会被视为一个完整的数据报,而不会与其他数据包发生合并或拆分的情况。
因此,UDP协议的特性决定了它不会发生粘包问题。每个UDP数据包都具有独立性和完整性,接收方可以准确地处理每个数据包,而不需要担心数据包的边界和顺序。
然而,尽管UDP不会发生粘包问题,但它也存在其他的问题,例如数据丢失、数据无序等。由于UDP是无连接、不可靠的协议,它不提供重传机制和确认应答机制,因此在网络不稳定或拥塞的情况下,UDP数据包可能会丢失或无序到达。因此,在使用UDP进行数据传输时,应用层需要自行处理这些问题,如使用序列号、确认应答等手段来确保数据的可靠性和顺序性。
即所谓的 Zero-copy, 就是在操作数据时, 不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域. 因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升.
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据. 例如 Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间. 正因为有这样的映射关系, 我们就不需要在 用户态(User-space) 与 内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率.
而需要注意的是, Netty 中的 Zero-copy 与上面我们所提到到 OS 层面上的 Zero-copy 不太一样, Netty的 Zero-coyp完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念.
Netty 的 Zero-copy 体现在如下几个个方面:
Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.
通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.
ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.
通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.
Netty的接收和发送ByteBuffer使用直接DirectBuffer内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用JVM的堆内存进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于使用直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。