redis,memcached,nginx网络组件

目录

1 梳理reactor网络编程

2 编程细节 (返回值以及错误码)

连接的建立

连接的断开

消息的到达

 消息发送完毕

网络 IO 职责

epoll编程

reactor的应用

3 redis,ngnix,memcached reactor具体使用


1 梳理reactor网络编程

2 编程细节 (返回值以及错误码)

连接的建立

分为两种:服务端处理接收客户端的连接,服务端作为客户端连接第三方服务;

int clientfd = accept(listenfd, addr, sz);
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
errno = EINPOGRESS 正在建立连接
errno = EISCONN 连接建立成功

clientfd == -1 && errno == EWOULDBLOCK(全链接队列没有数据)

在linux中 listen(fd,listenblock);中的listenblock设置的是全链接队列的大小

mac中listenblock设置的是全链接队列和半链接队列的和

int ret = connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
errno = EINPOGRESS 正在建立连接
errno = EISCONN 连接建立成功

连接的断开

分为两种:主动断开和被动断开;

// 主动关闭
close(fd);
shutdown(fd, SHUT_RDWR);
// 主动关闭本地读端,对端写段关闭
shutdown(fd, SHUT_RD);
// 主动关闭本地写端,对端读段关闭
shutdown(fd, SHUT_WR);
// 被动:读端关闭
int n = read(fd, buf, sz);
if (n == 0) {
close_read(fd);
// close(fd);
}
// 被动:写端关闭
//有的网络编程需要支持半关闭状态
int n = write(fd, buf, sz);
if (n == -1 && errno == EPIPE) {
close_write(fd);
// close(fd);
}

主动:close shutdown
被动:
关闭读端 read = 0
关闭写端 write = -1 && errno = EPIPE(写端关闭了)

消息的到达

从读缓冲区中读取数据;

int n = read(fd, buf, sz);//从读缓冲区中的数据从内核态读取到用户态中来
if (n < 0) { // n == -1 EINTR是被信号打断了  EINTR和EWOULDBLOCK都是正向积极的错误 
if (errno == EINTR || errno == EWOULDBLOCK)
break;
close(fd);
} else if (n == 0) {
close(fd);
} else {
// 处理 buf
}

 消息发送完毕

往写缓冲区中写数据;

int n = write(fd, buf, dz);   //n == -1 或者 n==dz
if (n == -1) {//EWOULDBLOCK 写缓冲区满了  EINTR :信号打断   两者都是正向积极的
if (errno == EINTR || errno == EWOULDBLOCK) {
return;
}
close(fd);
}

网络 IO 职责

检测 IO

io 函数本身可以检测 io 的状态;但是只能检测一个 fd 对应的状态;io 多路复用可以同时检测多 个io的状态;区别是:io函数可以检测具体状态;io 多路复用只能检测出可读、可写、错误、断开 等笼统的事件;

操作 IO

只能使用 io 函数来进行操作;分为两种操作方式:阻塞 io 和非阻塞 io;

阻塞 IO 和 非阻塞 IO

阻塞在网络线程;

连接的 fd 阻塞属性决定了 io 函数是否阻塞;

具体差异在:io 函数在数据未到达时是否立刻返回;

// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;

//拿到fd的属性
int flag = fcntl(fd, F_GETFL, 0);
//设置fd的属性
fcntl(fd, F_SETFL, flag | O_NONBLOCK)

图形描述

redis,memcached,nginx网络组件_第1张图片

 IO多路复用

redis,memcached,nginx网络组件_第2张图片

IO多路复用只负责检测IO,不负责操作IO

epoll_wait(epfd,evs,sz,timeout);

timeout ==  -1 IO多路复用是永久阻塞的

timeout = 0 非阻塞 立刻返回

timeout == 一个具体的值  IO多路复用就是阻塞的

 epoll的部分源码

struct eventpoll {
// ...
struct rb_root rbr; // 管理 epoll 监听的事件  //红黑树
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件 //双向链表
// ...
};
struct epitem {
// ...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
// ...
};
struct epoll_event {  //重点关注************
__uint32_t events;
epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
int epoll_create(int size);
/**
op:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
event.events:
EPOLLIN 注册读事件
EPOLLOUT 注册写事件
EPOLLET 注册边缘触发模式,默认是水平触发
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/**
events[i].events:
EPOLLIN 触发读事件
EPOLLOUT 触发写事件
EPOLLERR 连接发生错误
EPOLLRDHUP 连接读端关闭
EPOLLHUP 连接双端关闭
*/
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int
timeout);

 epoll原理图

redis,memcached,nginx网络组件_第3张图片

 调用 epoll_create 会创建一个 epoll 对象;调用 epoll_ctl 添加到 epoll 中的事件都会与网 卡驱动程序建立回调关系,相应事件触发时会调用回调函数 ( ep_poll_callback ),将触发的 事件拷贝到 rdlist 双向链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中

