当前位置:网站首页>Epoll 反应堆模型核心原理及代码讲解
Epoll 反应堆模型核心原理及代码讲解
2022-06-11 01:04:00 【狱典司】
Epoll 反应堆模型核心原理及代码讲解
[Ⅰ] Epoll 原理及应用 && ET模式与LT模式
第一部分文章链接: Epoll 原理及应用 && ET模式与LT模式
[Ⅱ] Epoll 反应堆模型核心原理及代码讲解
一、反应堆核心原理
epoll反应堆模型的三个要素:
epoll ET模式
非阻塞轮询处理
struct epoll_event结构体中epoll_data_t联合体中的void *ptr指针 – 实现回调机制结构体回顾:
【重要理解】该
struct epoll_event结构体是可以理解为可(通过epoll——ctl())挂载到内核的epoll监听红黑树上的结构体(类似深拷贝的机制):struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; // 该fd就是传入epoll_ctl()的对应监听事件的fd uint32_t u32; uint64_t u64; } epoll_data_t;联合体又叫共用体,联合体内的变量共同使用一片地址空间。
最基本的使用中,放入联合体中的值是fd,如下例伪代码:
/* int connfd 是accpt()返回的socket连接句柄 */ struct epoll_event event = { 0, { 0}}; event.events = EPOLLIN; event.data.fd = connfd; epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); ...... // 业务逻辑 while (1) { /*监听红黑树efd, 将满足的事件的文件描述符加至events数组中, 阻塞wait*/ int nfd = epoll_wait(efd, events, MAX_EVENTS+1, -1); for (i = 0; i < nfd; i++) { /* 使用int类型, 接收联合体data的fd成员 */ int readyfd = events[i].data.fd; ...... // 业务逻辑 } }但反应堆模型不直接放入fd,而是放入一个自定义的结构体指针(强制转换成了void *类型),这样epoll_wait()返回的时候就可以取出之前存入的自定义结构体。
/* 用户自定义结构体 */ /* 描述就绪文件描述符相关信息 */ struct myevent_s { int fd; //要监听的文件描述符 int events; //对应的监听事件 void *arg; //泛型参数 void (*call_back)(int fd, int events, void *arg); //回调函数 int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听) char buf[BUFLEN]; int len; long last_active; //记录每次加入红黑树 g_efd 的时间值 }; ...... // 业务逻辑 /* struct myevent_s *ev 是用户自定义结构体 */ struct epoll_event epv = { 0, { 0}}; epv.events = ev->events = EPOLLIN; //EPOLLIN 或 EPOLLOUT epv.data.ptr = ev; // 注意这里不是epv.data.fd = connfd epoll_ctl(efd, EPOLL_CTL_ADD, ev->fd, &epv) ...... // 业务逻辑 while(1) { /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/ int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000); ...... //出错处理 for (i = 0; i < nfd; i++) { /*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/ struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; ...... // 业务逻辑 } }回调机制的实现:
在自定义结构体中存储指针函数,epoll_wait返回后取出
events[i].data.ptr指向的自定义结构体,然后调用结构体中存储的回调函数:struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; ...... //判断 ev->call_back(ev->fd, events[i].events, ev->arg);
二、反应堆模型示例
2.1 整体逻辑
socket、bind、listen – epoll_create 创建监听 红黑树 – 返回 epfd
epoll_ctl() 向红黑树上添加一个listenfd(监听socket)
while(1) {
【可选】每轮迭代监测100个连接,若存在超时连接(沉积用户)则主动关闭;
epoll_wait()监听 --> 对应监听fd有事件产生 --> 返回监听满足结构集 (即struct epoll_event结构体数组);判断返回数组元素 :
- lfd满足EPOLLIN事件(读事件) --> 回调
acceptconn()函数(主要完成accept()的任务)
acceptconn()函数:- 调用
accept()接受新的连接,开启 cfd socket,置为非阻塞; - 从全局的自定义结构体数组
struct myevent_s g_events[MAX_EVENTS+1]中找一个空闲元素ev; - 调用
eventset()函数将 cfd 和 回调函数senddata写入ev中; - 调用
eventadd()将 全局的&ev作为struct epoll_event结构体的data联合体的void *ptr指针,设置监听EPOLLIN(读事件),挂载到epoll的监听红黑树上。
- cfd满足EPOLLIN事件(读事件) --> 回调
recvdata()函数(主要完成读操作)
recvdata()函数:- 调用
epoll_ctl()和宏EPOLL_CTL_DEL将cfd从红黑树上摘下; - 处理输入,并将处理结构保存到用户自定义的结构体的
char buf[BUFLEN]成员变量中; - 更改监听事件为
EPOLLOUT,更改cfd对应的回调函数为senddata() - 调用
epoll_ctl()和宏EPOLL_CTL_ADD将cfd重新挂载到红黑树上;
注意:
严谨的来说,write也要通过监听机制确认是否可写,因为在实际网络环境中,假如通信对端存在半关闭的情况,或者滑动窗口满的情况,就无法成功wtrite数据。
- cfd满足EPOLLOUT事件(写事件)–> 回调
senddata()函数(主要完成写操作)
senddata()函数:- 调用
epoll_ctl()和宏EPOLL_CTL_DEL将cfd从红黑树上摘下; - 将保存到在用户自定义的结构体的
char buf[BUFLEN]中数据拷贝到cfd的发送缓冲区(即write()/send()函数); - 更改监听事件为
EPOLLIN,更改cfd对应的回调函数为recvdata() - 调用
epoll_ctl()和宏EPOLL_CTL_ADD将cfd重新挂载到红黑树上;
- lfd满足EPOLLIN事件(读事件) --> 回调
}
// while(1) end
2.2 重要函数讲解
① eventset()函数
声明:void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
功能:将自定义结构体 myevent_s 成员变量 初始化
调用示例:
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);eventset(ev, fd, recvdata, ev);eventset(ev, fd, senddata, ev);
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd; // 待监听fd
ev->call_back = call_back; // 设置回调函数
ev->events = 0; // 监听事件由函数 eventadd()函数指定
ev->arg = arg; // ev的arg即ev中回调函数的参数,是ev本身(比较难理解,但这是关键)
ev->status = 0; // 结构体状态标记为"已占用"
memset(ev->buf, 0, sizeof(ev->buf)); //清空结构体的char *缓冲区
ev->len = 0; // 将缓冲区数据长度置0
ev->last_active = time(NULL); //调用eventset函数的时间(可选,用于断开沉积连接)
return;
}
② eventadd()函数
声明:void eventadd(int efd, int events, struct myevent_s *ev)
功能:向 epoll监听的红黑树添加一个监听节点
调用示例:
eventadd(g_efd, EPOLLIN, &g_events[i]);eventadd(g_efd, EPOLLIN, ev);eventadd(g_efd, EPOLLOUT, ev);
/* eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); */
void eventadd(int efd, int events, struct myevent_s *ev)
{
/* 从自定义的结构体指针struct myevent_s *的变量ev中 提取数据到一个可以挂在到epoll监听红黑树上的struct epoll_event变量 epv上 */
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) { //已经在红黑树 g_efd 里
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
}
③ eventdel()函数
声明:void eventadd(int efd, int events, struct myevent_s *ev)
功能:从epoll监听的红黑树上摘除一个监听节点
调用示例:
eventdel(g_efd, ev);
void eventdel(int efd, struct myevent_s *ev){
//不在红黑树上
if (ev->status != 1) return;
//修改状态
ev->status = 0;
//从红黑树 efd 上将 ev->fd 摘除
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, NULL);
return ;
}
④ acceptconn()函数
声明:void acceptconn(int lfd, int events, void *arg)
架构:
- 调用
accept()接受新的连接,开启 cfd socket,置为非阻塞; - 从全局的自定义结构体数组
struct myevent_s g_events[MAX_EVENTS+1]中找一个空闲元素ev; - 调用
eventset()函数将 cfd 和 回调函数senddata写入ev中; - 调用
eventadd()将 全局的&ev作为struct epoll_event结构体的data联合体的void *ptr指针,设置监听EPOLLIN(读事件),挂载到epoll的监听红黑树上。
/* 当lfd的读事件就绪, epoll返回, 调用该函数 与客户端建立链接 */
/* 在acceptconn内部去做accept */
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {
//将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
//将cfd添加到红黑树g_efd中,监听读事件
eventadd(g_efd, EPOLLIN, &g_events[i]);
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return ;
}
⑤ recvdata()函数
声明:void recvdata(int fd, int events, void *arg)
架构:
- 调用
epoll_ctl()和宏EPOLL_CTL_DEL将cfd从红黑树上摘下; - 处理输入,并将处理结构保存到用户自定义的结构体的
char buf[BUFLEN]成员变量中; - 更改监听事件为
EPOLLOUT,更改cfd对应的回调函数为senddata() - 调用
epoll_ctl()和宏EPOLL_CTL_ADD将cfd重新挂载到红黑树上;
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
//将该节点从红黑树上摘除
eventdel(g_efd, ev);
//读文件描述符, 数据存入myevent_s成员buf中
len = recv(fd, ev->buf, sizeof(ev->buf), 0);
if (len > 0) {
ev->len = len;
//手动添加字符串结束标记避免缓冲区溢出
ev->buf[len] = '\0';
printf("C[%d]:%s\n", fd, ev->buf);
//设置该 fd 对应的回调函数为 senddata
eventset(ev, fd, senddata, ev);
//将fd加入红黑树g_efd中,监听其写事件
eventadd(g_efd, EPOLLOUT, ev);
} else if (len == 0) {
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
} else {
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
/* 如果配合线程池使用,期望的是线程结束任务之后返回线程池,而不是被系统回收资源,所以这部分的线程不能够设置分离属性 */
}
⑥ senddata()函数
声明:void senddata(int fd, int events, void *arg)
架构:
- 调用
epoll_ctl()和宏EPOLL_CTL_DEL将cfd从红黑树上摘下; - 将保存到在用户自定义的结构体的
char buf[BUFLEN]中数据拷贝到cfd的发送缓冲区(即write()/send()函数); - 更改监听事件为
EPOLLIN,更改cfd对应的回调函数为recvdata() - 调用
epoll_ctl()和宏EPOLL_CTL_ADD将cfd重新挂载到红黑树上;
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
//从红黑树g_efd中移除
eventdel(g_efd, ev);
//直接将数据 回写给客户端。未作处理
len = send(fd, ev->buf, ev->len, 0);
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
//将该fd的 回调函数改为 recvdata
eventset(ev, fd, recvdata, ev);
//从新添加到红黑树上, 设为监听读事件
eventadd(g_efd, EPOLLIN, ev);
} else {
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
2.3 示例源码(Server端)
/* *epoll基于非阻塞I/O事件驱动 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAX_EVENTS 1024 //监听上限数
#define BUFLEN 4096
#define SERV_PORT 8080
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/* 描述就绪文件描述符相关信息 */
struct myevent_s {
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型参数
void (*call_back)(int fd, int events, void *arg); //回调函数
int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
};
int g_efd; //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
/*将结构体 myevent_s 成员变量 初始化*/
/* eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]); */
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL); //调用eventset函数的时间
return;
}
/* 向 epoll监听的红黑树 添加一个 文件描述符 */
/* eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); */
void eventadd(int efd, int events, struct myevent_s *ev)
{
/* 从自定义的结构体指针struct myevent_s *的变量ev中 提取数据到一个可以挂在到epoll监听红黑树上的struct epoll_event变量 epv上 */
struct epoll_event epv = {
0, {
0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) {
//已经在红黑树 g_efd 里
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加/修改
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
}
/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
void eventdel(int efd, struct myevent_s *ev)
{
struct epoll_event epv = {
0, {
0}};
if (ev->status != 1) //不在红黑树上
return ;
//epv.data.ptr = ev;
epv.data.ptr = NULL; //抹去指针
ev->status = 0; //修改状态
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除
return ;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
/* 在acceptconn内部去做accept */
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {
//将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return ;
}
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读文件描述符, 数据存入myevent_s成员buf中
eventdel(g_efd, ev); //将该节点从红黑树上摘除
if (len > 0) {
ev->len = len;
ev->buf[len] = '\0'; //手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件
} else if (len == 0) {
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
} else {
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return; /* 期望的是线程结束任务之后返回线程池,而不是被系统回收资源,所以这部分的线程不能够设置分离属性 */
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接将数据 回写给客户端。未作处理
eventdel(g_efd, ev); //从红黑树g_efd中移除
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata
eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件
} else {
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化lfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
return ;
}
int main(int argc, char *argv[])
{
unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口
g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树,返回给全局 g_efd
if (g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化监听socket
struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组 以供epoll_wait使用
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1) {
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
for (i = 0; i < 100; i++, checkpos++) {
//一次循环检测100个。 使用checkpos控制检测对象
if (checkpos == MAX_EVENTS) //根节点不参与检测
checkpos = 0;
if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
continue;
long duration = now - g_events[checkpos].last_active; //客户端不活跃的世间
if (duration >= 60) {
close(g_events[checkpos].fd); //关闭与该客户端链接
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除
}
}
/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0) {
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++) {
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
//读就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
//lfd EPOLLIN
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
//写就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前释放所有资源 */
return 0;
}
边栏推荐
- [error record] Android application security detection vulnerability repair (strandhogg vulnerability | set activity component android:taskaffinity= "")
- Internet of things final assignment - sleep quality detection system (refined version)
- Oracle tablespaces, users, and authorization to users
- Basic underlying principles of concurrent programming (4)
- SAP smartforms text content manual wrap output
- 20n10-asemi medium and small power MOS transistor 20n10
- In the past 10 years, from zero foundation testing to test architect, he has made himself successful
- JS Part 5
- SSH配置密钥登录时需要注意私钥是否设置了密码(passphrase)
- Merge sort ()
猜你喜欢

Understand the role of before and after Clearfixafter clear floating

NFT Insider #61:Animoca Brands 在 340 项投资中持有 15 亿美元的加密资产

The annual salary of testers in large factories ranges from 300000 to 8K a month. Roast complained that the salary was too low, but he was ridiculed by netizens?

腾讯面试官曰Mysql架构的内部模块索引原理及性能优化思路谁会?

JS basic part hand exercises

14: 00 interview, came out at 14:08, the question is really too

Enrichment of core knowledge points of interface automation to add points to the interview
![[3.delphi common components] 6 scroll bar](/img/55/891e56de4500a9128ac89e3c5b1721.jpg)
[3.delphi common components] 6 scroll bar

Internet of things final assignment - sleep quality detection system (refined version)

ABAP CDs realizes multi line field content splicing
随机推荐
浅析直播间海量聊天消息的架构设计难点
Fb02 edit coding block field
ABAP CDs realizes multi line field content splicing
Analysis of the difficulties in the architecture design of massive chat messages in the live broadcast room
Md61 plan independent demand import Bapi [by daily dimension / dynamic template / dynamic field]
Coordinates of the capital of each province in China
Task01: be familiar with the basic process of news recommendation system
Programming implementation: input any English month, and output its corresponding Chinese prompt after looking up the month table. Abbreviations can also be found.
NFT Insider #61:Animoca Brands 在 340 项投资中持有 15 亿美元的加密资产
Record the actual record of my question brushing
Data and electricity course design: circuit of full adder / subtractor
JS basic part hand exercises
Coordonnées des capitales provinciales des provinces chinoises
Sequence table exercises
[3.delphi common components] 6 scroll bar
Method of using dism command to backup driver in win11 system
[BSP video tutorial] BSP video tutorial issue 17: single chip microcomputer bootloader topic, startup, jump configuration and various usage of debugging and downloading (2022-06-10)
CRS-4544 & ORA-09925
CRS-5017
ACM tutorial - heap sorting