TCP网络编程最本质是处理三个半事件
1,连接建立:服务器accept接收连接,客户端发起连接
2,连接断开:主动断开(close、shutdown),被动断开(read返回0)
3,消息到达:文件描述符可读
4,消息发送完毕:这算半个。对于低流量的服务,可不关心这个事件;这里的发送完毕是指数据写入操作系统缓冲区,将由TCP协议栈负责数据的发送与重传,不代表对方已经接收到数据
在接收缓冲区内:
当一个套接字有数据到来的时候,会先被内核接收,网络库的可读事件就被触发,可读事件触发将数据从内核缓冲区移动到应用层的缓冲区中,
并且网络库会回调一个函数OnMessage
,但收到的数据包有可能不是一个完整的包,OnMessage
中就需要根据协议判断这个数据包是否是一个完整的数据包,如果不是一个完整的数据包就OnMessage
就立刻返回,知道内核中又接收了一些数据,网络库事件循环中的可读事件就又会被触发,数据就又会移动到应用层缓冲区尾部,OnMessage
再判断是一个完整的数据包了,就取出数据包read
、解包decode
、计算处理compute
、将处理结果打包encode
、发送write
在发送缓冲区: write(buf,…)
将缓冲区buf的数据填充到内核缓冲区,如果内核缓冲区的数据足够的话就全部填充进去,网络库会触发调用一个OnWriteComplete
发送完成事件;如果内核缓冲区不足以容纳数据,就要把数据追加到应用层的发送缓冲区中,当内核中的数据发送出去了,有足够的空间了就会触发套接口的可写事件,就会将应用层缓冲区的数据填充到内核发送缓冲区中,如果应用层的数据都发送完了就也触发OnWriteComplete
这个事件
EchoServer类图
用EchoServer
类看看muduo库对这三个半事件的封装
类图:
通过TcpServer提供了三个接口来注册三个回调函数,连接和断开通过一个标记用的都是setConnectionCallBack_
,EchoServer
包含了一个TcpServer
对象,(就是基于对象的编程思想!!),在EchoServer的构造函数中会去调用TcpServer的三个函数,将回调函数注册进去:它要注册的回调函数是EchoServer的成员函数,连接建立与连接断开onConnection
函数,将connectionCallback_
注册进去;消息到达事件onMessage
,将setMessageCallbak
注册进去。到时候连接建立或者断开了就会调用onConnection
,消息到达事件产生了就会调用onMessage
函数
也就是编程的时候,只需要定义一个服务器类EchoServer
,将TcpServer包含进来(对象),在EchoServer中提供感兴趣的几个回调函数,创建对象时会在TcpServer的构造函数中调用TcpServer的接口,将这些回调函数注册到EchoServer回调函数里去
…
它的main函数:
首先定义了一个EventLoop
的对象,这就是事件循环的对象
**?*每个线程都有一个EventLoop对象(最多只有一个,也可以没有,没有的话就不是IO线程)one loop per thread+threadpool
然后初始化一个地址对象listenAddr
,
构造一个对象server,传入loop地址和地址对象,由类图可见TcpServer中由start()
,最后的loop.loop()是在不断地捕捉事件,一旦事件到来就会回调EchoServer中相应的回调函数
EventLoop的封装
(点开就是吓一跳==)
先看看简化代码,了解这个类的实现过程后,再完善起来
.h文件?
namespace muduo
{
namespace net
{
///
/// Reactor, at most one per thread. EventLoop就是Recator模式的封装,每个线程最多一个
///
/// This is an interface class, so don't expose too much details.
class EventLoop : noncopyable
{public:typedef std::function<void()> Functor;EventLoop();~EventLoop(); // force out-line dtor, for std::unique_ptr members.////// Loops forever.////// Must be called in the same thread as creation of the object.///void loop();//事件循环函数,该函数不能跨线程调用void assertInLoopThread(){if (!isInLoopThread()){abortNotInLoopThread();}}bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
private:void abortNotInLoopThread();static EventLoop* getEventLoopOfCurrentThread();bool looping_; /* atomic */ //是否处于循环的状态const pid_t threadId_;//记录当前对象属于哪一个线程,当前对象所属线程id
};
}
}
#endif //MUDUO_NET_EVENTLOOP_H
.cc文件?
每一个线程最多有一个EventLoop对象,.cc
文件第一行代码就定义一个线程局部存储的当前线程EventLoop对象指针:__thread EventLoop* t_loopInThisThread = 0;
,再提一下,因为有__thread
这个关键字就表示每一个线程都有这样的一个变量,否则的话这个变量是共享的
创建对象成功的话,这个指针t_loopInThisThread
就指向当前对象,当然这个对象得是第一次创建,因为对象最多只能有一个。
报错都用日志的记录方式
构造函数
初始化looping为false,表示当前还没有处于循环的状态,并把当前线程的真实id初始化给ThreadId_
EventLoop::EventLoop():looping(false),thraedId_(CurrentThread::tid())
{LOG_TRACE << "EventLoop create "<<this<<"in thread "<<threadId_;//日志的记录if (t_loopInThisThread)//检查当前线程是否已经创建了其他EventLoop对象,如果已经创建了则LOG_FATAL终止程序{LOG_FATAL << "Another EventLoop " << t_loopInThisThread<< " exists in this thread " << threadId_;}else{t_loopInThisThread = this;//既然创建了一个对象,那么当前的对象指针就指向this}
}
**析构函数:**指向对象的指针置空
EventLoop::~EventLoop()
{LOG_DEBUG << "EventLoop " << this << " of thread " << threadId_<< " destructs in thread " << CurrentThread::tid();t_loopInThisThread = NULL;
}
loop函数:事件循环
事件循环,该函数不能跨线程调用,只能在创建该对象的线程中调用
void EventLoop::loop()
{assert(!looping_);//断言还没有开始循环assertInLoopThread();//断言当前处于创建该对象的线程中,函数的实现?looping_ = true;//开始要事件循环了,先将looping_置为trueLOG_TRACE << "EventLoop " << this << " start looping";//打印事件::poll(NULL, 0, 5*1000);//NULL表示当前没有关注事件,关注事件个数为0,等待5秒,仅仅而已,走个场LOG_TRACE << "EventLoop " << this << " stop looping";//loop完也打印一下looping_ = false;//并置回false
}
assertInLoopThread()
判断是都处于创建该对象的线程中
void assertInLoopThread(){if (!isInLoopThread())//只需要将当前线程的id与threadId比较是否相等?{abortNotInLoopThread();//阻止程序?}}bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }void EventLoop::abortNotInLoopThread()
{LOG_FATAL << "EventLoop::abortNotInLoopThread - EventLoop " << this<< " was created in threadId_ = " << threadId_<< ", current thread id = " << CurrentThread::tid();
}//这就是日志的用法啦,开心