Linux 多线程服务端编程读书笔记(七)
第七章 muduo编程示例
1、UNP 中五个简单的示例
-
discard:丢弃所有收到的数据,简单的长连接TCP应用层协议
void DiscardServer::onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time) { string msg(buf->retrieveAllAsString()); LOG_INFO << conn->name() << " discards " << msg.size() << " bytes received at " << time.toString(); }
-
daytime:短连接协议,在发送完当前时间后,由服务器主动断开连接
void DaytimeServer::onConnection(const TcpConnectionPtr& conn) { LOG_INFO << "DaytimeServer - " << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort() << " is " << (conn->connected() ? "UP" : "DOWN"); if (conn->connected()) { conn->send(Timestamp::now().toFormattedString() + "\n"); conn->shutdown();//主动断开连接 } }
-
time : 与daytime极其相似,只不过它返回的不是日期时间字符串,而是一个32bit的整数
void TimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn) { LOG_INFO << "TimeServer - " << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort() << " is " << (conn->connected() ? "UP" : "DOWN"); if (conn->connected()) { time_t now = ::time(NULL); int32_t be32 = sockets::hostToNetwork32(static_cast<int32_t>(now)); conn->send(&be32, sizeof be32); conn->shutdown(); } }
time客户端:time服务端发送的是二进制数据,不易读取,因此客户端来解析
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime) { if (buf->readableBytes() >= sizeof(int32_t)) { const void* data = buf->peek(); int32_t be32 = *static_cast<const int32_t*>(data); buf->retrieve(sizeof(int32_t)); time_t time = sockets::networkToHost32(be32); Timestamp ts(time * Timestamp::kMicroSecondsPerSecond); LOG_INFO << "Server time = " << time << ", " << ts.toFormattedString(); } else { LOG_INFO << conn->name() << " no enough data " << buf->readableBytes() << " at " << receiveTime.toFormattedString(); } } };
-
echo:前面的都是一个单向接收和发送数据,这是第一个双向发送的协议,即将服务端发送的数据原封不动的发送回去
void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time) { muduo::string msg(buf->retrieveAllAsString()); LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, " << "data received at " << time.toString(); conn->send(msg); }
-
Chargen: 只发送数据,不接受数据,且发送数据的速度不能快过客户端接收的速度
void ChargenServer::onConnection(const TcpConnectionPtr& conn) { LOG_INFO << "ChargenServer - " << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort() << " is " << (conn->connected() ? "UP" : "DOWN"); if (conn->connected()) { conn->setTcpNoDelay(true); conn->send(message_); } } void ChargenServer::onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time) { string msg(buf->retrieveAllAsString()); LOG_INFO << conn->name() << " discards " << msg.size() << " bytes received at " << time.toString(); }
-
五合一
前面的五个程序都用到了Evenloop,其实是一个Reactor,用于注册和分发IO事件
int main() { LOG_INFO << "pid = " << getpid(); EventLoop loop; // one loop shared by multiple servers ChargenServer chargenServer(&loop, InetAddress(2019)); chargenServer.start(); DaytimeServer daytimeServer(&loop, InetAddress(2013)); daytimeServer.start(); DiscardServer discardServer(&loop, InetAddress(2009)); discardServer.start(); EchoServer echoServer(&loop, InetAddress(2007)); echoServer.start(); TimeServer timeServer(&loop, InetAddress(2037)); timeServer.start(); loop.loop(); }
这就是Reactor模式复用线程的能力,让一个单线程程序同时具备多个网络服务功能
2、 文件传输
利用 onWriteComplete() 实现分段传输,做到不必一次全部读入内存。
3、 TCP的半关闭问题
- shutdown()没有直接关闭TCP连接,这样是为了收发数据的完整性
- TCP是个全双工协议,同一个文件描述符既可读也可写,shutdownWrite()关闭了“写”方向上连接,保留了读方向上的,这成为TCP的半关闭状态,如果直接close, 那么socket_fd就不能读或者写了
- muduo把主动关闭连接的行为分两步走,先关闭写端,等对方关闭后,在关闭本地的读端
- muduo这种关闭连接要求对方read到0字节后会主动关闭连接,当对方故意不关闭连接,muduo的连接就一直半开阿哲,消耗系统资源。必要时调用Tcp::connection::handleclose()强行关闭连接
- TCP正在关闭是在TcpConnection对象析构的时候,这里会用到RAII
4、TCP的分包问题
- 消息长度固定
- 使用特殊字符或字符串作为消息边界,如http中的heads以“\r\n"
- 在每条消息头部加一个长度字段
- 利用消息本身的格式分包,例如XML、JSON
5、muduo Buffer类的设计与使用
-
为什么非阻塞网络编程中应用层buffer是必须的
- non-blocking IO 的核心思想是避免阻塞在 read() 或 write() 或其他 IO 系统调用上,这样可以最大限度地复用 thread-of-control ,让一个线程能够服务于多个 socket 连接。这就是需要应用层 buffer 的原因。
- **TcpConnection必须要有output buffer ** :比如TCP发送了100kb的数据,但是咋write()调用,操作系统只接受了80kb,因为不想原地等待(非阻塞),所以要尽快交出控制权,返回事件循环中。
- 对于应用程序而言,它只管生成数据,它不应该关心到底数据是一次发送还是分成几次发送
-
TCP粘包问题
网络库在处理“socket可读”事件的时候必须一次性把socket的数据一次性读完(从操作系统的buff搬运到应用层的buff上面),否则会反复触发POLLIN事件,造成busy-loop.这是因为采用的是LT模式
-
Buffer的设计
- 对外表现是一块连续的内存,以方便客户代码的编写
- 其size()可以自动增长,适应不同的消息
- 内部以vector来保存数据
-
Buffer的数据结构就是三个指针,一个数组,具体看书
-
Buffer其他设计方案
-
自己管理内存不用vector
-
zerocopy,注意不是严格意义上的0拷贝,数据从内核到用户空间有一次拷贝,如libevent2.0.x设计方案
内存不是连续的,是分快的
-
6、一种自动反射消息的Protobuf网络传输方案,
此部分具体看书
7、限制服务器的最大并发连接数
这里的并发连接数是指同时支持的客户端的连接数
- 不希望程序超载
- 因为fd是稀缺资源
8、定时器
- 与时间相关的常见任务
- 获取当前时间,计算时间间隔
- 失去转换与日期计算
- 定时操作
- muduo使用的时间操作函数
- 计时:只使用 gettimeofday(2) 获取当前时间
- 定时:只使用 timerfd_* 系列函数。
- 在非阻塞服务端编程中,绝对不能用 sleep() 或类似的办法(这是因为该函数的实现可能用到了SIGALM,与多线程水火不容,见读书笔记4)来让程序原地等待,这会让主事件循环被挂起,程序失去响应。
- 测量两台机器的网络延迟与时间差
9、用timing wheel (时间轮)踢掉空闲连接
- 如果一个连接是如果若干秒没有收到数据,则认为是空闲连接
- 使用的数据结构是循环队列
10、 简单的消息广播服务
此后的内容具体看书