SIGPIPE 信号
- 如果向一个已经关掉的管道写数据,write系统调用会返回一个 SIGPIPE 信号
例1
- 于读写IO,当我们使用管道连接一行命令时,如果管道末端的命令执行失败,那么整个管道的程序将会依次收到SIGPIPE。避免了无效的计算。
gunzip -c message.log.gz | grep -a Succeeded. | head -10 # gunzip -c或--stdout或--to-stdout 把解压后的文件输出到标准输出设备 # grep -a grep如果碰到\000 NUL字符,就会认为文件是二进制文件,而 grep 匹配 默认忽略二进制数据。-a 将 binary 文件以 text 文件的方式搜寻数据
- 此命令的只取压缩文件中含有 “Succeeded.” 的前10函数数据,当成功匹配到10行数据后,gunzip也会停止执行,这样就可以避免将整个大文件都解压缩
例2
- 对于网络IO,当我们关闭了一个连接时,如果尝试向他写入数据,也会收到一个 SIGPIPE 信号
- 对于这种情况,如果在服务端没有做特殊处理,有一个客户端意外退出,造成连接关闭,而服务端如果此时发送消息的话,就会收到 SIGPIPE 信号,导致服务端被迫退出
- 因此,网络程序一般在启动时,都会忽略掉 SIGPIPE 信号
signal(SIGPIPE, SIG_IGN);
- 另外,需要注意的是,忽略SIGPIPE后,如果对方关闭了连接,我们的程序可能不会退出,因此,我们需要额外关注一些函数的返回值,例如我们向一个连接请求数据后将它输出到标准输出时,我们需要额外关注printf 函数的返回值,如果它的返回值是负值,则表示对端连接已关闭。那么我们的程序应该退出
Nagle 算法
- Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组
- 目的:避免发送大量的小包,网络上每次只能一个小包存在,在小包被确认之前,只能积累发送大包,如果包长度达到MSS,则允许发送;如果该包含有FIN,则允许发送;但发生了超时(一般为200ms),则立即发送, 启动TCP_NODELAY,就意味着禁用了Nagle算法
Nagle 算法缺点
- 如果我们的程序设计的不够合理,Nagle算法可能会增加程序的延迟。
- 如果你的程序是 write-write-read 模式,在使用了Nagle算法后,第二个 write 就会被推后一个RRT发送而造成一个很长的ack等待,从而产生一个延迟。为了避免这种情况,一般建议在应用层做缓冲,将两个write合在一起,成为 write-read。
- 但是还有一种情况是,在一个连接上并发的有多个请求时,我们很难将数据整合在一起,它们来自程序中不同的位置。而这种情况Nagle算法会大大增加程序的延迟。
- 因此,如果你没有十足的把握驾驭Nagle算法的话,我们建议使用 TCP_NODELY 关闭Nagle算法。
Nagle 算法延迟示例
服务器端代码:recipes/tpc/nodelay_server.cc
客户端代码:recipes/tpc/nodelay.cc
- 客户端是一个 write-write-read 程序,通过制定参数 对比 采取不同TCP选项下程序的延迟情况
- 客户端
double start = now(); for (int n = 0; n < num; ++n) { printf("Request no. %d, sending %d bytes\n", n, len); if (buffering) // 如果设置缓冲,我们将header与数据合在一起发送 { std::vector<char> message(len + sizeof len, 'S'); memcpy(message.data(), &len, sizeof len); int nw = stream->sendAll(message.data(), message.size()); printf("%.6f sent %d bytes\n", now(), nw); } else // 如果没有设置缓冲,分两次发送header和数据 { stream->sendAll(&len, sizeof len); printf("%.6f sent header\n", now()); usleep(1000); // prevent kernel merging TCP segments std::string payload(len, 'S'); int nw = stream->sendAll(payload.data(), payload.size()); printf("%.6f sent %d bytes\n", now(), nw); } }
- 服务端
// nodelay_server.cc 服务端代码部分 // 处理逻辑:收header ——》收数据 ——》回响应 //... int main(int argc, char* argv[]) { //... bool nodelay = argc > 1 && strcmp(argv[1], "-D") == 0; while (true) { TcpStreamPtr tcpStream = acceptor.accept(); printf("accepted no. %d client\n", ++count); if (nodelay) tcpStream->setTcpNoDelay(true); while (true) { int len = 0; // 收header int nr = tcpStream->receiveAll(&len, sizeof len); if (nr <= 0) break; printf("%f received header %d bytes, len = %d\n", now(), nr, len); assert(nr == sizeof len); std::vector<char> payload(len); // 收数据 nr = tcpStream->receiveAll(payload.data(), len); printf("%f received payload %d bytes\n", now(), nr); assert(nr == len); // 回响应 int nw = tcpStream->sendAll(&len, sizeof len); assert(nw == sizeof len); } printf("no. %d client ended.\n", count); } }
- 编译
编译服务端代码:
g++ -o nodelay_server nodelay_server.cc Acceptor.cc InetAddress.cc TcpStream.cc Socket.cc -lpthread -std=c++11
编译客户端代码:
g++ -o nodelay nodelay.cc Acceptor.cc InetAddress.cc TcpStream.cc Socket.cc -lpthread -std=c++11
- 测试
使用ping命令,可以看到正常的RTT约31ms延迟。然后执行程序,大概有61ms的延迟,是RTT的两倍
使用 buffering 选项,可以看到延迟减少到35ms左右
通过tcpdump我们可以看到,客户端一次发送了 1005 个字节的数据。客户端 write-read
使用SO_REUSEADDR选项
-
SO_REUSEADDR ——复用地址 。
-
一般服务器的监听socket都应该打开它。它允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接。
-
对于以下两种情况,SO_REUSEADDR会很有作用:
-
服务器启动后,与客户端建立连接,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此将无法启动服务器进程。
-
服务器父进程监听客户端,建立连接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以此时重启父进程会失败。
对于以上两张情况,重启服务器都会出现bind: : Address already in use错误。而当我们使用了 SO_REUSEADDR 选项后,服务器退出后,仍然允许我们马上重启进程。