redis、memcached、Nginx组件中的TCP

Redis、memcached、Nginx组件

  • 一、网络编程需要关注的问题
  • 二、网络io职责
    • 1、操作io
    • 2、检测io
    • 3、epoll结构
  • 三、reactor原理
  • 四、Redis、memcached、Nginx 组件介绍
    • 1、Redis
    • 2、memcached
    • 3、Nginx
  • 五、总结

一、网络编程需要关注的问题

这里主要以TCP连接实现server为例,主要介绍网络编程中需要关注的问题,以及在每个状态下,io函数的作用。下面也主要从以下四个问题讲述

1、连接建立
2、连接断开
3、消息到达
4、消息发送

二、网络io职责

1、操作io

操作方式:
我们知道,文件描述符是网络编程的基础,对io的操作都是通过文件描述符fd实现的,所以在开始之前我们需要好好了解一下fd的属性,接下来我们看看下面这张图
redis、memcached、Nginx组件中的TCP_第1张图片
请注意途中红圈圈出来的部分,当阻塞io调用read/recv函数时,此时如果内核缓冲区的数据还没有准备好,也就是协议栈还未收到数据或者是协议栈收到数据,但数据还没copy到内核缓冲区中,那么此时就会阻塞等待,知道内核中的read buffer中有数据才会返回,返回值代表的是实际返回的字节数,而不是我们在调用函数时传入的参数。

非阻塞io中调用read/recv函数时,如果内核read buffer没有数据,此时函数会直接返回并设置errno,errno为EWOULDBLOCK,如果有数据就是返回实际读取到的字节数,所以我们可以看到,阻塞io和非阻塞io的区别就在于数据的准备阶段。

那非阻塞io是如何处理连接建立、断开、消息送达和消息发送四个问题的呢?

建立连接:
连接建立分为主动连接,就是server作为客户端调用connect函数去连接数据库获取数据;被动连接就是被动接受处理客户端的链接请求。首先是主动连接,当server调用connect函数之后,此时就会开始TCP三次握手,因为设置了fd是非阻塞,此时connect返回值是-1。
errno是
EINPROGRESS,表示套接字为非阻塞套接字,且连接请求没有立即完成。此时TCP正在进行连接,我们需要通过epoll去注册读事件就可以判断连接是否可以建立成功了,建立成功后,connect返回-1,errno为EISCONN表示已经连接套接字了。
所以通过这里我们可以看出我们可以通过判断设置的errno来判断io是否建立连接。

被动连接:
被动连接发生之前,server需要进行socket、bind、listen操作,并将listenfd放到epoll树上注册读事件,此时就等待客户端连接了。当连接来临时,server端需要等待三次握手结束后accept返回值才不为-1,此时accept返回的值就是客户端锁持有的fd。

连接断开

主动断开:
当server端调用shutdown函数时会关闭读端或写端,那对应就会关闭客户端的写端或读端。比如server端shutdown掉读端,那客户端的写端就会被关闭,也就是客户端无法将数据发送到server端了,但是服务端依旧可以将数据发送给客户端,因为TCP通信是全双工的,关闭了读端是不会影响到另一端的。还有就是调用close,此时是全部关闭,既不能读也不能写。

被动断开:
什么时候是被动断开呢?就是当server段调用read/recv函数时,返回值是0,此时表明客户端的写端,服务端的读端关闭了。此时是可以继续调用write将未发送完的数据继续发送出去,然后根据返回值,和设置的errno判断,如果是EPIPE,这说明断开了连接。
redis、memcached、Nginx组件中的TCP_第2张图片

图片中红线所在的地方是调用的read返回0,但是在下一次发送FIN,也就是调用close之前,这段时间是科技继续发送数据的。但如果调用write返回-1,并且errno为EPIPE表示服务端写端关闭。

redis、memcached、Nginx组件中的TCP_第3张图片

消息到达:

消息到达表明我们需要调用read从内核的read buffer中读取数据,那么此时如果read返回-1
且errno设置为EWOULDBLOCK 表示此时read buffer中没有数据,调用失败,如果errno设置为EINTR表示read调用被其他中断打断,调用失败。那如果返回值大于0,就表示read成功。

消息发送:

消息发送就是server端调用write函数,此时根据设置的errno判断,如果是EWOULDBLOCK表示表示客户端的read buffer无法读,原因就是客户端调用shutdown关闭了读端,如果返回EINTR表示write调用失败,被其他中断打断,如果返回大于0,表示写成功,返回值是指真正写入缓缓从去的字节数,而不是调用write时传入的参数

2、检测io

io多路复用(epoll)是如何做到io检测的呢?
首先我们要明白,io多路复用知识把io检测出来而不去操作io,也就是io检测和操作io是分开的。首先需要socket、bind、listen,然后epoll_create、epoll_wait函数,然后遍历epoll_wait的返回值,这时大致的流程,我们来看看这个过程具体发生了什么。

当程序执行到epoll_wait这个函数时,会根据timeout参数决定是否阻塞、阻塞多长时间,如果此时客户端调用connect函数发起三次握手,在三次握手结束之后,这个节点会被放进accept队列,此时通过注册的写事件就可以检测io了,那为什么需要注册写事件而不是读事件呢?这时因为,当客户端主动连接时,此时服务器关注最后一次握手也就是客户端最后一次发送的ack包,此时会同时发送一个信号触发epoll树注册的写事件,这时候就说明该连接成功建立了。此时调用accept从accept队列取出节点进行处理,最后再将节点添加到epoll树上(调用epoll_ctl函数)。此时在下一次返回时就可以根据具体的读写事件进行io到的操作了。这就是当服务端作为客户端主动连接时发生的情况。那如果是服务端被动连接呢,此时我们主要注册监听的就是读事件了,因为,此时服务器是需要拿到第三次握手的数据的,也就是需要客户端主动将数据发送到服务器的read buffer,此时我们只要检测到读事件就表示连接建立成功了,这就是被动建立。

