Pebble网络通信实现(上)

这部分不知该如何给个适合的标题,主要是想借此谈谈如何写个网络组件,跟上层业务解耦,以消息的形式通知上层业务,不需要过多关心网络的问题,发送消息时,只需要把buffer地址、长度和sessionid(这里不使用socket id是因为可能考虑到断线重连问题导致需要更新上层应用的fd,而使用某个类似sessionid去标识某个连接会比较好些)传递到网络组件部分,直接返回,网络组件会以协定好的消息格式打包并发送出去,若底层发送失败则会通知上层,因为网络要处理的问题比较复杂,上层业务不太适合使用原始套接字接口比如read/write等,防止出现诸如阻塞后面的业务等问题。

由于业务的特殊性,有些消息是不能丢失的,比如业务进程挂了或重启后,在这个阶段中收到的消息是需要处理,那如何才能保证这部分的功能?如果消息处理到一半是否算作废,考虑幂等性?一条消息要包含哪些内容?如何保证消息的完整性和有效性?当收到消息时以何种形式通知业务进程?业务进程是否需要感知到连接的断开?如果网络部分挂了或重启那确实没有办法,全部都要重连,实现一个网络组件不容易,考虑的问题太多,而且业务不同,可能技术选形也不同,比如使用单进程多线程那种,其中一个线程处理IO问题,其他线和处理逻辑,再弄个线程做些统计和负载相关的工作,一般我分析/经历过的(开源)项目,有的实现是单个网络进程,有的是网络线程,前者需要考虑进程间通信,一般使用共享内存存放消息,后者要考虑线程间的同步等问题,这方面dpdk有不错的实现,比如使用cas实现两个无锁队列收和发,其他开源项目也有类似的实现,比如那种消息中间件kafka,rocketmq。

这里我打算分几篇文章去实现,这边会先分析Pebble中的网络部分,也可能会分析下libevent/libev中的网络部分,像pink/redis类似的网络部分比较简单,其它的开源项目可能有但是没具体分析过。

我也会假设当你看到这篇文章的时候,你已经知道基本的socket api函数,比如read/write/accept等,知道其工作原理,知道阻塞和非阻塞,知道同步和异步,知道epoll/select等网络模型和其特点,知道tcp/ip等基本工作原理,主机/网络序,这些都可以在网上有不错的资料和书籍比如apue/高性能网络服务器编程等参考。

下面正式开始分析Pebble中的网络实现,Pebble中的网络实现总体上来说是比较简单的,各模块也比较清晰。

每条连接使用NetConnection类表示,里面缓存待发送的消息消息,但不缓存收到的消息,即只收取一次消息并通知上层处理后,再接收新消息,可能这么做的原因是考虑到消息的时效性?这里设计也有些问题会在分析代码的时候说明下,部分声明:

 28 class NetConnection {
 29 public:
 34     int32_t CacheSendData(const uint8_t* data, uint32_t data_len, bool full_pkg); 
 36     int32_t AppendSendData(const uint8_t* data, uint32_t data_len);
 37     
 38     int32_t RecvMsg(uint8_t* buff, uint32_t* buff_len);
 40     int32_t PeekMsg(const uint8_t** msg, uint32_t* msg_len);

 48     uint8_t* _buff;         // 接收缓冲区
 49     uint32_t _buff_len;     // 接收缓冲区大小
 50     uint32_t _msg_head_len; // 消息头长度,用于TCP分包
 51     
 52     uint32_t _recv_len;     // 已经接收的数据长度
 53     uint32_t _cur_msg_len;  // 待接收消息的总长度,这个长度由上层用户解析消息头后给出,连接根据这个去收取一个完整包
 54     int64_t  _arrived_ms;   // 接收消息的时间戳
   
 57     struct Msg {
 58         Msg() : _msg_len(0), _msg(NULL), _full_pkg(0) {}
 59         uint32_t _msg_len;
 60         uint8_t* _msg; 
 61         uint8_t  _full_pkg; 
 62     };  
 63     uint32_t _max_send_list_size;
 64     std::list _send_msg_list;
 65 };

