muduo是一个高质量的Reactor网络库,采用one loop per thread + thread pool架构实现,代码简洁,逻辑清晰,是学习网络编程的很好的典范。
使用Sockets API进行网络编程是一件简单地技术,但是高级语言的Sockets库并没有对Sockets API提供更高层次的封装,直接用它编写网络程序很容易掉到陷阱里,因此需要网络库来降低开发难度,网络库的价值还在于能方便地处理并发连接。
1.库是将代码集合成的一个产品,供程序员调用。面向对象的代码组织形式而成的库也叫类库。面向过程的代码组织形式而成的库也叫函数库。
在函数库中的可直接使用的函数叫库函数。开发者在使用库的时候,只需要使用库的一部分类或函数,然后继续实现自己的功能。
框架则是为解决一个(一类)问题而开发的产品,框架用户一般只需要使用框架提供的类或函数,即可实现全部功能。可以说,框架是库的升级版。开发者在使用框架的时候,必须使用这个框架的全部代码。
2.库中的类相对比较独立,我们编写应用的时候需要编写一些“胶水代码”来粘合。
框架是恶能够应用于特定领域的,不需要编写过多的“胶水代码”
3.框架与库的重要区别是:框架提供用户一些回调函数,使得框架能够调用我们所编写的回调函数,这就使得控制反转了
muduo库可以认为是一个小型框架。
muduo库中有许多Event Loop(如果是单线程的话只有一个Event Loop),这个Event Loop实际上就是poll、epoll机制,是一个循环,在等待网络事件,如果网络事件触发,就回调我们的应用。
比如当一个客户端连接的时候,能够回调表示客户端已连接,那么客户端连接以后的一些具体操作,就可以在App中编写逻辑,当有数据到来的时候,muduo库能够回调有消息到来的对应函数,使得具体的业务逻辑可以在应用层进行编写,而不是把具体的业务放在库中编写。
面向对象的编程风格
muduo库只暴露具体类,不暴露抽象类,也不适用虚函数做接口
基于对象的编程风格
使用boost::function、boost::bind实现基于对象的编程风格
muduo/base目录是一些基础库,都是用户可见的类,可以在用户程序中直接使用。
base
AsyncLogging.(h,cc) 异步日志backend
Atomic.h 原子操作与原子整数
BlockingQueue.h 无界阻塞队列(消费者生产者队列)
BoundedBlockingQueue.h 有界阻塞队列
Condition.h 条件变量,与Mutex一同使用
copyable.h 一个空基类,用于标识(tag)值类型
CountDownLatch "倒计时门闩"同步
Date.{h,cc} Julian日期库(即公历)
Exception.{h,cc} 带stack trace的异常基类
Logging.{h,cc} 简单的日志,可搭配AsynLogging使用
Mutex.h 互斥器
ProcessInfo.{h,cc} 进程信息
Singleton.h 线程安全的singleton
StringPiece.h 从Google开源代码借用的字符串参数传递类型
tests 测试代码
Thread.{h,cc} 线程对象
ThreadLocal.h 线程局部数据
ThreadLocalSingleton.h 每个线程一个singleton
ThreadPool.{h,cc} 简单的固定大小线程池
Timestamp.{h,cc} UTC时间戳
TimeZone.{h,cc} 时区与夏令时
Types.h 基本类型的声明,包括muduo::string
时间戳,关于时间的类
(1)BOOST_STATIC_ASSERT(sizeof(Timestamp) == sizeof(int64_t));编译时断言,也就是在编译时刻就能判断下面等式是否成立, assert是运行时断言
(2)less_than_comparable
提供两个转型函数,隐式转换函数和向下转型函数
原子操作与原子整数操作的封装类
异常机制,通过异常机制我们可以轻松的捕获要出现的异常
线程类的实现,采用的是基于对象的方式实现的。启动线程(创造线程)->线程的入口函数->runInThread()->回调函数(通过构造函数引入的)
Mutex.h MutexLock/MutexLockGuard
互斥锁
MutexLock有死锁的风险,比如:
MutexLock mutex;
void f()
{
mutex.lock();
if(...)
{
...
return;//从这里退出的话没有解锁,所以存在死锁的风险
}
mutex.unlock();
}
使用MutexLockGuard 的话,会在构造函数中加锁,析构函数中解锁,所以更加保险
也就是说,return的时候,函数销毁,自动调用析构函数,从而解锁
Condition.{h,cc}
条件变量
CountDownLatch.{h,cc}
实际上是对条件变量Condition类的封装,倒计时门闩类。既可以用于子线程等待主线程发起“起
跑”,也可以用于主线程等待子线程初始化完毕才开始工作
缓冲区队列上就是生产者生产的资源,用于消费者进行消费。如果是无界缓冲区,那么生产者就可以无限制的生产,而不必等待消费者进行消费。否则如果达到缓冲区队列的最大值时,必须等消费者消费之后才能继续生产。
无界缓冲区,借助于queue
有界缓冲区,借助于助于boost中的环形缓冲区
生产者-消费者模型既可以用信号量来实现,也可以用条件变量,上面两者用的都是条件变量。
线程池的实现,这个线程池是固定线程池,线程的个数是固定的,不能自动伸缩。线程池问题本质上也是一个生产者、消费者问题。
线程池中有一个线程队列还有一个任务队列,外部线程可以往线程池中的任务队列添加任务,这些外部线程相当于生产者;一旦任务队列中有任务就唤醒线程队列中的线程来执行这些任务,这些线程相当于消费者线程。
线程安全的单例实现,保证一个类只有一个实例,单例对象
pthread_once能够保证某函数只被执行一次,这样的话能够保证对象只被生成一个并且是线程安全的
关键实现: pthread_once(&ponce_, &Singleton::init);
在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。
在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。
但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问。
POSIX线程库通过维护一定的数据结构来解决这个问题,这个些数据称为(Thread-specific Data,或 TSD)。
线程特定数据也称为线程本地存储TLS(Thread-local storage)
对于POD类型的线程本地存储,可以用__thread关键字,非POD类型可以通过TSD来解决。
POSIX线程库通过4个函数来操作线程特定数据
pthread_key_create
创建一个key,一旦一个线程创建了一个key,所有线程都有这个key,不同的线程都可以通过相同的key访问实际数据,但数据是各不相同的
pthread_key_delete 删除key
pthread_getspecific 获取特定数据
pthread_setspecific 指定特定数据
muduo中的ThreadLocal
线程本地单例封装,每个线程都有T类型的单例对象,前面的Singleton是所有线程共有的单例对象
整个ThreadLocalSigleton类用了两种线程本地存储机制,一种是__thread,另一种是TSD
日志类的封装
TRACE
指出比DEBUG粒度更细的一些信息事件(开发过程中使用)
DEBUG
指出细粒度信息事件对调试应用程序是非常有帮助的。(开发过程中使用)
INFO
表明消息在粗粒度级别上突出强调应用程序的运行过程。
WARN
系统能正常运行,但可能会出现潜在错误的情形。
ERROR
指出虽然发生错误事件,但仍然不影响系统的继续运行。
FATAL
指出每个严重的错误事件将会导致应用程序的退出。
muduo库的日志输出很简单,靠的是几个宏,既可以输出到标准输出,也可以输出到文件
整个过程是首先构造一个Logger对象,然后调用stream()方法,返回一个LogStream对象,再通过调用这个对象的operator<<()进而输出日志到标准输出或指定文件,如下:
缓冲区类,就是调用插入运算符,把它格式化到缓冲区中。
self& operator<<(short);
self& operator<<(unsigned short);
self& operator<<(int);
self& operator<<(unsigned int);
self& operator<<(long);
self& operator<<(unsigned long);
self& operator<<(long long);
self& operator<<(unsigned long long);
StringPiece.h 可以把这个类当成是字符串,用以实现高效的字符串传递
const char* s1;
std::string s2;
void foo(const StringPiece& x);
这里既可以用const char*、也可以用std::string类作为参数传递,并且不涉及内存拷贝
如果采用void foo(const std::string& x);
对于前面两者也都可以实现
但对于const char* str_就涉及内存拷贝,效率不高
日志滚动
日志滚动条件
文件大小(例如每写满1G换下一个文件)
时间(每天零点新建一个日志文件,不论前一个文件是否写满)日志滚动条件
上面两个条件任意一个满足,都会产生一个新的文件
一个典型的日志文件名
logfile_test.20130411-115604.popo.7743.log
日志文件的basename.年.月.日.时.分.秒.主机名称.进程ID.后缀
FileUtil.{h,cc}中的class AppendFile,文件类的实现;class ReadSmallFile,小文件类的实现