引言 - 整体认识

  redis ae 事件驱动模型, 网上聊得很多. 但当你仔细看完一篇又一篇之后, 可能你看的很舒服, 但对于

作者为什么要这么写, 出发点, 好处, 缺点 ... 可能还是好模糊, 不是吗?

我们这里基于阅读的人已经了解了 IO 复用大致流程且抄写过 ae 的全部代码. 好, 那开始吧, 希望后面的

点拨, 给同学们醍醐灌顶一下.

  先看看 ae.h 设计

/* A simple event-driven programming library. Originally I wrote this code
* for the Jim's event-loop (Jim is a Tcl interpreter) but later translated
* it in form of a library for easy reuse.
*
* Copyright (c) 2006-2012, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/ #ifndef __AE_H__
#define __AE_H__ #include <time.h> #define AE_OK 0
#define AE_ERR -1 #define AE_NONE 0 /* No events registered. */
#define AE_READABLE 1 /* Fire when descriptor is readable. */
#define AE_WRITABLE 2 /* Fire when descriptor is writable. */
#define AE_BARRIER 4 /* With WRITABLE, never fire the event if the
READABLE event already fired in the same event
loop iteration. Useful when you want to persist
things to disk before sending replies, and want
to do that in a group fashion. */ #define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4
#define AE_CALL_AFTER_SLEEP 8 #define AE_NOMORE -1
#define AE_DELETED_EVENT_ID -1 /* Macros */
#define AE_NOTUSED(V) ((void) V) struct aeEventLoop; /* Types and data structures */
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop); /* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent; /* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent; /* A fired event */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent; /* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
int flags;
} aeEventLoop; /* Prototypes */
aeEventLoop *aeCreateEventLoop(int setsize);
void aeDeleteEventLoop(aeEventLoop *eventLoop);
void aeStop(aeEventLoop *eventLoop);
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc);
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
int aeWait(int fd, int mask, long long milliseconds);
void aeMain(aeEventLoop *eventLoop);
char *aeGetApiName(void);
void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep);
void aeSetAfterSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *aftersleep);
int aeGetSetSize(aeEventLoop *eventLoop);
int aeResizeSetSize(aeEventLoop *eventLoop, int setsize);
void aeSetDontWait(aeEventLoop *eventLoop, int noWait); #endif

很多朋友首次看, 或者第一次手写完毕 ae.h 结构设计文件, 印象里 60% 是模糊不可描述 ~ 也许大致知

道这宏有点感觉应该是和 IO Event 事件有关吧 ...

我这里先稍微要剧透点, 带大家快速了解这个库的结构设计的意图. C 先看结构, 比先看接口设计更容

易获取到核心信息. 上面代码中最重要四个结构分别是

  aeFileEvent, aeTimeEvent, aeFiredEvent, aeEventLoop

aeFileEvent 是文件描述符 Event, 注册在 aeEventLoop 中, 当触发后会生成事件结构 aeFiredEvent,

用于后续处理.  aeTimeEvent 是 timer Event 同样注册在  aeEventLoop 中用于触发定时事件. (太懒,

懒画图, 有兴趣朋友可以自行理解画出好理解的图) 对于  aeEventLoop 内部字段的设计,  先不剧透了.

后面正文部分会讨论一些.

前言 - 底层解密

  ae 文件整体结构如下

很清晰的看出 epoll, evport, kqueue, select IO 复用的核心包装. 但写完整个 ae.c 发现对其设计影响

最深可能就是 ae_select.c 中兼容 select 思路.

/* Select()-based ae.c module.
*
* Copyright (c) 2009-2012, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/ #include <sys/select.h>
#include <string.h> typedef struct aeApiState {
fd_set rfds, wfds;
/* We need to have a copy of the fd sets as it's not safe to reuse
* FD sets after select(). */
fd_set _rfds, _wfds;
} aeApiState; static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState)); if (!state) return -;
FD_ZERO(&state->rfds);
FD_ZERO(&state->wfds);
eventLoop->apidata = state;
return ;
} static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
/* Just ensure we have enough room in the fd_set type. */
if (setsize >= FD_SETSIZE) return -;
return ;
} static void aeApiFree(aeEventLoop *eventLoop) {
zfree(eventLoop->apidata);
} static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata; if (mask & AE_READABLE) FD_SET(fd,&state->rfds);
if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds);
return ;
} static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata; if (mask & AE_READABLE) FD_CLR(fd,&state->rfds);
if (mask & AE_WRITABLE) FD_CLR(fd,&state->wfds);
} static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, j, numevents = ; memcpy(&state->_rfds,&state->rfds,sizeof(fd_set));
memcpy(&state->_wfds,&state->wfds,sizeof(fd_set)); retval = select(eventLoop->maxfd+,
&state->_rfds,&state->_wfds,NULL,tvp);
if (retval > ) {
for (j = ; j <= eventLoop->maxfd; j++) {
int mask = ;
aeFileEvent *fe = &eventLoop->events[j]; if (fe->mask == AE_NONE) continue;
if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds))
mask |= AE_READABLE;
if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds))
mask |= AE_WRITABLE;
eventLoop->fired[numevents].fd = j;
eventLoop->fired[numevents].mask = mask;
numevents++;
}
}
return numevents;
} static char *aeApiName(void) {
return "select";
}

