目录
传输层
传输层是负责将数据从发送端转发到接收端的。传输层及以下都是属于操作系统的。
再谈端口号

端口号标识了一个主机上进行通信的不同的应用程序。当协议栈接收到报文之后,一定要能够将报文发给上一层,从传输层交给应用层时,传输层报头中就会有对应服务的端口号,根据端口号即可将报文交给指定的服务。

上图理解为HTTP服务是多进程的。客户端发送请求后,服务器接收到请求后,是需要发送应答的,发送应答就一定需要知道客户端的IP地址和端口号,这是服务器通过recvfrom得到的。所以,通过服务器的IP地址和端口号、客户端的IP地址和端口号,即可标识一对通信。
实际上,在应用层获取到的IP地址是记录在IP报文的首部的,端口号是记录在TCP/UDP报文的首部的。并且在报文当中,是会含有传输层所采用的协议的字段的。所以现在可以使用一个五元组标识一对通信,源IP地址、目标IP地址、源端口号、目标端口号、传输层所采用的协议。
端口号范围划分
- 0 - 1023:知名端口号。HTTP、FTP、SSH等这些广为使用的应用层协议,他们的端口号都是固定的
- 1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的
Linux中,有一个配置文件记录了网络服务中常见的端口号:/etc/services
- 一个进程是否可以绑定多个端口号?
可以,甚至可以既绑定TCP的端口号,又绑定UDP的端口号。
- 一个端口号是否可以被多个进程绑定?
不能。因为需要根据端口号区分进程。
UDP协议
UDP是不考虑连接的,只要套接字创建好了,就可以直接发送消息。因为UDP面向数据报的,所以是不需要考虑粘包问题和字节流问题的,UDP的报文长度在内核中OS是知道的。
UDP协议端格式

cpp
struct udphdr {
__be16 source; // 源端口号(16位,大端字节序)
__be16 dest; // 目标端口号(16位,大端字节序)
__be16 len; // UDP数据报长度(头部+数据,单位:字节)
__be16 check; // 校验和(覆盖头部和数据,可选)
};
传输层及以下,都是属于OS的,所以在这里面的协议都是结构体。在OS中,与应用层是不同的。在OS中可以直接传输结构化数据,是二进制传输的,不需要进行序列与反序列化。因为对于OS而言,效率更重要。添加报头的本质就是定义一个结构体对象,然后将这个结构体对象与上层的报文的拷贝合并起来。
上面的图片就是一个UDP报文,前8个字节就是UDP报头,后面的数据就是有效载荷。
我们来看几个问题:
- UDP是如何解包的?
因为UDP的报头是固定长度的,8字节。直接读取UDP报文的前8个字节,将剩下的有效载荷交给上层即可。
- UDP是如何做到分用的?
UDP的报头中有一个目的端口号。而UDP服务器绑定过端口号,就会使用报头中的端口号进行匹配。就能够找到对应进程了,再将有效载荷交给这个进程即可。之前说端口号最大是65536,因为一个端口号只有16位。这是由协议决定的。
- UDP是面向数据报的,如果有多个UDP报文粘在在一起了,怎么确保准确读完有效载荷呢?
报头中还有16位的UDP长度。这表示的是整个UDP报文的长度,单位是字节,减去8即可得到有效载荷的长度。
虽然UDP不保证可靠性,即报文丢了就丢了。但是UDP会保证交给上层的数据是合法、可靠的。所以会有一个16位的UDP校验和。用来对整个UDP报文做校验,校验失败了直接丢弃,校验成功了才向上层交付。
报头中有有效载荷的长度,这在我们之前自己实现的网络版计算器和HTTP协议中都有使用。像这种在报头中会描述自己有效载荷的字段,称为协议中的自描述字段。
- UDP是如何进行封装的?
当我们调用sendto将应用层的报文发送到传输层时。UDP中为了能够对应用层发送过来的报文进行封装,就需要有一段缓冲区。当应用层发送报文时,实际上就是将报文拷贝到了缓冲区里面。而在OS中,发送报文的同时可能也在接收报文,所以oS就需要对这些报文进行管理。每一个报文就是一个sk_buff,sk_buf是一个报文的管理结构,里面会包含很多字段,其中就有指向应用层发送过来的报文的指针。当应用层刚将报文发送给传输层时,会将这个报文拷贝到传输层对应的缓冲区中台,一个指针end指向拷贝过来的报文的结束。

到达传输层的报文是需要添加UDP报头的,也就是进行封装,怎么添加UDP报头呢?
- head -= sizeof(struct udphdr)
- 填写UDP报头
(struct udphdr*)head -> source = 8080;
(struct udphdr*)head -> dest = 9090;
...

这就是UDP封装。要将添加了UDP报头的数据发送给下一层要怎么发送呢?
只需要将sk_buff链入到网络层的相应管理结构中,这样带着数据就下去了。并不会将传输层缓冲区内的报文拷贝到网络层缓冲区当中,因为只要通过指针即可访问。
UDP的特点
无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接;
不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
面向数据报:不能够灵活的控制读写数据的次数和数量。
这里的可靠与不可靠不是优缺点,而是特性。因为有的情况下对可靠性的要求并不那么高。TCP是可靠的,为了保证可靠性,它就要做更多的工作。不可靠说明这个协议本身比较简单一些。
面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。发送一次,接收方的应用层读取UDP报文时,一次就必须读完一整个UDP报文。TCP则是发一次,可能需要接收多次才能接收完。如果发送端调用一次 sendto,发送100 个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节;而不能循环调用10次recvfrom,每次接收10个字节。
UDP的缓冲区
我们之前说过,TCP的一个套接字就对应一个发送缓冲区和一个接收缓冲区。所以TCP是全双工的。TCP要保证接收到的报文与发送时的顺序一致,也属于可靠性的一种。
UDP是有接收缓冲区的,因为收到数据时,可能并不在recvfrom中。UDP并没有真正意义上的发送缓冲区,因为它不需要。因为UDP是不可靠的,应用层将报文交给UDP后,UDP直接添加报头然后发给下一层。但是,不存在发送缓冲区并不影响它的全双工。
UDP的注意事项
我们注意到,UDP协议首部中有一个16 位的最大长度。也就是说一个UDP能传输的数据最大长度是 64K(包含 UDP 首部)。如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。既然已经拆分了,不如使用TCP了。
基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议