UDP 的 connect

Unix 与 Linux 中,UDP 也可以使用 connect(),但是与 TCP 不同。
TCP 编程时,connect() 创建一个传输层的连接,然后就可以通过 write,read 函数只使用 socket descriptor 发送接收包,因为 connect 之后,一个 socket 实际上维护了一对地址,包括四条记录————双方的 IP 地址,及两个应用程序的 TCP 端口。
而 UDP 编程时,只能使用 sendto/recvfrom,每次都要指定地址和长度参数,很麻烦。而且,更重要的是,UDP 不能判断来源地址,所以可能接收到其他地址发过来的包,这是不好的,如果要处理,需要大量代码。

所以,POSIX 使 connect() 也可以用于 UDP,连接之后,一个 UDP 套接字就只能与一个对端通信。虽然 sendto() 实际上依然是可用的,但是我们应该使其最后两个参数为 NULL 和 0,或者使用 write/read 更好。然后,目的地为这个 IP 和端口号,源地址却不是 connect 到的地址的数据包,不会被内核投递到该套接字,这样就限制了只能与一个对端通信。(UNP 指出“确切的说,一个已连接的 UDP 套接字只能与一个 IP 地址交换数据报,因为 connect 到多播或广播地址是可能的。)

对一个已连接的 UDP 套接字再次 connect,可以改变它的 IP 地址和端口号。

malloc() 的 errno

我在参看 man 3 malloc 手册页时,发现 RETURN VALUE 一节并没有指出会这个函数设置 errno,但是下面却紧跟 ERRORS 一节并指出了 ENOMEM 错误。

我感到很奇怪,于是在手册页中搜索 errno,发现它仅出现在最后的 NOTES 里:

SUSv2 requires malloc(), calloc(), and realloc() to set errno to ENOMEM upon failure. Glibc assumes that this is done (and the glibc versions of these routines do this); if you use a private malloc implementation that does not set errno, then certain library routines may fail without having a reason in errno.

于是我打算自己验证一下,首先查看 data segment 的大小限制:

$ ulimit -a

结果是:

data seg size           (kbytes, -d) unlimited

这就意味着我的系统对 malloc 可申请的空间不做限制,一个程序可以不断申请,直到系统物理内存耗尽。但是,实际上,这个限制用一个 rlim_t 类型的变量约束,根据文档,可以知道,其实 rlim_t 就是 unsigned long int 类型,于是,我使用 getrlimit() 函数查看具体大小,调用如下:

getrlimit(RLIMIT_AS, &rlim);

RLIMIT_AS 表示:

The maximum size of the process’s virtual memory (address space) in bytes. This limit affects calls to brk(2), mmap(2), and mremap(2), which fail with the error ENOMEM upon exceeding this limit. Also automatic stack expansion will fail (and generate a SIGSEGV that kills the process if no alternate stack has been made available via sigaltstack(2)).

结果是:

18446744073709551615 bytes

相当大的一个数,粗略算一下,差不多 16EB,也就是 16777216TB,可以认为是无限大。

然后我再用

rlim.rlim_cur = 10;
setrlimit(RLIMIT_AS, &rlim);
if (malloc(100) == NULL)
	perror("malloc error");

果然,出错了:

malloc error: Cannot allocate memory

TCP recv()

TCP 经常会在一个无限循环里使用 recv/read 来等待对方的消息,但是如果在等待过程中,对端关闭 socket,就会立即发生灾难,因为:

When a stream socket peer has performed an orderly shutdown, the return value will be 0 (the traditional “end-of-file” return).

所以,如果对端关闭,recv 和 read 都会立即返回 0,而没有任何等待,所以在无限循环中的后果可想而知。

很显然,可以通过判定返回值,在对端关闭时马上退出循环,但是,为什么返回 0 就一定表示对端关闭呢?

因为,recv 和 read 在读 TCP 套接字时,不出错时,都会返回读到的数据的字节数。而 TCP 提供的是字节流服务,所以,write(sockfd, NULL, 0),根本不会将任何数据放进字节流里,也自然就无法读到 0 字节的数据了。而 TCP 又正好需要一个用于表征对端关闭的信息,所以就使用了 recv 的返回值 0。虽然 read 的手册页并没有明确指出这种行为,但实际上:

read(sockfd, buf, MAX);
recv(sockfd, buf, MAX, 0);

这两行是完全等同的。如果要知道为什么,可以看内核代码。

那么,UDP 又如何呢?
UDP 是数据报服务,允许发送 0 字节数据报,但是它是无连接的,没有”了解对端是否关闭连接“这样的需求。