作者实现这个 select 思路不是很好, 他把 ae_select.c 当做局部文件去设计, 没有想拆出来独挡一面.

其次对于 select 的第四个参数 error fds 集合没有处理(ae_epoll.c 中 EPOLLHUB 和 EPOLLERR 是

处理). 实现层面 aeApiPoll 也不够好, 推荐采用下面实现

#include "ae.h"
#include <string.h>
#include <sys/select.h> static int aeApiPoll(aeEventLoop * eventLoop, struct timeval * tvp) {
aeApiState * state = eventLoop->apidata;
int retval, j, numevents = ; memcpy(&state->_rfds, &state->rfds, sizeof(fd_set));
memcpy(&state->_wfds, &state->wfds, sizeof(fd_set)); retval = select(eventLoop->maxfd+, &state->_rfds, &state->_wfds, NULL, tvp);
for (j = ; j <= eventLoop->maxfd && numevents < retval; j++) {
int mask = AE_NONE;
aeFileEvent * fe = &eventLoop->events[j]; if (fe->mask == AE_NONE) continue;
if (fe->mask & AE_READABLE && FD_ISSET(j, &state->_rfds))
mask |= AE_READABLE;
if (fe->mask & AE_WRITABLE && FD_ISSET(j, &state->_wfds))
mask |= AE_WRITABLE;
if (mask == AE_NONE) continue; eventLoop->fired[numevents].fd = j;
eventLoop->fired[numevents].mask = mask;
numevents++;
} return numevents;
}

降低不需要处理的 AE_NONE 空事件.  随后的 epoll kqueue 都差不多(evport 不熟, 有心的朋友也别看)

正文 - 细节点拨

  整体看 ae 事件模型设计, 还是有些简陋的. 我猜测是 redis 重IO和内存操作, 对很多文件描述符需求

较固定,  一个文件描述符多数自始至终. 应对的场景不是那种大量的创建, 交互, 关闭. 所以整体设计也能

接受.

1.  setsize maxfd event fired 到底想表达什么?

#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <poll.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h> #include "ae.h"
#include "config.h"
#include "zmalloc.h" /* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_epoll.c"
#else
#include "ae_select.c"
#endif
#endif
#endif aeEventLoop * aeCreateEventLoop(int setsize) {
aeEventLoop * eventLoop;
int i; if (!(eventLoop = zmalloc(sizeof(*eventLoop)))) goto err; eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (!eventLoop->events || !eventLoop->fired) goto err; eventLoop->setsize = setsize;
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = ;
eventLoop->stop = ;
eventLoop->maxfd = -;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
eventLoop->flags = ;
if (aeApiCreate(eventLoop) == -) goto err;
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
for (i = ; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop; err:
if (eventLoop) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
}
return NULL;
}

有心的同学可以关注  eventLoop->events 和 eventLoop->fired zmalloc 这块, 这基本已经

把之前的 ae_select.c ae_epoll.c ae_kqueue.c ... 串起来了. 分别用于存要监控的事件和有变动的事件.

对于 setsize 也是个看点我们分别看 server.c server.h config.c 局部代码

[server.c]
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR); [server.h]
#define CONFIG_MIN_RESERVED_FDS 32 /* When configuring the server eventloop, we setup it so that the total number
* of file descriptors we can handle are server.maxclients + RESERVED_FDS +
* a few more to stay safe. Since RESERVED_FDS defaults to 32, we add 96
* in order to make sure of not over provisioning more than 128 fds. */
#define CONFIG_FDSET_INCR (CONFIG_MIN_RESERVED_FDS+96) [config.c]
/* Unsigned int configs */
createUIntConfig("maxclients", NULL, MODIFIABLE_CONFIG, , UINT_MAX, server.maxclients, , INTEGER_CONFIG, NULL, updateMaxclients),

可以看出来 setsize 分为两部分, 一分部分是配置的, 默认是 10000; 另外一部分是预留 128个.

(128 分为两部分 CONFIG_MIN_RESERVED_FDS = 32 + 96, 前者是 redis fd 保留的最少个数)

和上面 aeCreateEventLoop 相似的功能有 aeResizeSetSize