上面是整个类的声明,相关的注释在旁边有说明,然后分析下收Recv和发Send,以及Poll实现,其他的都比较简单,不再这里分析,有兴趣可移至开源项目。

124 int32_t NetConnection::CacheSendData(const uint8_t* data, uint32_t data_len, bool full_pkg) {
125     if (data_len == 0 || data == NULL) {
126         return -1;
127     }
128     if (_send_msg_list.size() > _max_send_list_size) {
129         return -2;
130     }
131 
132     Msg msg;
133     msg._msg_len = data_len;
134     msg._msg     = (uint8_t*)malloc(data_len);
135     if (msg._msg == NULL) {
136         return -3;
137     }
138     msg._full_pkg = full_pkg ? 1 : 0;
139 
140     memcpy(msg._msg, data, data_len);
141     _send_msg_list.push_back(msg);
142     return 0;
143 }

CacheSendData处理缓存的消息,上面的代码有个性能问题,在GCC的实现中,list的size调用是O(N)的时间复杂度:

size_type size() const  { return std::distance(begin(), end()); }

而在VC中却是O(1)的时间复杂度:

size_type size() const { return (_Mysize); } 

可以参考下《Effective STL》

328 int32_t NetMessage::Send(uint64_t handle, const uint8_t* msg, uint32_t msg_len) {
329 
330     int32_t send_len = 0;
331     uint64_t local_handle = GetLocalHandle(handle);
332     const SocketInfo* socket_info = m_netio->GetSocketInfo(local_handle);
333     // UDP直接发,不缓存
334     if (socket_info->_state & UDP_PROTOCOL) {
335         if (socket_info->_state & CONNECT_ADDR) { // udp protocol connect
336             send_len = m_netio->Send(handle, (char*)msg, msg_len);
337         } else { // udp protocol listen
338             send_len = m_netio->SendTo(local_handle, handle, (char*)msg, msg_len);
339         }
340         return send_len == (int32_t)msg_len ? 0 : kMESSAGE_SEND_FAILED;
341     }
342 
343     // TCP
344     NetConnection* connection = GetConnection(handle);
345     if (connection == NULL) {
347         return kMESSAGE_UNKNOWN_CONNECTION;
348     }
350     // 有缓存,直接放入缓存中,消息排队
351     SendCacheData(handle, connection);
352     if (!connection->_send_msg_list.empty()) {
353         int32_t ret = connection->CacheSendData(msg, msg_len, true);
354         if (ret != 0) {
356             return kMESSAGE_CACHE_FAILED;
357         }
358         return 0;
359     }
360 
361     // 无缓存,直接发送到socket
362     send_len = m_netio->Send(handle, (char*)msg, msg_len);
363     if (send_len < 0) {
364         OnSocketError(handle);
365         return send_len;
366     }
367 
368     // 发送成功
369     if (send_len == (int32_t)msg_len) {
370         return 0;
371     }
372 
373     // 未发送数据缓存,send_len = 0 说明数据包完整
374     int32_t ret = connection->CacheSendData(msg + send_len, msg_len - send_len, send_len == 0);
375     if (ret != 0) {
377         return kMESSAGE_CACHE_FAILED;
378     }
379 
380     return 0;
381 }

