东阳的学习笔记
前面几篇所介绍的 TcpConnection 的主体功能接近完备,可以应付大部分 muduo 示例的需求了。这里再补充几个小功能.
一、SIGPIPE
SIGPIPE
的默认行为是结束进程,在命令行程序这是合理的,但是在网络编程中, 这意味着如果对方断开连接而本地继续写入的话,这会造成服务进程意外退出。
假如服务进程繁忙,没有及时处理对方断开连接的事件,就有可能出现在连接断开之后继续发送数据的情况。下面这个例子模拟了这种情况:
void onConnection(const muduo::TcpConnectionPtr& conn)
{
if (conn->connected())
{
printf("onConnection(): new connection [%s] from %s\n",
conn->name().c_str(),
conn->peerAddress().toHostPort().c_str());
+ if (sleepSeconds > 0)
+ {
+ ::sleep(sleepSeconds);
+ }
conn->send(message1);
conn->send(message2);
conn->shutdown();
}
else
{
printf("onConnection(): connection [%s] is down\n",
conn->name().c_str());
}
}
假设 sleepSecond 是5s,用 nc localhost 9981 创建连接之后立即CRTL-C 断开客户端,服务器进程过几秒就会退出。解决方法很简单,在程序刚开始的时候就忽略 SIGPIPE,可以用 C++ 全局对象做到这一点。
- (禁用CTRL-C)
class IgnoreSigPipe
{
public:
IgnoreSigPipe()
{
::signal(SIGPIPE, SIG_IGN);
}
};
IgnoreSigPipe initObj;
二、TCP No Delay 和 TCP keepalive
TCP No Delay 和 TCP keepalive 都是常用的 TCP 选项:
- 前者的作用是禁用 Nagle 算法,避免连续发包出现延迟,这对编写低延迟网络服务很重要
- 后者的作用是定期探测 TCP 连接是否存在。一般来说如果有应用层心跳的话,不是必须的,但是作为一个通用的网络库应该暴露其接口。
// TcpConnection.h
void shutdown();
void setTcpNoDelay(bool on);
// TcpConnection.cc
void TcpConnection::setTcpNoDelay(bool on)
{
socket_->setTcpNoDelay(on);
}
// Socket.cc
void Socket::setTcpNoDelay(bool on)
{
int optval = on ? 1 : 0;
::setsockopt(sockfd_, IPPROTO_TCP, TCP_NODELAY,
&optval, sizeof optval);
// FIXME CHECK
}
TcpConnection::setKeepAlive() 的实现与之类似,此处从略,可参考 muduo 源码。
三、 WriteCompleteCallback 和 HighWaterMarkCallback
前面提到了非阻塞网络编程的发送数据比读取数据要困难的多:
- 一方面,什么时候关注 writable 事件是一个问题,这会带来编码方面的难度;
- 另一方面,如果发送数据的速度高与接收数据的速度,会造成数据在内存中堆集,这又带来设计及安全性方面的难度。
muduo对此的解决方法是提供两个回调,有的网络库把他们称为高水位回调
和低水位回调
3.1 WriteCompleteCallback(如果发送缓冲区为空
,就调用它)
WriteCompleteCallback 比较容易理解,如果发送缓冲区被清空
,就调用它。
TcpConnection 有两处可能触发此回调,入下:
第一处: TcpConnection::sendInLoop()
void TcpConnection::sendInLoop(const std::string& message)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
// if no thing in output queue, try writing directly
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
nwrote = ::write(channel_->fd(), message.data(), message.size());
if (nwrote >= 0) {
if (implicit_cast<size_t>(nwrote) < message.size()) {
LOG_TRACE << "I am going to write more data";
+ } else if (writeCompleteCallback_) {
+ loop_->queueInLoop(
+ boost::bind(writeCompleteCallback_, shared_from_this()));
}
} else {
第二处:TcpConnection::handleWrite()
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting()) {
ssize_t n = ::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0) {
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) {
channel_->disableWriting();
+ if (writeCompleteCallback_) {
+ loop_->queueInLoop(
+ boost::bind(writeCompleteCallback_, shared_from_this()));
+ }
if (state_ == kDisconnecting) {
shutdownInLoop();
}
// ...
TcpConnection 和 TcpServer 也需要相应地暴露 WriteCompleteCallback 的接口。
3.2 HighWaterMarkCallback
如果输出缓冲的长度超过用户指定的大小,就会触发回调(只在上升沿触发一次
)。
如果用非阻塞的方式写一个 proxy,proxy 有 C 和 S 两个连接。只考虑 server 发给 client 的数据流(反过来也是一样),为了防止 server 发过来的数据撑爆 C 的输出缓冲区。
- 一种方法是在 C 的 HighWaterMarkCallback 中停止读取 S 的数据,而在 C 的 WriteCompleteCallback 中恢复读取 S 的数据。这就跟用粗水管往水桶里灌水,用细水管放水一样,上下两个水龙头要轮流开合,类似 PWM。