/* Resize the maximum set size of the event loop.
* If the requested set size is smaller than the current set size, but
* there is already a file descriptor in use that is >= the requested
* set size minus one, AE_ERR is returned and the operation is not
* performed at all.
*
* Otherwise AE_OK is returned and the operation is successful. */
int aeResizeSetSize(aeEventLoop *eventLoop, int setsize) {
int i; if (setsize == eventLoop->setsize) return AE_OK;
if (eventLoop->maxfd >= setsize) return AE_ERR;
if (aeApiResize(eventLoop,setsize) == -) return AE_ERR; eventLoop->events = zrealloc(eventLoop->events,sizeof(aeFileEvent)*setsize);
eventLoop->fired = zrealloc(eventLoop->fired,sizeof(aeFiredEvent)*setsize);
eventLoop->setsize = setsize; /* Make sure that if we created new slots, they are initialized with
* an AE_NONE mask. */
for (i = eventLoop->maxfd+; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return AE_OK;
}

透过这两个函数希望你对 aeEventLoop 中 setsize maxfd event fired 这些字段能了解透彻.

2. aeTimeEvent 怎么用, 怎么设计的?

redis 中 timer Event 设计比较简单, 单纯的无序时间链表. 下面这段作者意图表达的很清晰.

/* Search the first timer to fire.
* This operation is useful to know how many time the select can be
* put in sleep without to delay any event.
* If there are no timers NULL is returned.
*
* Note that's O(N) since time events are unsorted.
* Possible optimizations (not needed by Redis so far, but...):
* 1) Insert the event in order, so that the nearest is just the head.
* Much better but still insertion or deletion of timers is O(N).
* 2) Use a skiplist to have this operation as O(1) and insertion as O(log(N)).
*/
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
aeTimeEvent *te = eventLoop->timeEventHead;
aeTimeEvent *nearest = NULL; while(te) {
if (!nearest || te->when_sec < nearest->when_sec ||
(te->when_sec == nearest->when_sec &&
te->when_ms < nearest->when_ms))
nearest = te;
te = te->next;
}
return nearest;
}

而其中到底怎么跑起来的呢, 我截取 processTimeEvents 中部分代码, 帮读者了然于心

/* Process time events */
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = ;
aeTimeEvent *te;
long long maxId;
time_t now = time(NULL);
...
{
...
aeGetTime(&now_sec, &now_ms);
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms))
{
int retval; id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
te->id = AE_DELETED_EVENT_ID;
}
}
...
}
...
return processed;
}

从 retval = te->timeProc -> if 那段. 对于 id 打标为  AE_DELETED_EVENT_ID 标识轮循到的时候要删除.

一旦 retval != AE_NOMORE 就再次修改这个timer Event 相关时间, 方便下次接着跑. 同样我们抽一个例

子出来, 同样核心也在 server.c 中

[server.c]
/* Create the timer callback, this is our way to process many background
* operations incrementally, like clients timeout, eviction of unaccessed
* expired keys and so forth. */
if (aeCreateTimeEvent(server.el, , serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit();
} [server.c]
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
return /server.hz;
}

整体看他这个 timer Event 很骚, 返回毫秒时间后, 继续注入进去, 继续当循环轮循定时器事件使用

static void aeAddMillisecondsToNow(long long milliseconds, long *sec, long *ms) {
long cur_sec, cur_ms, when_sec, when_ms; aeGetTime(&cur_sec, &cur_ms);
when_sec = cur_sec + milliseconds/;
when_ms = cur_ms + milliseconds%;
if (when_ms >= ) {
when_sec ++;
when_ms -= ;
}
*sec = when_sec;
*ms = when_ms;
}

设计的思路挺巧妙的. 多数正常思路通过类型特殊处理, 或者特殊地方再次主动注册.

3. EventLoop 是怎么跑的?

EventLoop 奔跑思路很简单, 一个地方轮循, 内部先跑 aeFileEvent, 再跑 aeTimeEvent

[ae.c]
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = ;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
} [server.c]
int main(int argc, char **argv) {
... aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
return ;
} /* The End */

整体而言 redis ae 模型还是非常简单, 处理的这些的事情完全是为 redis io 定制的. 够用了.

后续有机会我再大家分析 redis 中特定的 socket io 是怎么处理的.

后记 - 为爱展望

❤ 错误是难免的, 欢迎有心同学指正和补充图, 文字是干瘪的 ~

 Here We Are Again - https://music.163.com/#/song?id=27876900

