UDP协议是相对于TCP协议不是面向连接的,也是不可靠的,因此UDP套接字编程在思路上和TCP套接字编程很不一样。
普通的UDP套接字
sentto函数和recvfrom函数
sentto
函数和recvfrom
函数比面向连接(稍后并不仅指TCP)的send
和recv
函数多了flag
和表示送达和接收地址的SA
。
容易看到这两个函数是适合UDP这样的无连接协议的。对于客户端来说,相当于将connect
函数功能去掉,然后每次都显式传地址。对于服务端来说,它也不需要accept
函数,每次recvfrom
过来,它都可以取到这是从谁发过来的。甚至recvfrom
的SA
参数可以设为nullptr
,这样表示我接受所有信息,不管是谁发的。
注意recvfrom
传入的最后一个长度参数必须是已经初始化后的。否则UDP函数返回的地址和端口都会是0。
普通的UDP套接字存在的问题
异步错误
上面这样的设计看起来似乎很好,但考虑当服务端进程未开启,那从客户端过来的UDP包是送不到的,这时候recvfrom
阻塞了。UNP书中给出了echo服务的例子,需要注意的是在Ubuntu的终端中我们可以仍可以输入,但实际上线程是阻塞的。
但是对客户端来说并不是这样,因为sendto
函数是立即返回的(不返回也没有意义啊,毕竟无连接的,并不能指望对方一定会回复)。但对系统来说,这个包并不是就这么杳无音信的,因为服务端会发一个端口不可达的ICMP过来,可惜这个不会被客户端的进程接受(原因稍后论述)。并且这个ICMP是具有时延,所以它也不能被立即返回的sendto
接受。这样的错误称为异步错误。
BTW,注意ICMP本身也是不可靠的,可能会被丢掉。所以客户端存在收不到ICMP的情况,这可能是因为数据报根本没发过去,也可能是对方主机回复的ICMP丢了。TCP甚至有个专门的Duplicate ACK机制来解决“是发过去的包丢了还是返回的ACK丢了”的问题。
为什么对应进程收不到端口不可达的ICMP
考虑刚才的recvfrom
函数,假如说给recvfrom
函数设置一个超时(Ch14.2),那么它就不会在端口不可达时永远陷入阻塞,此时我们考虑它的行为。
假设一个多宿的UDP套接口(它可向多个IP发送数据包)向另外3个服务器发送了数据包,然后阻塞在reccfrom
上等待回应。这三台服务器中前两台开启了相应的端口并给出了回应,但是第三台服务器是端口不可达的,因而对端回复了ICMP报文。按照直截了当的思路,内核应当把源IP和端口等信息写到recvfrom
的SA * from
参数里面,然后设置errno = -1
,这样recvfrom
就可以超时返回了,客户端对应进程也就能知道刚才的的一个sendto
失败了。
作者指出从逻辑上就是不现实的,这是因为在recvfrom
函数的返回值里面不能知晓是自己发送的3个UDP包中发向哪个服务器的包出现了问题,导致自己收不到回应,因为返回的errno
无法承载IP地址信息。不过讲得并不清楚。
这是由于sendto
函数调用后,它是立即返回的,此时内核已经释放了和对端套接字相关的数据结构,当端口不可达的ICMP过来时,内核无法追踪出对应的套接字了,所以它无法通知上层的应用。
基于上面的两点原因,最好不要重复利用UDP套接口,也就是说将它连接到一个对端。因此对UDP也有了connect
函数。
重新考虑上面的问题,在connect的情况下,内核中记录了这个五元组(源IP, 源port, 宿IP, 宿port, 协议)
的状态。如果对端传来了ICMP消息,内核可以取出ICMP中的端口号,然后根据记录的五元组找到相应的进程。因此进程就能收到这个错误,虽然还是异步的。
“连接的”套接字
一个不面向连接的UDP套接字不能收到它引发的异步错误,除非它已经被connect
。但这里的connect
和TCP中的并不一样,它没有TCP中三次握手的过程,事实上它仅仅在内核中注册了一下,并未和对端服务器进行交互。此时应当使用write
、send
来代替sendto
(硬要用sendto
需要将第5个参数即宿地址设为nullptr
),用read
、recv
、recvmsg
来代替recvfrom
,这是因为connect
声明了我们的套接口只和某个ip:port进行交互了。已连接的套接字会忽略其他IP或端口传来的数据报。
同理,断开UDP的“连接”也不需要进行四次挥手,而是直接用AF_UNSPEC
参数再次调用connect
函数即可。对于不同的POSIX系统,还有其他各不相同的方法。
套接口参数
SO_REUSEPORT、SO_REUSEADDR
这个 Linux3.9 版本带来的新特性,类似于一个负载均衡。在使用 SO_REUSEPORT
后,多个进程可以同时 bind 和 listen 同一个 ip:port
,产生多个套接字,然后由内核决定将新连接发送给谁。当然,监听这些端口的套接口只能在同一个用户(effective user id)下面,这是为了保证安全性。
特别说明,因为套接口连接是五元组,所以对于同一个四元组,可以建立 tcp 连接和 udp 连接。但是如果不开启 SO_REUSEPORT
选项,是不能多个套接口绑定到一个 port 上的。可以简单想一想,此时一个 tcp 包来了,如何选择对应的套接字呢?没有 SO_REUSEPORT
的情况下,默认报错;有 SO_REUSEPORT
的情况下,我们随机选一个。
但是在文章中指出,并不一定能够实现负载均衡。
网络编程相关
epoll
QA
是不是所有 fd 都可以被 epoll
并不是的,需要实现 poll 函数的fd。1
2struct file_operations {
__poll_t (*poll) (struct file *, struct poll_table_struct *);为什么要有等待队列
所有等待这个 epoll 的进程都会在等待队列上面,这个有点类似于 Python 的 Condition 里面的 waiters 数组。水平和边沿触发
为什么 epoll 快
- 只有调用
epoll_ctl
的时候才拷贝 fd,而不是在epoll_wait
的时候拷贝。 epoll_wait
不会遍历所有的 fd,而是只判断就绪队列是否为空。epoll_ctl
的复杂度是O(log(n))
的,这是因为引入了红黑树。- 回调机制
- 只有调用
为什么 epoll 使用红黑树
socket 的句柄是 key,socket 对象是红黑树的节点。方便快速找到对应的 socket 对象,从而快速修改监听 socket 的读写事件。
一个 epoll 的demo
1 |
|