redis、memcached、nginx网络组件

本文涉及的产品
云数据库 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
目录
相关文章
|
2月前
|
NoSQL Redis
Redis原理之网络通信协议笔记
1. RESP协议 ​2. 自定义Socket连接Redis
|
2月前
|
NoSQL Linux Redis
Redis原理之网络模型笔记
Redis采用单线程模型,这意味着一个Redis服务器在任何时刻都只会处理一个请求。Redis的网络模型涉及到阻塞I/O(Blocking I/O)、非阻塞I/O(Non-blocking I/O)、I/O多路复用(I/O Multiplexing)、信号驱动I/O(Signal-driven I/O)以及异步I/O(Asynchronous I/O)。
|
2月前
|
缓存 NoSQL 应用服务中间件
2.2.2 redis,memcached,nginx网络组件
2.2.2 redis,memcached,nginx网络组件
|
7天前
|
缓存 NoSQL Redis
如何在Python中使用Redis或Memcached进行缓存?
如何在Python中使用Redis或Memcached进行缓存?
10 2
|
2月前
|
前端开发
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
elementui-upload组件自定义样式上传(upload中常用的属性,但是网络上却找不到教程)(解决bug删除之后再次上传会上传删除的图片)专注后端工程师的前端速成
20 0
|
2月前
|
NoSQL 算法 关系型数据库
redis与mysql的数据一致性问题( 网络分区)
redis与mysql的数据一致性问题( 网络分区)
15 0
|
2月前
|
NoSQL 网络协议 关系型数据库
Redis(二)网络协议和异步方式(乐观锁&悲观锁、事务)
Redis(二)网络协议和异步方式(乐观锁&悲观锁、事务)
15 0
|
2月前
|
存储 NoSQL 关系型数据库
Redis协议与异步方式(redis网络层、pipeline、事务、lua脚本、ACID特性、发布订阅、hiredis实现同步连接与异步连接)
Redis协议与异步方式(redis网络层、pipeline、事务、lua脚本、ACID特性、发布订阅、hiredis实现同步连接与异步连接)
52 0
|
11天前
|
前端开发 应用服务中间件 Linux
nginx解决springcloud前后端跨域问题,同时配置ssl
nginx解决springcloud前后端跨域问题,同时配置ssl
|
4天前
|
PHP
百度虚拟机 bcloud_nginx_user.conf配置
百度虚拟机 bcloud_nginx_user.conf配置
11 0

热门文章

最新文章

相关产品

  • 云迁移中心