epoll编程

 连接的建立

// 一、处理客户端的连接
// 1. 注册监听 listenfd 的读事件
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev);
// 2. 当触发 listenfd 的读事件,调用 accept 接收新的连接
int clientfd = accept(listenfd, addr, sz);
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev);
// 二、处理连接第三方服务
// 1. 创建 socket 建立连接
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
connect(connectfd, (struct sockaddr *)&addr, sizeof(addr));
// 2. 注册监听 connectfd 的写事件
struct epoll_event ev;
ev.events |= EPOLLOUT;
epoll_ctl(efd, EPOLL_CTL_ADD, connectfd, &ev);
// 3. 当 connectfd 写事件被触发,连接建立成功
if (status == e_connecting && e->events & EPOLLOUT) {
status == e_connected;
}

连接的断开 

  

epoll_wait();
if (e->events & EPOLLRDHUP) {
// 读端关闭
close_read(fd);
// close(fd);
}
if (e->events & EPOLLHUP) {
// 读写端都关闭
close(fd);
}

 数据到达

//rector 为什么要用非阻塞IO
//数据到达如果经过一个错误的校验,epoll发送一个数据可读的信号,
但实际上读缓冲区是没有数据可以读的,那么如果用的是阻塞IO,
则会一直阻塞在IO的操作函数上面,
会对业务的处理或者说的代码的正常执行上面造成效率降低的影响,
如果用的是非阻塞的IO 那么就不会存在这样的问题。

epoll_wait();
if (e->events & EPOLLIN) {
wjile(1)
int n = read(fd, buf, sz);
if (n < 0) {
if (errno == EINTR)
        continue;
if (errno == EWOULDBLOCK)
        break;
close(fd);
} else if (n == 0) {
  close_read(fd);
  // close(fd);
    }
   //具体的业务逻辑
}    

数据发送完毕 

int n = write(fd, buf, dz);
if (n == -1) {
if (errno == EINTR)
continue;
if (errno == EWOULDBLOCK) {
struct epoll_event ev;
ev.events = EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);  //注册写事件 然后写成功后,就可以把这个写事件给删除
}
close(fd);
}
// ...
if (e->events & EPOLLOUT) {
int n = write(fd, buf, sz);
//...
if (n > 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
}

reactor的应用

组成:IO多路复用+非阻塞IO

事件方式通知,事件循环

将对IO的处理转换成对事件的处理

while(1) {
    int n = epoll_wait();
    for (int i = 0;i < n;i++) {
       

    }




}

3 redis,ngnix,memcached reactor具体使用

单reactor(redis)

redis,memcached,nginx网络组件_第4张图片

 

redis 网络的封装

 redis,memcached,nginx网络组件_第5张图片

 redis,memcached,nginx网络组件_第6张图片

 redis优化方法:

        将 int n = read(fd,buf,sz)

            cmd args = decode(buf,sz)

                加入队列当中通过负载均衡的方式分派给IO多线程去执行。

        out  = logic(cmd,args);再回归到主线程执行。

得出的out结果加入队列再次交给IO多线程去处理encode   write

        

 注意

read 只能用单线程去读,而不能用多线程去读,否则会出现读取的数据出现错乱的情况。

write 可以多线程 也可以单线程的去写。

skynet的优化方法:

redis,memcached,nginx网络组件_第7张图片

 将后面除了读以外的四个步骤加入到多线程上面去执行

memcached

redis,memcached,nginx网络组件_第8张图片

 

 CPU的核数有多少个就分配多少哥reactor,可以充分的利用reactor

(nginx)

redis,memcached,nginx网络组件_第9张图片

 nginx 的worker的共享内存当中有一把锁,当有连接进来时候,哪个worker拿到锁,就accept 接收这一条连接,加入epoll红黑树当中

redis memcached采用的是水平触发,nginx采用的是边沿触发

nginx 为什么采用边沿触发?

nginx用来做反向代理,主要是用来转发的,nginx收到数据后就直接往后台转发,直接用边沿触发用while把数据一次性读出来,这样的效率是最高的。

而redis memcached 用水平触发的原因是用户层不知道读缓冲区有多少数据,需要界定数据包,首先读一个数据包然后做相应的业务逻辑。所以用水平触发。

结论:一般而言,需要做业务处理 和数据量小的时候用水平触发。 对数据包不用太在意时就用边沿触发。

worker进程都是监听的同一个端口,master进程执行的listen,然后再开始fork出子进程。

然后一条连接进来后,今后造成所有的work会产生惊群,nginx处理惊群的方法是,worker进程间的共享内存当中会有一把锁,当哪个worker拿到锁之后就执行accept建立好连接,然后注册读事件

你可能感兴趣的:(C/C++服务器开发,nginx,memcached,redis)