目录
生疏
- 在进行日志库的开发时,涉及到的几个重要的宏:
__FILE__
:文件名称,包含路径__LINE__
:行号__func__
:函数名称
- cpp中含有默认参数时,函数声明和定义不能同时出现
- cpp中静态成员变量以及静态成员函数在定义时候不能加static关键字,同理还有virtual关键字
- __thread关键字:该关键字修饰的变量为线程局部存储变量
- FD_CLOEXEC标志:这是文件描述符的标志之一。在新创建一个文件描述符时都应该设置这个标志位,特别是多线程程序下。man文档描述如下:
__VA_ARGS__
:一个可变参数宏,一般使用在宏定义中,例如:
#define LOG_ERROR(formatString, ...)\
do {\
Logger& logger = Logger::getInstance();\
logger.setLogLevel(static_cast<int>(Logger::LoggerLevel::ERROR));\
char buffer[1024] = {0};\
snprintf(buffer, 1024, formatString, ##__VA_ARGS__);\
} while (0)
- 名称空间
- 匿名命名空间:在使用
namespace
关键字定义名称空间时不指定名称,例如namespace {}。 - 引用全局命名空间时一般指定
::
,例如main函数就是在全局命名空间下。
- 匿名命名空间:在使用
#pragma GCC diagnostic ignored
的使用- extern关键字的作用
- __builtin_expect的使用
- syscall(SYS_gettid)函数
- open("/dev/null)
- enable_shared_from_this的使用
- mutable关键字的使用:
- 修饰类的成员变量
mutable std::mutex mutex_; size_t EventLoop::queueSize() const { std::lock_guard<std::mutex> lock(mutex_); return pendingFunctors_.size(); } error: binding reference of type ‘std::lock_guard<std::mutex>::mutex_type&’ {aka ‘std::mutex&’} to ‘const std::mutex’ discards qualifiers
- lambda表达式中修饰以值捕获的变量,这样仅仅可以让编译通过。但是lambda表达式的函数体中修改以值捕获的变量不会影响外部变量的改变。自导自演
- SO_KEEPALIVE选项:这是一个套接字选项,用于判断连接是否存活。但是在实际的游戏服务器开发中,在应用层上实现心跳包机制。
阻塞、非阻塞
- 阻塞:典型的一次IO一般分为两个阶段:数据准备以及数据读写(数据在内核空间与用户空间之间的往返)。在数据准备阶段,调用IO方法的线程进入阻塞状态。例如recv方法默认就是阻塞的,如果没有数据到来就会阻塞当前线程,当前线程将会挂起,状态从运行态变为阻塞态
- 非阻塞:典型的一次IO一般分为两个阶段:数据准备以及数据读写。在数据准备阶段,即使没有数据,调用IO方法的线程也不会被阻塞,而是直接返回值。例如:当套接字设置为非阻塞的,调用recv方法将会立即返回,即使内核的接收缓冲区没有数据。这样的话线程还能有机会继续运行着。
- 综上:根据系统io操作的就绪状态来判断一个方法是阻塞的还是非阻塞的。
同步、异步
- 同步:典型的一次IO一般分为两个阶段:数据准备以及数据读写。在数据读写阶段,例如recv接口就是一个同步IO接口,如果TCP内核的接收缓冲区有数据,用户态的应用程序则会等将内核的接收缓冲区数据拷贝到用户缓冲区以后才会继续执行下去。一些IO复用函数例如poll、epoll、select都是同步IO
- 异步:典型的一次IO一般分为两个阶段:数据准备以及数据读写。在数据读写阶段,用户态的应用程序需要给内核传递一个buffer(这是一个传入传出参数)以及指定一种通知方式,当内核准备好数据就会以信号、事件回调等方式通知应用程序。这一期间,用户态的应用程序可以继续执行下去,不用等内核准备数据。例如aio_read接口以及aio_write接口。
- 综上:根据应用程序和内核的交互方式来判断异步和同步,在数据读写阶段,应用程序是否需要等待数据的准备才可以继续执行其他逻辑。
Linux上的五种IO模型
- 阻塞IO:分为同步阻塞IO和异步阻塞IO。同步阻塞IO常用,异步阻塞IO一般少见。
- 非阻塞IO:分为同步非阻塞IO和异步非阻塞IO。对于同步非阻塞IO,因为非阻塞IO会立即返回,所以一般需要在循环中去判断数据是否准备好,但这个循环判断也是在白白消耗CPU,没有意义。因此只有在事件已经发生的情况下操作非阻塞IO才能提高程序的效率,因此开发中常使用IO多路复用或者信号驱动。
- IO多路复用:IO复用函数本身默认是阻塞的,但是可以通过超时参数设置成非阻塞的。
- 信号驱动:应用程序调用
系统调用
来注册信号的处理程序,当发生相应的事件以后,内核会通知应用程序,在这期间应用程序可以继续执行其他逻辑。 - 异步IO:数据从内核空间拷贝到用户空间或者从用户空间拷贝到内核空间,也由操作系统完成,而不是由用户程序完成。用户程序可以放飞自我,继续执行其他的逻辑。数据由操作系统拷贝完成以后,应用程序处理数据。
Reactor模式
- 主流的网络库比如说libevent,muduo都是用的这个模式,这是一种高效的事件处理模式,同步IO模型通常使用Reactor模式实现。
- 在Reactor模式中,主线程只负责监听文件描述符上是否有事件发生,有的话立即将事件通知工作线程。读写数据、接受新的连接、以及处理客户请求都在工作线程中完成。Reactor模式的重要组件如下:
- Event:事件
- Reactor:反应堆
- Demultiplex:事件分发器
- EventHandler:事件处理器
muduo的基本使用
- 参考https://github.com/chenshuo/muduo-tutorial/tree/master,将其克隆至本地,然后选择第二种构建方式。
- echo-server示例如下:
- EchoServer.h
#pragma once #include "muduo/net/TcpServer.h" #include "muduo/net/EventLoop.h" #include "muduo/net/InetAddress.h" #include "muduo/net/TcpConnection.h" #include "muduo/base/AsyncLogging.h" #include "muduo/base/Logging.h" #include "muduo/base/CurrentThread.h" #include <memory> class EchoServer { public: EchoServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress addr); void start() { _serverptr->start(); } private: std::unique_ptr<muduo::net::TcpServer> _serverptr; muduo::net::EventLoop * _eventloop; void onConnection(const muduo::net::TcpConnectionPtr&); void onMessage(const muduo::net::TcpConnectionPtr&, muduo::net::Buffer*, muduo::Timestamp); };
- EchoServer.cc
#include "EchoServer.h" #include <functional> #include <string> EchoServer::EchoServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress addr) : _eventloop(loop){ _serverptr.reset(new muduo::net::TcpServer(_eventloop, addr, "ECHO-SERVER")); _serverptr->setConnectionCallback(std::bind(&EchoServer::onConnection, this, std::placeholders::_1)); _serverptr->setMessageCallback(std::bind(&EchoServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _serverptr->setThreadNum(4); } void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn) { LOG_TRACE << conn->peerAddress().toIpPort() << " -> " << conn->localAddress().toIpPort() << " is " << (conn->connected() ? "UP" : "DOWN"); } void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time) { std::string msg(buf->retrieveAllAsString()); LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toString(); conn->send(msg); }
- main.cc
#include "EchoServer.h" int kRollSize = 500*1000*1000; std::unique_ptr<muduo::AsyncLogging> g_asyncLog; void asyncOutput(const char* msg, int len) { g_asyncLog->append(msg, len); } void setLogging(const char* argv0) { muduo::Logger::setOutput(asyncOutput); char name[256]; strncpy(name, argv0, 256); g_asyncLog.reset(new muduo::AsyncLogging(::basename(name), kRollSize)); g_asyncLog->start(); } int main(int argc, const char* argv[]) { // 设置日志级别 muduo::Logger::setLogLevel(muduo::Logger::TRACE); setLogging(argv[0]); LOG_INFO << "pid = " << getpid() << ", tid = " << muduo::CurrentThread::tid(); muduo::net::EventLoop loop; muduo::net::InetAddress listenAddr(8888); EchoServer server(&loop, listenAddr); server.start(); loop.loop(); return 0; }
- 编译:
g++ main.cc EchoServer.cc -o test -lmuduo_base -lmuduo_net
- 从echo server这个例子,可知基于muduo开发服务器程序的步骤如下:
- 你的服务器主类中组合TcpServer对象以及EventLoop对象(这个EventLoop其实就是一个mainLoop)
- 在服务器主类中注册处理用户连接的创建和断开的回调函数以及处理读写事件的回调函数
- 设置合适的服务器线程数量,内部的EventThreadPool会根据设置的线程数量创建相应个数的subLoop。如果不设置线程数量,则服务器程序默认只有1个mainLoop负责监听客户端的连接请求以及和客户端通信。
- 启动server
server.start();
- mainLoop进入循环,监听客户端的连接请求
loop.loop();