700 void NetMessage::SendCacheData(uint64_t netaddr, NetConnection* connection) {
701     if (connection == NULL) {
702         connection = GetConnection(netaddr);
703         if (connection == NULL) {
705             return;
706         }
707     }
708 
709     // 每个连接默认最多缓存1000个消息,可以直接发完,避免net_util吃掉OUT事件了
710     while (!connection->_send_msg_list.empty()) {
711         NetConnection::Msg& msg = connection->_send_msg_list.front();
712         int32_t send_len = m_netio->Send(netaddr, (char*)msg._msg, msg._msg_len);
713         if (send_len < 0) {
714             OnSocketError(netaddr);
715             break;
716         }
717 
718         if (send_len == (int32_t)msg._msg_len) {
719             // 发送完整数据,清掉缓存
720             free(msg._msg);
721             connection->_send_msg_list.pop_front();
722         } else if (send_len > 0) {
723             // 发送部分数据,重新缓存
724             uint8_t* new_buff = (uint8_t*)malloc(msg._msg_len - send_len);
725             memcpy(new_buff, msg._msg + send_len, msg._msg_len - send_len);
726             free(msg._msg);
727             msg._msg = new_buff;
728             msg._msg_len -= send_len;
729             msg._full_pkg = 0;
730             break;
731         } else {// 发送0字节,缓存不变
732             break;
733         }
734     }
735 }

简化起见,有些类的作用在后面说明。

SendCacheData实现中,有几个问题:
情况一:我们不考虑消息的丢失,在Send数据时,传入的是msg和msg_len表示要发送的数据buffer和长度size,由于每次Send的时候,是对原msg内容的拷贝,而非raw 指针;
情况二:另外尝试SendCacheData发送缓存里消息,代码行724〜729对要发送的数据拷贝;
以上两点,可能作者有其他想法,如果上层处理完请求并响应,那么响应结果是const的,这里如果要malloc/new/local buffer,那么针对情况一可以省掉这次copy的;
一条待发送的msg,这里的结构是:

 78     struct Msg {
 79         Msg() : _msg_len(0), _msg(NULL), _full_pkg(0) {}
 80         uint32_t _msg_len;
 81         uint8_t* _msg; 
 82         uint8_t  _full_pkg;
 83     };

如果只发送了一部分,那么总会涉及到拷贝,是否可以加个offset表示从哪个位置开始发送,根据发送的字节数移动offset,那么就避免没发送成功,需要暂存数据时引起可能多次copy和malloc等;
情况三:每次malloc/new/free/delete可能造成碎片啥的以及开销,可以用其他方式来代替,把这个问题留给你。

然后回到代码351〜378,主要思想是:
如果对一个socket fd设置非阻塞,并且使用epoll网络模型的话,比如ET模式,当应用层要发送数据时,或者该socket fd发生可写事件时进行回调,如果该连接对象缓存中有待发送的消息,则尝试发送缓存中的消息;
然后再判断缓存中是否还有消息,如果有直接push到缓存列表中然后返回,因为发不了;
否则,尝试发送Send,不能发完则缓存,因为ET模式在socket fd从不可写到可写时,会回调咱们的回调发送函数,此时可以继续Send,如果没有要发送的数据,建议移走该事件,等需要的时候再注册,貌似这里没有进行该操作。

以下是应用层从接连中获取一条完整消息的逻辑,非网络到server,主要是根据代码168行判断:

166 int32_t NetConnection::RecvMsg(uint8_t* buff, uint32_t* buff_len) {
168     if (_recv_len > 0 && _recv_len == _cur_msg_len) {
169         if (*buff_len < _cur_msg_len) {
170             return -1;
171         }
172         memcpy(buff, _buff, _cur_msg_len);
173         *buff_len = _cur_msg_len;
174 
176         _recv_len    = 0;
177         _cur_msg_len = 0;
178         return 0;
179     }
182     return -2;
183 }
480 int32_t NetMessage::Recv(uint64_t handle, uint8_t* buff, uint32_t* buff_len,
481                          MsgExternInfo* msg_info) {
482 
483     NetConnection* connection = GetConnection(handle);
484     if (connection == NULL) {
486         return kMESSAGE_UNKNOWN_CONNECTION;
487     }
488 
489     int32_t ret = connection->RecvMsg(buff, buff_len);
490     if (ret != 0) {
492         return kMESSAGE_RECV_FAILED;
493     }
494 
495     msg_info->_msg_arrived_ms = connection->_arrived_ms;
496     msg_info->_remote_handle  = handle;
497     msg_info->_self_handle    = handle;
498 
499     const SocketInfo* socket_info = m_netio->GetSocketInfo(handle);
500     if (socket_info->_state & ACCEPT_ADDR) {
501         msg_info->_self_handle = m_netio->GetLocalListenAddr(handle);
502     }
503     if ((socket_info->_state & UDP_PROTOCOL) && (socket_info->_state & LISTEN_ADDR)) {
504         msg_info->_remote_handle = connection->_peer_addr;
505     }
506 
507     return 0;
508 }

