文章目录
- [1. 传输层](#1. 传输层)
- [2. 端口](#2. 端口)
- [3. UDP](#3. UDP)
-
- [3.1 UDP报文](#3.1 UDP报文)
- [3.2 UDP特点](#3.2 UDP特点)
- [3.3 UDP应用场景](#3.3 UDP应用场景)
- [3.4 Linux UDP接口](#3.4 Linux UDP接口)
1. 传输层
在tcp/ip
协议栈中,应用层之下是传输层。
传输层中的很多行为都是内核帮我们完成,但是用户程序中也要调用一些接口进行设置。
传输层的协议有两个:UDP和TCP。
本篇博客中介绍UDP
.
2. 端口
端口本质上是一个unsigned short
类型的无符号短整数,范围是0~65535
。
在tcp/ip协议中,端口号作为源IP,源端口,目的IP,目的端口,协议号中的重要组成部分,共同标识一个通信。
IP用于定位具体的机器,而端口用于定位到这台机器上的具体进程。
关于进程和端口的关系,一般来说,一个进程可以绑定多个端口,但是一个端口只能被一个进程绑定,对于后者,在没有对socket进行额外设置时,是正确的,但如果设置了某些属性,情况会有所不同。
自己的程序有些端口号是不能绑定的,通常内核也不会允许我们绑定,这些端口号是知名端口号,对应了一些知名应用层协议------即使用这些协议访问服务器时,访问的服务器端口号已经确定了。
3. UDP
3.1 UDP报文

上图是UDP的报文结构。
其中,源端口号和目的端口号,这些不必多说。
16位UDP长度表示整个UDP报文的长度,可见,UDP报文的最大长度是65535字节,这个长度包括报头(报头总共8字节)和有效载荷,如果超过这个长度,那么对于有效载荷,就需要在应用层手动分包。
16位UDP检验和,这个数据可用于验证该UDP报文在网络传输的过程中,内容是否发生更改。
检验和有其自身计算逻辑,发送端在发送时计算一次,接收端在接收时计算一次,二者进行比对,即可确认内容是否改变。
3.2 UDP特点
UDP网络通信有三个最大特点:无连接,不可靠,面向数据报。
什么是无连接呢?
相对于TCP,UDP网络通信,通信双方完全没有建立连接的机制,即便UDP使用connect,本质上无连接,只不过将对端IP和端口记录到内核中,后续通信接口由使用sendto/recvfrom,改为使用send/recv罢了。
什么是不可靠呢?
UDP网络通信没有确认应答机制,即接收端是否正常受到UDP报文,发送端是无法得知的。
UDP网络通信没有超时重传机制,用户层写入的发送数据,封装为UDP报文后,永远只会发送一次。
同时,UDP通信中,由于实际网络问题,先发送的UDP报文,可能后到达,而UDP内核中并不会维护这个顺序,谁先到就都谁,而实际上这个信息可能是后发的,并不是最先应该读取的信息。
而由于UDP是无连接,不维护连接状态,所以即便通信的一方彻底关闭,另一方也不会有任何感知,读写行为仍照旧,只不过此时通信已没有任何意义。
什么是面向数据报呢?
面向数据报是指UDP报文有明确的数据边界。TCP报文是字节流传输的,一次写入的数据可能被分批发送,也可能和一些其它数据一起发送,读取时,可能一次性无法读到完整数据,需要多次读取。 但UDP不一样,UDP报文发送,要么完整,要么不发送 ;UDP报文读取,要么不读,读就读到完整的UDP报文。
因此,UDP报文明确的数据边界,使得UDP网络通信时,不需要在用户层处理粘包问题。
3.3 UDP应用场景
性质决定用途,UDP无连接,不可靠,面向数据报的性质,使得UDP的应用场景如何呢?
UDP不用维护连接,网络通信不可靠,即不需要维护超时重传,确认应答等各种复杂机制,这就使得UDP在不考虑通信质量的情况下,通信效率是非常高的。
因此,对于那些对通信效率要求高,即实时性要求高,而允许一定程度的通信数据丢失场景下,UDP应用非常多,比如实时视频通话,或者网络直播等等。
除了上述场景外,在简单的网络通信场景中,比如DNS域名解析场景,总共就一问一答,非常简单,使用UDP即可,不必使用TCP,否则反倒因为TCP复杂机制,带来不必要的额外消耗。
3.4 Linux UDP接口
网络通信接口使用分为客户端和服务端。
客户端:
-
使用
socket
创建用于通信的网络套接字,返回相应文件描述符。int fd = socket(AF_INET,SOCK_DGRAM,0);//指定协议号为0,让内核自动判别即可
-
客户端一般不显式绑定,而是在
sendto
发送数据时,由内核完成绑定。 -
使用
sendto
进行数据发送。size_t n = sendto(fd,buffer,len,0,(struct sockaddr*)dest_addr,addrlen);//一般flag标志位给0即可,取默认情况
-
使用
recvfrom
进行数据接收。size_t n = recvfrom(fd,buffer,len,0,(struct sockaddr*)src_addr,&addrlen);//注意这里的addrlen是取地址,因为要实际拿到写入src_addr的字节数
服务端:
-
使用
socket
创建网络套接字进行通信,与客户端相同。 -
服务端必须使用
bind
显式绑定。int ret = bind(sockfd,addr,addrlen)
那么在第二个参数,即类型为struct sockaddr这个结构体中,我们需要手动填入什么呢?
在网络通信中,实际使用的是struct sockaddr_in这个类型,传参时需要做类型转化。
在这个结构体中,我们需要初始化三个变量:sin_family
初始化为AF_INET
,表示是网络通信,使用IpV4
;sin_addr
对应的是ip
地址,需要使用相关接口进行主机字节序到网络大端字节序的转换 ;sin_port
对应的是具体端口,与ip
地址相同,需用相关接口完成字节序的转换。
- 服务端使用
recvfrom
接收数据,接口和参数均同上。 - 服务端使用
sendto
发送数据,接口和参数也同上。
服务端使用sendto发送数据时,可能会有疑惑:服务端如何知道客户端的IP+PORT呢?这本质上是逻辑顺序的问题。因为,客户端和服务端之间的通信,通常都是客户端已知服务端IP+PORT,然后客户端先给服务端发信息,此时服务端通过recvfrom的相关参数,即可得到客户端地址,然后再向此地址客户端发送消息,而客户端虽然未主动bind,但在sendto中,已经由内核自动bind,因此也能正常接收服务端消息。自此,双端通信的逻辑就是完善的。