这部分不知该如何给个适合的标题,主要是想借此谈谈如何写个网络组件,跟上层业务解耦,以消息的形式通知上层业务,不需要过多关心网络的问题,发送消息时,只需要把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实现,今年的计划大致是这样,过的还蛮充实的。
今天刚搬家,跑了六七趟,本来想找搬家公司的,后来想想,一方面不是是远,来回骑个半小时单车就算一趟,另一方面,东西不多,就被子和衣服,还有几本书,现在住的地方到公司只有四个公交站,出门就是站台,这样,可以每天省下近四十分钟,后面打算每天七点半左右起床,然后花一个多小时分析些代码并整梳理,晚上下班回来如果不是很累的话也花个一小时左右,这些是计划,希望多学点技术,走的更远一些。