从网络到server的消息接收是:

580 int32_t NetMessage::Poll(uint64_t* handle, int32_t* event, int32_t timeout_ms) {
581      //... more code
587     int32_t ret = m_epoll->Wait(timeout_ms);
588     if (ret <= 0) {
589         return -1;
590     }    
591 
592     uint32_t events  = 0;
593     uint64_t netaddr = 0;
594     ret = m_epoll->GetEvent(&events, &netaddr);
595     if (ret != 0) {
596         return -1;
597     }
598 
599     if (events & EPOLLERR) {
601         OnSocketError(netaddr);
602         return kMESSAGE_GET_ERR_EVENT;
603     } 
604 
605     if (events & EPOLLOUT) {
606         SendCacheData(netaddr, NULL);
607     }
609     ret = -1;
610 
611     const SocketInfo* socket_info = m_netio->GetSocketInfo(netaddr);
612     if (events & EPOLLIN) {
613         // 收包处理,区分TCP和UDP的收包逻辑
614         if (socket_info->_state & TCP_PROTOCOL) {
615             if (socket_info->_state & LISTEN_ADDR) {
616                 uint64_t peer_handle = m_netio->Accept(netaddr);
617                 if (peer_handle != INVAILD_NETADDR) {
618                     CreateConnection(peer_handle);
619                 }
620                 return ret;
621             }
622             do {
623                 ret = RecvTcpData(netaddr);
624             } while (ret == RECV_CONTINUE);
625         } else {
626             ret = RecvUdpData(netaddr);
627         }
628         //... more code
634         *handle = netaddr;
635     }
636 
637     return ret;
638 }

这里的Poll由最外层Update--->ProcessMessage--->Poll驱动,和以往一个for循环依次处理有事件发生的fd不同,如果是监听套接字则可读则表示有新的连接到来;这里也区分了TCP和UDP情况,考虑TCP读数据:

737 int32_t NetMessage::RecvTcpData(uint64_t netaddr) {
738     // 每次收取1包,等待用户消费完后再收取下一包
739     NetConnection* connection = GetConnection(netaddr);
740     if (connection == NULL) {
742         return kMESSAGE_UNKNOWN_CONNECTION;
743     }
744 
745     if (connection->HasNewMsg()) {
746         return RECV_END_PKG;
747     }
748 
749     uint32_t old_len = connection->_recv_len; // 已经收取的数据长度
750     uint32_t need_read = 0;     // 还需要读取的长度
751     bool get_msg_len = false;   // 是否需要解析头获取数据部分长度
752     if (old_len < m_msg_head_len) {
753         need_read = m_msg_head_len - old_len;
754         get_msg_len = true;
755     } else {
756         if (connection->_cur_msg_len < old_len) {
758             CloseConnection(netaddr);
759             return kMESSAGE_RECV_INVALID_DATA;
760         }
761         need_read = connection->_cur_msg_len - old_len;
762     }
764     int32_t recv_len = m_netio->Recv(netaddr, (char*)connection->_buff + old_len, need_read);
765     if (recv_len < 0) {
767         OnSocketError(netaddr);
768         return kMESSAGE_RECV_FAILED;
769     }
770     connection->_recv_len += recv_len;
771     if (recv_len < (int32_t)need_read) {
772         // 未收完期望的数据,等待下次再收
773         return RECV_END_PART;
774     }
775 
776     if (get_msg_len) {
777         int32_t msg_data_len = m_get_msg_data_len_func(connection->_buff, m_msg_head_len);
778         if (msg_data_len < 0) {
780             CloseConnection(netaddr);
781             return -1;
782         }
783         connection->_cur_msg_len = m_msg_head_len + msg_data_len;
784         connection->_arrived_ms  = TimeUtility::GetCurrentMS();
785     }
786 
787     if (connection->HasNewMsg()) {
788         return RECV_END_PKG;
789     }
790 
791     return RECV_CONTINUE;
792 }