被动断开:

我们根据epoll返回的errno, 如果errno是EPOLLRDHUP,说明服务端的读端已经关闭了,如果是EPOLLHUP说明读写段全部关闭,这样我们就可以通过判断epoll的返回值和errno来检测io了

消息到达:

消息到达需要服务端通过epoll监听客户端fd的读事件,如果read buffer有数据,此时就会发送信号给epoll触发EPOLLIN事件,接下来就可以调用read函数,此时read函数执行的就是操作io的功能了。

消息发送:

消息发送我们通常检测的是fd的写事件,因为如果write buffer有空间,epoll就会检测出来然后出发写事件,此时调用write就会将数据写write buffer。

3、epoll结构

epoll_create 函数首先会建立一个红黑树用来存储监听的fd节点事件,一个双端队列用来存储发生事件的fd节点
redis、memcached、Nginx组件中的TCP_第4张图片

epoll_ctl函数主要工作就是将注册好事件的fd挂载到epoll_create创建的epoll树上去
redis、memcached、Nginx组件中的TCP_第5张图片
epoll_wait函数主要是监听fd 是否发生变化,并将发生变化的fd放到双端队列中去
redis、memcached、Nginx组件中的TCP_第6张图片
调用epoll_create会创建一个epoll对象;调用epoll_ctll添加到epoll中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数( ep_epoll_callback),将触发的事件拷贝到rdlist双向链表中;调用epoll_wait将会把rdlist中就绪事件拷贝到用户态中;

三、reactor原理

reactor 是通过io多路复用和非阻塞io搭配实现的,所有的io都交由多路复用检测,然后调用具体的io函数去处理fd。reactorhi异步事件的。

那rector为什么使用非阻塞io呢,这里需要放到具体的环境中去解释。
多线程环境:
多线程环境下,我们可以将一个listenfd放到多个epoll中去检测,这是支持的。然后连接请求到来并放进accept队里中时,会发送信号给所有的epoll,这种现象叫做epoll惊群。这是假如,第一个epoll调用accept从队列中取出fd进行连接建立处理并成功执行,此时accept队列是没有fd需要处理了,那如果此时是阻塞io,其它的epoll就会阻塞在accept函数这里等待执行。如果是非阻塞io,accept会直接返回-1 并设置errno,这就避免了阻塞情况。

边缘触发下:
边缘触发下,一次读事件的触发会read一次,这里一般都会一次把read buffer读空,因为,如果读不完,剩下的数据就需要在下一次触发读事件才能继续读。如果io是阻塞的,读完之后read buffer内没有数据,此时调用read会阻塞直到read buffer有数据,如果是非阻塞io,这里就会直接返回,不会阻塞。

io多路复用选用select时:
当某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有被设置nonblocking,此read将阻塞当前线程。

优点:
1、rector将io检测和处理解耦出来,分离了io的职责。
2、epoll的惊群会在协议栈通过加锁处理,不需要关注。
3、reactor是一个线程执行一次事件循环。

四、Redis、memcached、Nginx 组件介绍

1、Redis

环境:redis是一种key-value的内置内存型数据库,value支持多种类型;然后redis的命令处理是单线程的。

redis为什么使用单线程实现?
首先就是value支持多种数据结构,多种数据结构涉及到的加锁问题尤其麻烦,而且,redis的命令处理是单线程的,更没必要使用多线程。但在6.0版本之后也支持了多线程,具体实现下面会说。

redis如何处理reactor?
redis、memcached、Nginx组件中的TCP_第7张图片
redis 是主线程持有一个reactor,然后reactor的epoll去检测io,当io触发时会根据事件类型去执行对应的数据解码,编码的工作,这里是将read操作和数据的decode、write和encode放到多线程环境处理,compute命令操作还是由主线程处理的。这也是redis针对redis做的优化。

2、memcached

坏境:
memcached是一种key-value的内存型数据库,但是value只支持单一数据结构,命令处理是多行的。

memcached为什么实用多线程处理reactor?
因为value仅支持单一数据结构,加锁简单直接,而且命令处理也是多行的,多线程也更有利于提高业务的并发程度

memcached是如何处理reactor的?
redis、memcached、Nginx组件中的TCP_第8张图片主线程的reactor负责接收连接,然后通过管道PIPE和其他线程通信,负载均衡的将具体的clientfd交由不同的线程的reactor处理,在每一个线程里去处理具体的业务逻辑、命令。这里是没有将数据的读取,decode、写和encode交由多线程处理的。

3、Nginx

redis、memcached、Nginx组件中的TCP_第9张图片
Nginx使用的是多进程版本的reactor去检测处理io,socket、bind、listen之后就会执行fork产生子进程,这里的惊群问题是在用户层通过加锁进行处理的,这里是存在一个共享资源的,我们将锁加在这里,然后进程去争夺这把锁,谁争夺到了就去处理listenfd。

五、总结

1、网络编程需要关注建立、断开、消息到达和发送四个问题。
2、主动连接、被动连接、主动断开、被动断开时,相应的函数具体做了什么。
3、io函数是具有检测和操作两种功能的。
4、reactor的实现由单线程、多线程、多进程版本的

你可能感兴趣的:(C++学习,redis,memcached,nginx)