当前位置:网站首页>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;
}
边栏推荐
- Task05: tree
- Secret
- Task03: building an offline material system
- Start with interpreting the code automatically generated by BDC, and explain the trial version of the program components of sapgui
- Method of using dism command to backup driver in win11 system
- [matlab] image enhancement (power transformation, histogram specification processing method, smoothing and sharpening filtering)
- Is it appropriate for a 27 - year-old girl to change her career from zero to software testing?
- Jump without refresh - detailed explanation of pushstate and replacestate methods in history
- cannot import name ‘dtensor‘ from ‘tensorflow.compat.v2.experimental‘
- Video compression data set TVD
猜你喜欢

Task07: double pointer
![Is it correct to declare an array in this way? int n=10,a[n]; What if so? const int n =10; int a[n];](/img/e9/b09151d9b92d0a216e2ba079a7beb4.jpg)
Is it correct to declare an array in this way? int n=10,a[n]; What if so? const int n =10; int a[n];

Today's sleep quality record 80 points

Start with interpreting the code automatically generated by BDC, and explain the trial version of the program components of sapgui

Data and electricity course design: circuit of full adder / subtractor

Oracle collects statistics

CRS-4544 & ORA-09925
![[3.delphi common components] 8 dialog box](/img/4b/c06f8789cee58705a0b77f3ca1eee6.jpg)
[3.delphi common components] 8 dialog box

flutter_ Swiper carousel map plug-in

Seven principles that should be known by software testers
随机推荐
How to change the administrator's Avatar in win11? Win11 method of changing administrator image
Infinite level classification (or menu) design
Introduction and practice of QT tcp/udp network protocol (II) UDP communication
Task04: String
接口自动化核心知识点浓缩,为面试加分
Metersphere tutorial: how to assert when the returned result of the interface is null
Fallible point--
Task07: double pointer
安全生产月知识竞赛——新安法知多少
[3.delphi common components] 5 List class component
Battery control of QT widget (qpainter)
[matlab] basic operation of MATLAB image processing
[matlab] image restoration
[leetcode] breadth first search level traversal general disassembly template
In May, the main growth ranking list (platform of station B) of the single flying melon data up was released
---Arrange numbers---
QT database learning notes (I) basic concepts of database
Task03: building an offline material system
AI fanaticism | come to this conference and work together on the new tools of AI!
从测试零基础到测试架构师,这10年,他是这样让自己成才的