Endian
Endian.h
是一个公共头文件,里面包含了一些网络字节序和主机字节序相互转换的问题。
其中所使用的方法如下:
// XX 位主机转网络
uint64_t htobe64(uint64_t data);
uint32_t htobe32(uint32_t data);
uint16_t htobe16(uint16_t data);
// XX 位网络转主机
uint64_t be64toh(uint64_t data);
uint32_t be32toh(uint32_t data);
uint16_t be16toh(uint16_t data);
SocketsOps
SocketOps.h
中封装了许多套接字相关的函数,仅为 Muduo 库内部使用。
如下方法用来创建一个非阻塞的套接字:
int sockets::createNonblockingOrDie(sa_family_t family) {
int sockfd = ::socket(family, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);
if (sockfd < 0) {
LOG_SYSFATAL << "sockets::createNonblockingOrDie";
}
return sockfd;
}
其中的 SOCK_NONBLOCK
标志即表示该套接字为非阻塞套接字。对于套接字而言,如果进程将套接字设置为非阻塞,就是在通知内核:当所请求的 I/O 操作必须要将本进程投入睡眠时才可以完成时,不要把本进程投入睡眠,而是返回一个错误。
而 SOCK_CLOEXEC
标志即 close on exec。如果进程监听端口后,fork 出一个子进程,这时我们 kill 掉父进程,然后再重启父进程,此时系统提示 “端口占用”。通过 netstat 查看发现,此时子进程占用了父进程监听的端口。
这是因为子进程在 fork 出来后,由于 “写时复制” 机制,子进程获得了父进程的数据空间、堆、栈,其中就包含父进程所创建的套接字文件描述符。之后,一般会在子进程中调用 exec 执行另一程序,此时会用全新的程序替换掉子进程的正文、数据、堆、栈等。此时,我们就无法关闭无用的文件描述符,因此在 exec 之前,需要关闭无用的文件描述符。
但是如果打开的文件描述符过多,逐一清理会很繁琐,因此我们可以设置 SOCK_CLOEXEC
标志以在执行 exec 之后就关闭该文件描述符。
同样的方式也出现在如下方法中:
int sockets::accept(int sockfd, struct sockaddr_in6* addr) {
socklen_t addrlen = static_cast<socklen_t>(sizeof *addr);
int connfd = ::accept4(sockfd, sockaddr_cast(addr),
&addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
// ...
}
对于所接受的套接字描述符也同样设置 SOCK_NONBLOCK
和 SOCL_CLOEXEC
标志。
通常关闭套接字时,有如下两种方法:
- close
- shutdown
其中,close
一个 TCP 套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为 read
或 write
的第一个参数。然后 TCP 将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的 TCP 连接终止序列。同时,在并发服务器中,父进程关闭已连接套接字只是导致相应描述符的引用计数减 1。直到其值为 0,才真正关闭。
而如果我们想在某个 TCP 连接上放松一个 FIN,可以改用 shutdown
以代替 close。与 close 不同的是,shutdown 可以不管引用计数就激发 TCP 的正常连接终止序列。其次,close 终止读和写两个方向的数据传输,而 shutdown 可以之关闭连接的一端。它可以使用的关闭标志如下:
SHUT_RD
: 关闭连接的读一端,即套接字不再有数据可接受,而且套接字接收缓冲区中的现有数据都被丢弃。若设置该标志,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。SHUT_WR
: 关闭连接的写一端,对于 TCP 套接字而言,称为半关闭。当前留在套接字发送缓冲区中的数据被发送掉,然后 TCP 进行正常的连接终止序列。SHUT_RDWR
: 连接的读端和写端都关闭。
Muduo 中的实现如下:
void sockets::close(int sockfd) {
if (::close(sockfd) < 0) {
LOG_SYSERR << "sockets::close";
}
}
void sockets::shutdownWrite(int sockfd) {
if (::shutdown(sockfd, SHUT_WR) < 0) {
LOG_SYSERR << "sockets::shutdownWrite";
}
}
InetAddress
InetAddress
类封装了 IP 地址和端口号。该类只有一个成员变量,使用 union
来共享同一内存区域,既可以用来表示 IPv4,也可以用来表示 IPv6。
union {
struct sockaddr_in addr_;
struct sockaddr_in6 addr6_;
};
在我们绑定 IP 地址时,有如下两个宏可供使用:
INADDR_ANY
: 绑定地址 0.0.0.0 上的监听,能收到任意网卡的连接;INADDR_LOOPBACK
: 绑定环回接口,即 127.0.0.1 上的监听;
在 Muduo 库中封装如下:
static const in_addr_t kInaddrAny = INADDR_ANY;
static const in_addr_t kInaddrLoopback = INADDR_LOOPBACK;
在该类中,有一个用于解析域名的方法:
bool InetAddress::resolve(StringArg hostname, InetAddress* out) {
assert(out != NULL);
struct hostent hent;
struct hostent* he = NULL;
int herrno = 0;
memZero(&hent, sizeof(hent));
int ret = gethostbyname_r(hostname.c_str(), &hent, t_resolveBuffer, sizeof t_resolveBuffer, &he, &herrno);
if (ret == 0 && he != NULL) {
assert(he->h_addrtype == AF_INET && he->h_length == sizeof(uint32_t));
out->addr_.sin_addr = *reinterpret_cast<struct in_addr*>(he->h_addr);
return true;
} else {
if (ret) {
LOG_SYSERR << "InetAddress::resolve";
}
return false;
}
}
其中的核心是 gethostbyname()
,该函数通过域名或主机名来获取 IP 地址,即通过 DNS
获取主机名与 IP 地址之间的映射关系。而如上代码中所使用的 gethostbyname_r()
为 gethostbyname()
的可重入版本,因此该函数是线程安全的。
可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。而不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。
Socket
Socket
类封装了套接字文件描述符,其使用常成员变量 sockfd_
来存储套接字描述符。
在该类中,封装了大部分网络编程函数,例如 bind、listen、accept、shutdown,其内部的核心函数全部来自于 SocketOps.h
文件中的 sockets
命名空间。
除此之外,还包括部分可以用来设置 TCP 套接字选项的函数:
void setTcpNoDelay(bool on);
void setReuseAddr(bool on);
void setReusePort(bool on);
void setKeepAlive(bool on);
TCP_NODELAY
对于 TCP_NODELAY
而言,开启该套接字选项将禁止 TCP 的 Nagle
算法,默认情况下,该算法是启动的。该算法的目的在于减少广域网上小分组的数目。也就是说,发送端为了将发往接收端的包更有效的发送给对方,将多次间隔小、数据量小的数据,合并成一个大的数据块,然后进行封包。
该算法的具体做法是:如果发送端欲多次发送包含少量字符的数据包,则发送端会先将第一个小包发送出去,而将后面到达的少量字符数据都缓存起来而不立即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了一定数量的数据(比如缓存的字符数据已经达到数据包报文段的最大长度)等多种情况才将其组成一个较大的数据包发送出去。
通常,Nagle 算法还与另外一个 TCP 算法联合使用:ACK 延滞算法
。该算法使得 TCP 在接收到数据后不立即发送 ACK,而是等待一小段时间(典型值为 50~200ms),然后才发送 ACK。TCP 期待在这一小段时间内自身有数据发回客户端,被延滞的 ACK 就可以由这些数据捎带,从而省掉一个 TCP 分节。
一般将长度小于 MSS 的数据包称为小包。
SO_KEEPALIVE
对于 SO_KEEPALIVE
而言,开启该选项后,如果 2 小时内在该套接字的任一方向上都没有数据交换,TCP 就自动给对端发送一个 保持存活探测分节(keep-alive probe
)。这是一个对端必须响应的 TCP 分节,它会导致以下三种情况之一:
- 对端以期望的 ACK 响应。表示应用进程一切正常。
- 对端以 RST 响应,它告知本端 TCP:对端已崩溃且已重新启动。该套接字的待处理错误被置为
ECONNRESET
,套接字本身则被关闭。 - 没有任何响应。源自 Berkeley 的 TCP 将另外发送 8 个探测分节,两两相隔 75 秒,试图得到一个响应。如果期间内仍无任何响应则放弃。
如果根本没有对 TCP 的探测分节响应,该套接字的待处理错误就被置为 ETIMEOUT
,套接字本身则被关闭。然而如果该套接字接收到一个 ICMP 错误作为某个探测分节的响应,那就返回相应的错误,套接字本身也被关闭。这种情况下的一个常见的 ICMP 错误是 "host unreachable",即主机不可达,说明对端主机可能并未崩溃,只是不可达,这种情况下待处理错误为 EHOSTUNREACH
。发生这种情况的原因或者是发生网络故障,或者是对端主机已经崩溃,而最后一跳的路由器也已经检测到它的崩溃。
SO_REUSEXXXX
对于 SO_REUSEADDR
而言,该选项允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将该端口用作它们的本地端口的连接仍存在。其发生的情况如下:
- 启动一个监听服务器;
- 连接请求到达,派生一个子进程来处理该客户;
- 监听服务器终止,但子进程继续为现有连接上的客户提供服务;
- 重启监听服务器;
而 SO_REUSEPORT
则允许将多个 AF_INET 或 AF_INET6 套接字绑定到相同的套接字地址。在对套接字调用 bind 之前,必须在每个套接字上设置该选项。对于 TCP 套接字而言,该选项允许通过为每个线程使用不同的监听套接字来改进多线程服务器中的 accept 负载均衡,提升了新连接的分配性能。