redis、memcached、nginx网络组件

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: redis、memcached、nginx网络组件

网络编程关注的问题

连接的建立

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

int clientfd = accept(listenfd, addr, sz);
// 举例为非阻塞io,阻塞io成功直接返回0;
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = connect(connectfd, (struct sockaddr
*)&addr, sizeof(addr));
// ret == -1 && errno == EINPROGRESS 正在建立连接
// ret == -1 && 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);
    // write()
    // close(fd);
}
// 被动:写端关闭
int n = write(fd, buf, sz);
if (n == -1 && errno == EPIPE) {
    close_write(fd);
    // close(fd);
}

消息的到达

从读缓冲区中读取数据

int n = read(fd, buf, sz);
if (n < 0) { // n == -1
    if (errno == EINTR || errno == EWOULDBLOCK)
        break;
    close(fd);
} else if (n == 0) {
    close(fd);
} else {
    // 处理 buf
}

消息发送完毕

往写缓冲区中写数据;

int n = write(fd, buf, dz);
if (n == -1) {
    if (errno == EINTR || errno == EWOULDBLOCK) {
        return;
   }
    close(fd);
}

阻塞io与非阻塞io

阻塞在网络线程;

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

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

// 默认情况下,fd 是阻塞的,设置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);

常见的io函数检测与操作io的方式

connect:

检测功能:检测是否与服务端建立连接
操作功能:与服务端建立连接

accept:

检测功能:检测全连接队列是否为空
操作功能:从全连接队列中取出一条,建立连接(生成fd),并能够获取到客户端的ip地址和端口号

read:

检测功能:检测内核读缓冲区中是否有数据
操作功能:将内核读缓冲区的数据拷贝到用户态来

write:

检测功能:检测内核写缓冲区是否已满
操作功能:将用户态数据拷贝到内核写缓冲区中

注意:

io函数只能检测一条连接以及操作一条连接的数据

操作io方式:

如下描述了io处理函数中非阻塞io方式操作io

说明:

连接断开中,当服务器read0的时候,其实还是可以把数据发送给客户端的,因为read0只能说明读端关闭了,有些服务器的框架需要支持半关闭的情况,shutdown函数是关闭读端或者写端的意思,当关闭读端,那么对端的写端就关闭,close函数是关闭读写端

注意:

read/write函数经常要处理的错误码:

read==-1 && errno == EWOULDBLOCK    //表示内核读缓冲区中没数据
read==-1 && errno == EINTR          //表示读操作被打断,需要重新读
write==-1 && errno == EWOULDBLOCK    //表示内核写缓冲区已满
write==-1 && errno == EINTR          //表示写操作被打断,需要重新写
write==-1 && errno == EPIPE          //表示写端关闭,也即对端的读端关闭了

io多路复用

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

int n = epoll_wait(epfd, evs, sz, timeout);

timeout = -1 一直阻塞直到网络事件到达;

timeout = 0 不管是否有事件就绪立刻返回;

timeout = 1000 最多等待 1 s,如果1 s内没有事件触发则返 回

如何检测io

连接建立:

1、接收连接的时候,io多路复用一般去监听listenfd的读事件,当三次握手完成之后(这是在协议栈完成的),会在全连接队列中新增一条连接,此时协议栈会发送一个信号给到epoll,然后就触发读信号,此时就说明接收连接已经就绪了,接下来我们再调用accept函数,此时的accept函数只会执行操作io(即只是建立连接)的功能,因为检测io是由io多路复用去做的

2、主动连接的时候,即服务器作为客户端的时候,io多路复用一般去监听socketfd的写事件,当三次握手完成了前两次,并且在第三次握手建立的同时,协议栈会发送一个信号给到epoll(因为第三次握手是我给对方发的,所以这是一个写事件),此时会触发这个写事件,也说明连接已经建立成功了

连接断开:

1、被动断开,当epoll检测到socketfd的EPOLLRDHUP,说明客户端的写端关闭,也即服务端的读端关闭;当检测到socketfd的EPOLLHUP,说明客户端的读写端都关闭了

