一、接收后解包
将这个流程抽象出来,这个流程也是现在所有网络通信库都要做的工作:
1 while(退出条件)
2 {
3 //1. 检测非侦听socket可读
4 //2. 处理可读事件
5 //3. 检测可读取的字节数,出错就关闭,不出错,将收取的字节放入连接的读缓冲区
6 //循环做以下处理
7 //4. 检测可读缓冲区数据大小是否大于等于一个包头大小: 否,数据不够一个包,跳出该循环;
8 // 是,从包头中得到一个包体的大小,检测读缓冲区是否够一个包头+包体的大小;否,数据不够一个包,跳出循环
9 // 是,解包,根据包命令号,处理该包数据,可以产生一个任务,丢入任务队列。
10 // 从可读缓冲区中移除刚才处理的包数据的字节数目。
11 // 继续第4步。
12 }
之后将具体的业务逻辑放入任务队列。当加入任务后,任务队列线程被唤醒,从任务队列的头部拿出该任务执行。
二、封包后发送
总结下应答数据包的流程:
1 //1. 主消息泵检测到有其他任务需要做,做之。
2 //2. 该任务是从全局的链表中取出应答包数据,找到对应的连接对象,然后尝试直接发出去;
3 //3. 如果发不出,则将该数据存入该连接的发送缓冲区(写缓冲区),并监听该连接的socket可写事件。
4 //4. 下次该socket触发可写事件时,接着发送该连接的写缓冲区中剩余的数据。如此循环直到所有数据都发送成功。
5 //5. 取消监听该socket可写事件,以避免无数据的情况下触发写事件(该事件大多数情况下很频繁)
执行步骤1之前业务逻辑已将应答数据包封好放入全局变量 s_response_pdu_list。
步骤 1 执行:主线程事件循环。s_response_pdu_list 不为空则执行发送应答数据包业务,即回调 proxy_loop_callback 。
步骤 2 执行: void proxy_loop_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam),从全局变量 s_response_pdu_list 取出应答数据包,找出对应的连接 CProxyConn pConn;然后调用 CProxyConn::SendPdu(ResponsePdu_t*) “发出去”。但是,还是不行,因为这还没有解决上文提出的该连接上对端的tcp窗口太小导致数据发不出的问题。所以 pConn->SendPdu() 方法中一定不是调用底层函数 send(2) 直接发送数据。
步骤 3 执行:先试着调用底层send(2)方法去发送应答数据包,能发多少是多少,剩下发不完的,写入该连接的发送缓冲区中,并将忙碌标志 m_busy 置位(设置为ture),并监听可写事件。反之,如果数据一次性发送完成,则调用数据发送完成函数 OnWriteComplete(),这个函数目前为空,即不做任何事情。
步骤 4 执行:当该连接发生可写事件后,判断忙碌标志,若不忙碌则接着发送该连接的写缓冲区中剩余的数据,否则继续监听可写事件。如此循环直到所有数据都发送成功。
步骤 5 执行:整个应答数据包全部发送完后,记得移除该连接的可写事件。
上面的流程从第2步到第5步也是主流网络库的发数据的逻辑。总而言之,就是说,先试着发送数据,如果发不出去,存起来,监听可写事件,下次触发可写事件后接着发。一直到数据全部发出去后,移除监听可写事件。通常只要可写事件是不断会触发的,所以默认不监听可写事件,只有数据发不出的时候才会监听可写事件。这个原则,千万要记住。(redis的网络通信模块也是如此操作)
非阻塞套接字模式下,如果由于对端tcp窗口太小(导致tcp窗口太小的常见原因是:对方无法收包或不及时收包,数据积压在对方网络协议栈里面),不足以将数据发出去,它将立刻返回,不会阻塞执行流,此时返回值为-1,错误码是EAGAIN或EWOULDBLOCK,表示当前数据发不出去,希望你下次再试。但是返回值如果是-1,也可能是真正的出错了,也可能得到错误码EINTR,表示被linux信号中断了,这点需要注意一下。recv函数与send函数情形一样。