由于每次只收一个消息,如果连接中有未处理的完整消息,则不接收;
否则算下还要接收多少need_read = connection->_cur_msg_len - old_len
这里的逻辑就是由msghead+msgbody组成,先接收完整的head,从中解出body的长度,其中Recv是要从套接字上read数据,由于可能读到一半的消息,故每次read时,需要计算下need_read;

以上处理消息和管理连接的实现方式大概就分析到这,可能还有些其他的没在这里分析,但这些也比较简单。

下面大概分析下流程,比如从一个SYN包请求连接时,上层调用accept后到读数据及发送如何进行的,主要还是由Message::Poll()来分发读写逻辑。

do { if (Update() <= 0) Idle() }while (true)--->ProcessMessage()--->Message::Poll()--->m_net_message->Poll()--->m_netio->Accept()--->accept()

会对每个连接找一个空位,用于快速当事件发生时定位到哪个SocketInfo对象:

 812 NetAddr NetIO::AllocNetAddr()
 813 {
 814     NetAddr ret = INVAILD_NETADDR;
 815     if (0 != m_free_sockets.size())
 816     {
 817         ret = m_free_sockets.front();
 818         m_free_sockets.pop_front();
 819     }
 820     else if (m_used_id < MAX_SOCKET_NUM)
 821     {
 822         ret = (m_used_id++);
 823         m_sockets[ret].Reset();
 824         ret |= (static_cast(m_sockets[ret]._uin) << 32);
 825     }
 826     return ret;
 827 }
390         m_epoll->AddFd(new_socket, EPOLLIN | EPOLLERR, net_addr)
 206 int32_t Epoll::AddFd(int32_t fd, uint32_t events, uint64_t data)
 207 {
 208     struct epoll_event eve;
 209     eve.events = events;
 210     eve.data.u64 = data;
 211     return epoll_ctl(m_epoll_fd, EPOLL_CTL_ADD, fd, &eve);
 212 }

这样当fd上有事件发生时,取出eve.data.u64,知道是哪个SocketInfo实例对象。
其中m_netio是由NetMessage类拥有的,只有一个,提供收发消息的功能。

本篇写完会写下篇,然后整理下Pebble协程和libco协程的实现原理,七月份会回顾下这阶段的收获并准备分析brpc,因为在github上看到它的文档里有些技术点比较值得研究,往后可能先分析下SPDK应用框架,和以前工作用到过的DPDK有点差别,主要还是关于多线程框架和性能方面的问题,看它说明:“对CPU core和线程的管理;线程间的高效通信;I/O的的处理模型以及数据路径(data path)的无锁化机制。”就觉得有意思,会对比下以往知道的多线程框架优缺点和应用场景。后面的时间会继续分析下LevelDB实现,今年的计划大致是这样,过的还蛮充实的。

今天刚搬家,跑了六七趟,本来想找搬家公司的,后来想想,一方面不是是远,来回骑个半小时单车就算一趟,另一方面,东西不多,就被子和衣服,还有几本书,现在住的地方到公司只有四个公交站,出门就是站台,这样,可以每天省下近四十分钟,后面打算每天七点半左右起床,然后花一个多小时分析些代码并整梳理,晚上下班回来如果不是很累的话也花个一小时左右,这些是计划,希望多学点技术,走的更远一些。

你可能感兴趣的:(Pebble网络通信实现(上))