消息到达:

1、消息到达时,io多路复用一般去监听socketfd的读事件,当协议栈的socketfd对应的读缓冲区有数据的时候,协议栈会发送一个信号给到epoll,然后就会触发epoll的读事件,此时我们调用recv函数去操作io即可,因为检测io的工作epoll已经帮我们做好了

消息发送:

1、消息到达时,io多路复用一般去监听socketfd的写事件,当协议栈的socketfd对应的写缓冲区没满的时候(也即可写),协议栈就会发送一个信号给到epoll,从而触发epoll中所管理的对应的fd的可写事件,此时我们调用send函数去操作io即可,因为检测io的工作epoll已经帮我们做好了

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; // epollin epollout
epollel(边缘触发)
    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_create 会创建一个 epoll 对象;调用 epoll_ctl 添加到 epoll 中的事件都会与网卡驱动程序建立回 调关系,相应事件触发时会调用回调函数 (ep_poll_callback),将触发的事件拷贝到 rdlist 双向 链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到 用户态中;

reactor

reactor编程

连接的建立
// 一、处理客户端的连接
// 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_ctl(epfd, EPOLL_CTL_DEL, connectfd,
NULL);
}
连接的断开
if (e->events & EPOLLRDHUP) {
    // 读端关闭
    close_read(fd);
    close(fd);
}
if (e->events & EPOLLHUP) {
    // 读写端都关闭
    close(fd);
}
消息的到达
// reactor 要用非阻塞io  
// select
if (e->events & EPOLLIN) {
    while (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);
        return;
 }
    close(fd);
}
// ...
if (e->events & EPOLLOUT) {
    int n = write(fd, buf, sz);
    //...
    if (n == sz) {
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
   }
}

reactor应用

redis

memcached

redis为什么不使用多线程,而memcached却使用多线程呢?

因为redis中有很多数据结构,对这些数据结构的加锁比较复杂,不好控制粒度

memcached是那种比较简单的数据结构,所以用多线程效率会非常的高

有空去了解一下主线程和子线程之间通过管道通信

nginx

问题总结

1、多线程epoll情况下,epoll_wait或者accept的惊群问题应该怎么解决?

惊群问题其实在内核中已经处理了(通过增加互斥锁的方式),不过内核中只能保证最终只有一个线程能接收到这条连接,其他线程的accept可能返回-1并且errno被置为ewouldblock,所以我们用户态不需要特殊的处理;但在nginx中,它是一种多进程方式使用的epoll,nginx通过在多个进程之间加锁的方式在用户态解决惊群,目的是为了在用户层处理连接的负载均衡。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
NoSQL 网络协议 Linux
Redis的实现一:c、c++的网络通信编程技术,先实现server和client的通信
本文介绍了使用C/C++进行网络通信编程的基础知识,包括创建socket、设置套接字选项、绑定地址、监听连接以及循环接受和处理客户端请求的基本步骤。
46 6
|
18天前
|
缓存 JavaScript
Vue加载网络组件(远程组件)
【10月更文挑战第23天】在 Vue 中实现加载网络组件(远程组件)可以通过多种方式来完成。
|
1月前
|
NoSQL 网络协议 应用服务中间件
redis,memcached,nginx网络组件
redis,memcached,nginx网络组件
17 0
|
1月前
|
存储 监控 NoSQL
Redis的实现二: c、c++的网络通信编程技术,让服务器处理多个client
本文讨论了在C/C++中实现服务器处理多个客户端的技术,重点介绍了事件循环和非阻塞IO的概念,以及如何在Linux上使用epoll来高效地监控和管理多个文件描述符。
27 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
监控 NoSQL 应用服务中间件
elk+redis 搭建nginx日志分析平台
elk+redis 搭建nginx日志分析平台 logstash,elasticsearch,kibana 怎么进行nginx的日志分析呢?首先,架构方面,nginx是有日志文件的,它的每个请求的状态等都有日志文件进行记录。
1533 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
6天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
8天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构