C基础 带你手写 redis ae 事件驱动模型的更多相关文章

  1. C基础 带你手写 redis adlist 双向链表

    引言 - 导航栏目 有些朋友可能对 redis 充满着数不尽的求知欲, 也许是 redis 属于工作, 交流(面试)的大头戏, 不得不 ... 而自己当下对于 redis 只是停留在会用层面, 细节层 ...

  2. C基础 带你手写 redis sds

    前言 - Simple Dynamic Strings  antirez 想统一 Redis,Disque,Hiredis 项目中 SDS 代码, 因此构建了这个项目 https://github.c ...

  3. 关于布隆过滤器,手写你真的知其原理吗?让我来带你手写redis布隆过滤器。

    说到布隆过滤器不得不提到,redis, redis作为现在主流的nosql数据库,备受瞩目:它的丰富的value类型,以及它的偏向计算向数据移动属性减少IO的成本问题.备受开发人员的青睐.通常我们使用 ...

  4. 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍

    概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...

  5. Tensorflow编程基础之Mnist手写识别实验+关于cross_entropy的理解

    好久没有静下心来写点东西了,最近好像又回到了高中时候的状态,休息不好,无法全心学习,恶性循环,现在终于调整的好一点了,听着纯音乐突然非常伤感,那些曾经快乐的大学时光啊,突然又慢慢的一下子出现在了眼前, ...

  6. 带小伙伴手写 golang context

    前言 - context 源码 可以先了解官方 context.go 轮廓. 这里捎带保存一份当前 context 版本备份. // Copyright 2014 The Go Authors. Al ...

  7. 用C、python手写redis客户端,兼容redis集群 (-MOVED和-ASK),快速搭建redis集群

    想没想过,自己写一个redis客户端,是不是很难呢? 其实,并不是特别难. 首先,要知道redis服务端用的通信协议,建议直接去官网看,博客啥的其实也是从官网摘抄的,或者从其他博客抄的(忽略). 协议 ...

  8. caffe_手写数字识别Lenet模型理解

    这两天看了Lenet的模型理解,很简单的手写数字CNN网络,90年代美国用它来识别钞票,准确率还是很高的,所以它也是一个很经典的模型.而且学习这个模型也有助于我们理解更大的网络比如Imagenet等等 ...

  9. 手写redis的docker文件,通过docker-compose配置redis

    在前面一遍随笔,配置的是mysql主从的docker-compose配置.今天我们来学习配置编排容器redis. 准备环境: docker 18.06.1-ce docker-compose 1.23 ...

随机推荐

  1. 什么是IPFS?IPFS与区块链有什么关系

    1.什么是IPFS? IPFS是Inter Planetary File System(星际文件系统)的缩写,是一个典型的点对点分布式文件系统, 旨在用同一个文件系统连接所有的计算设备.这时候有些小伙 ...

  2. Day7 - K - Biorhythms POJ - 1006

    Some people believe that there are three cycles in a person's life that start the day he or she is b ...

  3. 解决上传代码到GitHub报错Push rejected: Push to origin/master was rejected

    最近在 push 代码到 github 时,IDEA报错 Push rejected: Push to origin/master was rejected 在网友找了一圈,发现都不是想要的答案 于是 ...

  4. 2020寒假 05 ——eclipse安装scala环境

    在eclipse中安装Scala环境 1安装eclipse插件步骤,点击help,选择Eclipse Marketplace 2.输入Scala,点击go 3.选择搜索到的Scala IDE 4.7. ...

  5. 013、MySQL取本月最后日期,取每个月的最后一天日期

    #取本月最后一天 SELECT last_day( curdate( ) ); #取上个月最后一天 , INTERVAL MONTH ) ); , INTERVAL MONTH ) ); , INTE ...

  6. java.lang.IllegalStateException: Active Spring transaction synchronization or active JTA transaction with specified [javax.transaction.TransactionManager] required

    错误信息: java.lang.IllegalStateException: Active Spring transaction synchronization or active JTA trans ...

  7. CSS样式表——格式与选择器

    1.分类 1)内联(写在标签内部) style="样式" 控制精确,代码重用性差 2)内嵌(在<head></head>中) <style type= ...

  8. u盘使用记录、痕迹删除技巧方法

    在日常生活的使用U盘过程当中,系统会记录下大量U盘的使用记录信息,那么接下来小编就来同大家分享介绍如何删除掉这些使用记录的方法知识. 1. 往系统里面添加环境变量devmgr_shownonprese ...

  9. CAN分帧发送程序说明

    试验平台 仅仅 需要一台主机 一台 周立功 CAN 助手, 一个232 助手就OK ICAN 协议 资源节点地址 电脑 我认为是0x01 51单片机主机的地址 是 0x1f 建立连接的 功能码 是0x ...

  10. NetWork--记一次Http和TLS抓包

    参考 前言 工具 wireshark IP 发送方IP: 150.236.224.39 服务IP: 10.210.164.20 消息 Http,Https消息使用org.apache.http.cli ...