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
目录
相关文章
|
3月前
|
XML 网络协议 物联网
基于surging的木舟IOT平台如何添加网络组件
【8月更文挑战第30天】在基于 Surging 的木舟 IOT 平台中添加网络组件需经历八个步骤:首先理解 Surging 及平台架构;其次明确组件需求,选择合适技术库;接着创建项目并配置;然后设计实现网络功能;再将组件集成至平台;接着进行详尽测试;最后根据反馈持续优化与维护。具体实施时应参照最新文档调整。
70 10
|
1月前
|
缓存 JavaScript
Vue加载网络组件(远程组件)
【10月更文挑战第23天】在 Vue 中实现加载网络组件(远程组件)可以通过多种方式来完成。
|
3月前
|
NoSQL 关系型数据库 Redis
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
mall在linux环境下的部署(基于Docker容器),docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongodb、minio详细教程,拉取镜像、运行容器
mall在linux环境下的部署(基于Docker容器),Docker安装mysql、redis、nginx、rabbitmq、elasticsearch、logstash、kibana、mongo
|
2月前
|
NoSQL 网络协议 应用服务中间件
redis,memcached,nginx网络组件
redis,memcached,nginx网络组件
22 0
|
4月前
|
存储 NoSQL Redis
pyhon之对memcached及redis操作
pyhon之对memcached及redis操作
|
4月前
|
C++
C++ Qt开发:QUdpSocket网络通信组件
QUdpSocket是Qt网络编程中一个非常有用的组件,它提供了在UDP协议下进行数据发送和接收的能力。通过简单的方法和信号,可以轻松实现基于UDP的网络通信。不过,需要注意的是,UDP协议本身不保证数据的可靠传输,因此在使用QUdpSocket时,可能需要在应用层实现一些机制来保证数据的完整性和顺序,或者选择在适用的场景下使用UDP协议。
212 2
|
1月前
|
缓存 应用服务中间件 网络安全
Nginx中配置HTTP2协议的方法
Nginx中配置HTTP2协议的方法
104 7
|
2月前
|
应用服务中间件 BI nginx
Nginx的location配置详解
【10月更文挑战第16天】Nginx的location配置详解
|
10天前
|
存储 应用服务中间件 nginx
nginx反向代理bucket目录配置
该配置实现通过Nginx代理访问阿里云OSS存储桶中的图片资源。当用户访问代理域名下的图片URL(如 `http://代理域名/123.png`)时,Nginx会将请求转发到指定的OSS存储桶地址,并重写路径为 `/prod/files/2024/12/12/123.png`。
43 5