TCP/IP网络编程 第十七章:优于select的epoll

epoll理解及应用

select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。

基于select的I/O复用技术速度慢的原因

第12章曾经实现过基于select的IO复用服务器端,很容易从代码上分析出不合理的设计,最
主要的两点如下。
□调用select函数后常见的针对所有文件描述符的循环语句。
□每次调用select函数时都需要向该函数传递监视对象信息。
上述两点可以从第12章回声服务器示例的第45,49行及第54行代码得到确认。调用select函数后,并不是把发生变化的文件描述符单独集中到一起,而是通过观察作为监视对象的fd_set变量的变化,找出发生变化的文件描述符(示例的第54、56行),因此无法避免针对所有监视对象的循环语句。而且,作为监视对象的fd_set变量会发生变化,所以调用select函数前应复制并保存原有信息(参考示例的第45行),并在每次调用select函数时传递新的监视对象信息。


各位认为哪些因素是提高性能的更大障碍?是调用select函数后常见的针对所有文件描述符对象的循环语句?还是每次需要传递的监视对象信息?

在代码层面上思考,很容易认为是循环。但相比于循环语句,更大的障碍是每次传递监视对象信息。因为传递对象信息具有如下含义:"每次调用select函数时向操作系统传递监视对象信息。"

应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决,因此将成为性能上的致命缺点。

select函数的这一缺点可以通过如下方式弥补:“仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”

这样就无需每次调用select函数时都向操作系统传递监视对象信息,但前提是操作系统支持这种处理方式(每种操作系统支持的程度和方式存在差异)。Linux的支持方式是epoll,Windows的支持方式是IOCP。

select也有优点

知道这些内容后,有些人可能对select函数感到失望,但大家应当掌握select函数。本章的epoll方式只在Limux下提供支持,也就是说,改进的IO复用模型不具有兼容性。相反,大部分操作系统都支持select函数。只要满足或要求如下两个条件,即使在Linux平台也不应拘泥于epoll。
□服务器端接入者少
□程序应具有兼容性
实际并不存在适用于所有情况的模型。各位应理解好各种模型的优缺点。

实现epoll时必要的函数和结构体

能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。
□无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
□调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

下面介绍epoll服务器端实现中需要的三个函数,希望各位结合epoll函数的优点理解这些函数的功能。

□epoll_create:创建保存epoll文件描述符的空间。

□epoll_ctl:像空间注册或注销文件描述符。

□epoll_wait:于select函数类似,等待文件描述符发生变化。

select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。但epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。
此外,为了添加和删除监视对象文件描述符,select方式中需要FD_SET,FD_CLR函数。但在epoll方式中,通过epoll_ctl函数请求操作系统完成。最后,select方式下调用select函数等待文件描述符的变化,而epoll中调用epoll_wait函数。还有,select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。

struct epoll_event{__uint32_t  events;epoll_data_t data;
}typedef union epoll_data{void *ptr;int fd;__uint32_t u32;__uint64_t u64;}epoll_data_t;

声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息被填入该数组。因此无需向select函数那样针对所有文件描述符进行循环。

epoll_create

epoll是从Linux2.5.44版内核开始引入的,所以使用epoll前需要验证Linux内核版本。下面仔细观察epoll_create函数。

#include<sys/epoll.h>
int epoll_create(int size);//成功时返回epoll文件描述符,失败时返回-1size   //epoll示例的大小

调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程"。还有一个注意点,这个传入的size大小实际上并非用来决定epoll例程的大小,而仅供操作系统参考。

epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。需要终止时,与其他文件描述符相同,也要调用close函数。

epoll_ctl

#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//成功时返回0,失败时返回-1epfd    //用于注册监视对象epoll例程的文件描述符op      //用于指定监视对象的添加,删除或更改等操作 fd      //需要注册监视对象文件描述符event   //监视对象的事件类型

接下来是两个简单示例

epoll_ctl(A,EPOLL_CTL_ADD,B,C);
epoll_ctl(A,EPOLL_CTL_DEL,B,NULL);

第一句含义是:"epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件"

第二句含义是:"从epoll例程A中删除文件描述符B"

从上述调用语句中可以看到,从监视对象中删除时,不需要监视类型(事件信息),因此向第四个参数传递NULL。

接下来介绍可以向epoll_ctl第二个参数传递的常量及含义。
□ EPOLL_CTL_ADD:将文件描述符注册到epoll例程。

□ EPOLL_CTL DEL:从epoll例程中删除文件描述符。

□ EPOLL _CTL_MOD:更改注册的文件描述符的关注事件发生情况。

下面讲解各位不太熟悉的epoll_ctl函数的第四个参数,其类型是之前讲过的epoll_event结构体指针。如前所述,epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll例程中注册文件描述符时,用于注册关注的事件!(有两个功能)函数中epoll_event结构体的定义并不显眼,因此通过调用语句说明该结构体在epoll_ctl函数中的应用。

struct epoll_event event;
....
event.events=EPOLLIN;//事件种类
event.data.fd=sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
....

上述代码将sockfd注册到epoll例程epfd中,并在需要读取数据的情况下产生相应事件。接下来给出epoll_event的成员events中可以保存的常量及所指的事件类型。
□ EPOLLIN:需要读取数据的情况。
□ EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
□ EPOLLPRI:收到OOB数据的情况。
□ EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
□ EPOLLERR:发生错误的情况。
□ EPOLLET:以边缘触发的方式得到事件通知。
□ EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向
epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

可以通过位或运算同时传递多个上述参数。关于“边缘触发”稍后将单独讲解,目前只需记住EPOLLIN即可。

epoll_wait

最后介绍与select函数对应的epoll_wait函数,epoll相关函数中默认最后调用该函数。

#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event*events,int maxevent,int timeout);epfd      //表示事件发生监视范围的epoll例程的文件描述符events    //保存发生事件的文件描述符集合的结构体地址maxevents //表示第二个参数中可以保存的最大事件数timeout   //以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件

该函数的调用方式如下。需要注意的是,第二个参数所指缓冲需要动态分配。

int event_cnt;
struct epoll_event* ep_events;
....
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
....
event_cnt=epoll_wait(epfd,ep_event,EPOLL_SIZE,-1);
....

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

基于epoll的回声服务端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);int main(int argc,char *argv[]){int serv_sock,clnt_sock;struct sockaddr_in serv_addr,clnt_addr;socklen_t addr_sz;int str_len,i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;in epfd,event_cnt;if(argc!=2){printf("Usage : %s <port>\n",argv[0]);exit(1);}serv_sock=socket(PF_INET,SOCK_STREAM,0);memset(&serv_addr,0,sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);serv_addr.sin_port=htons(argv[1]);if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)error_handling("bind() error");if(listen(serv_sock,5)==-1)error_handling("listen() error");epfd=epoll_create(EPOLL_SIZE);ep_events=malloc(sizeof(struct epoll_event)*EOLL_SIZE);event.events=EPOLLIN;event.data.fd=serv_sock;epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event);while(1){event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);if(event_cnt==-1){puts("epoll_wait() error");break;}for(i=0;i<event_cnt;;++i){if(ep_events[i].data.fd==serv_sock){addr_sz=sizeof(clnt_addr);clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,sizeof(addr_sz));event.events=EPOLLIN;event.data.fd=clnt_sock;epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);printf("connected client: %d \n",clnt_sock);}else{str_len=read(ep_events[i].data.fd,buf,BUF_SIZE);if(str_len==0){  //传输完成printf("close client: %d \n",ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);}else{write(ep_events[i].data.fd,buf,str_len);//回声}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *message){fputs(buf,stderr);fputc('\n',stderr);exit(1);
}

上述代码的演示和之前章节中的select函数代码相似,可以结合之前章节中的代码思路进行理解。

条件触发和边缘触发

有些人学习epoll时往往无法正确区分条件触发(Level Trigger)和边缘触发(Edge Trigger),
但只有理解了二者区别才算完整掌握epoll。

条件触发和边缘触发的区别在于发生事件的时间点

首先给出示例帮助各位理解条件触发和边缘触发。观察如下对话,可以通过对话内容理解条件触发事件的特点。

儿子:"妈妈,这次期末考试我全部都是A。"

妈妈:"好棒!"

儿子:"我的总分排全班第一。"

妈妈:"做的好!"

儿子:"但我的总分在年级中只排第十名"

妈妈:"不要灰心,以及很好了!"

从上述对话可以看出,儿子从说期末考开始一直向妈妈报告,这就是条件触发的原理。我将其整理如下:“条件触发方式中,只要输入缓冲有数据就会一直通知该事。"

例如,服务器端输入缓冲收到50字节的数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务器端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。接下来通过如下对话介绍边缘触发的事件特性。

儿子:"妈妈,我期末考了。"

妈妈:“考的怎么样。”

儿子:"........."

妈妈:"说话啊!是考差了吗?"

从上述对话可以看出,边缘触发中输人缓冲收到数据时仅注册1次该事件。即使输入缓冲中
还留有数据,也不会再进行注册。

掌握条件触发的事件特性

接下来通过代码来了解条件触发的事件注册方式。epoll默认是以条件触发的方式工作,因此可以通过该实例验证条件触发的特性。

#include <"与之前示例的头文件声明一致,故省略。">
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *buf);int main(int argc, char *argv[]){int serv_sock, clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!=2){printf("Usage : %s <port>\n", argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY)serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock,(struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock,5)==-1)error_handling("listen() error");epfd=epoll_create(EPOLL_SIZE);ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);event.events=EPOLLIN;event.data.fd=serv_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);if(event_cnt==-1){puts("epoll_wait() error");break;}puts("return epoll_wait");for(i=0; i<event_cnt; i++){if(ep_events[i].data.fd==serv_sock){adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);event.events=EPOLLIN;event.data.fd=clnt_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock,&event);printf("connected client: %d \n", clnt_sock);}else{str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len==0){ //关闭套接字printf("closed client: %d \n", ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);}else{write(ep_events[i].data.fd,buf,str_len);//回声}}}}close(serv_sock);close(epfd);return 0;
}void error_handling(char *buf){//与之前实例相同,故省略
}

上述示例与之前的差异如下。

□将调用read函数时使用的缓冲大小缩减为4个字节(第2行)

□插入验证epoll_wait函数调用次数的语句(第50行)
减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出”return epoll_wait”字符串。

从运行结果中可以看出,每当收到客户端数据时,都会注册该事件,并因此多次调用epoll_ wait函数。下面将上述示例改成边缘触发方式,需要做一些额外的工作。但我希望通过最小的改动验证边缘触发模型的事件注册方式。将上述示例的第57行改成如下形式运行服务器端和客户端:

event.events = EPOLLIN|EPOLLET;


更改后可以验证如下事实:“从客户端接收数据时,仅输出1次'return epoll_wait'字符串,这意味着仅注册1次事件。”虽然可以验证上述事实,但客户端运行时将发生错误。大家是否遇到了这种问题?能否自行分析原因?虽然目前不必对此感到困惑,但如果理解了边缘触发的特性,应该可以分析出错误原因。

边缘触发的服务器端实现中必知的两点

如下两点是实现边缘触发的必知内容。

□通过errno变量验证错误原因。
□为了完成非阻塞(Non-blocking)I/O,更改套接字特性。

Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这
些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了如下
全局变量:

int errno;

为了访问该变量,需要引入error.h头文件。另外每种函数发生错误时,保存到errno变量中的值都不同,没必要记住所有可能的值。学习每种函数的过程中逐一掌握,并能在必要时参考即可。本节只介绍如下类型的错误:"read函数发现输入缓冲中没有数据可读时返回-1,同时在errno中保存EAGAIN常量。”

稍后通过示例给出errno的使用方法。下面讲解将套接字改为非阻塞方式的方法。Linux提供更改或读取文件属性的如下方法(曾在第13章使用过)。

#include<fcntl.h>
int fcntl(int filedes,int cmd,...);
//成功是返回cmd参数相关值,失败时返回-1filedes  //目标的文件描述符。cmd      //表示函数调用的目的。

从上述声明中可以看到,fcntl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下2条语句。

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read&write函数时,无论是否存在数据,都会形成非阻塞文件。fcntl函数的适用范围很广,各位既可以在学习系统编程时一次性总结所有适用情况,也可以每次需要时逐一掌握。

实现边缘触发的回声服务器端

之所以介绍读取错误原因的方法和非阻塞模式的套接字创建方法,原因在于二者都与边缘触发的服务器端实现有密切联系。

首先说明为何需要通过errno确认错误原因:“边缘触发方式中,接收数据时仅注册1次该事件。”
就因为这种特点,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验
证输入缓冲是否为空。(不然套接字将无法注销)

既然如此,为何还需要将套接字变成非阻塞模式?边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿(等待数据到来)。因此,边缘触发方式中一定要采用非阻塞read&write函数。接下来给出以边缘触发方式工作的回声服务器端示例。

#include<“添加fcntl.h、errno.h,其他与之前示例的头文件声明一致。“>
#include <fcntl.h>
#include <errno.h>
#define BUF SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);int main(int argc, char *argv[]){int serv_sock,clnt_sock;struct sockaddr_in serv_adr, clnt_adr;socklen_t adr_sz;int str_len, i;char buf[BUF_SIZE];struct epoll_event *ep_events;struct epoll_event event;int epfd, event_cnt;if(argc!=2){printf("Usage : %s <port>\n",argv[0]);exit(1);}serv_sock=socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family=AF_INET;serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);serv_adr.sin_port=htons(atoi(argv[1]));if(bind(serv_sock,(struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)error_handling("bind() error");if(listen(serv_sock,5)==-1)error_handling("listen()error");epfd=epoll_create(EPOLL_SIZE);ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);setnonblockingmode(serv_sock);event.events=EPOLLIN;event.data.fd=serv_sock;epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);while(1){event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE,-1);if(event_cnt==-1){puts("epoll_wait() error");break;}puts("return epoll_wait");for(i=0; i<event_cnt; i++){if(ep_events[i].data.fd==serv_sock){adr_sz=sizeof(clnt_adr);clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);setnonblockingmode(clnt_sock);event.events=EPOLLIN|EPOLLET;event.data.fd=clnt_sock;epoll_ctl(epfd,EPOLL_CTL_ADD, clnt_sock,&event);printf("connected client: %d \n", clnt_sock);}else{while(1){str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);if(str_len==0){ // 客户端关闭连接printf("closed client: %d \n", ep_events[i].data.fd);close(ep_events[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);break;}else if(str_len<0){if(errno==EAGAIN)break;else {write(ep_events[i].data.fd, buf, str_len); // 回声}}}}}
close(serv_sock);
close(epfd);
return 0;
}void setnonblockingmode(int fd){int flag=fcntl(fd,F_GETFL,0);fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}void error_handling(char *buf){//示例和之前的示例相同,故省略
}

条件触发和边缘触发孰强孰弱

我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发
相对于条件触发的优点。边缘触发方式下可以做到如下这点:“可以分离接收数据和处理数据的时间点!”

虽然比较简单,但非常准确有力地说明了边缘触发的优点。关于这句话的含义,大家以后开
不同类型的程序时会有更深入的理解。现阶段给出如下情景帮助大家理解:有一个服务器端和三个客户端,这三个客户端分别是A,B,C,它们分别向服务器发送它们对应部分的数据,服务器需要将这些数据组合,以A,B,C的正向顺序排列发送给任意主机。

那么对应服务端的运行过程如下:

□服务器端分别从客户端A、B、C接收数据。
□服务器端按照A、B、C的顺序重新组合收到的数据。
□组合的数据将发送给任意主机。

为了完成该过程,若能按如下流程运行程序,服务器端的实现并不难。
□客户端按照A、B、C的顺序连接服务器端,并依序向服务器端发送数据。

□需要接收数据的客户端应在客户端A、B、C之前连接到服务器端并等待。

但现实中可能频繁出现如下这些情况,换言之,如下情况出现更符合实际。

□客户端C和B正向服务器端发送数据,但A尚未连接到服务器端。

□客户端A、B、C乱序发送数据。
□服务器端已收到数据,但要接收数据的目标客户端还未连接到服务器端。

因此,即使输入缓冲收到数据(注册相应事件),服务器端也能决定读取和处理这些数据的
时间点,这样就给服务器端的实现带来巨大的灵活性。相比于条件触发,如果尝试分离接受数据和处理数据的时间的话,则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务端能够接受吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/4898.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Camtasia Studio 2023保存为mp4格式的视频的详细教程,Camtasia的视频导出功能

很多用户刚接触Camtasia Studio&#xff0c;不熟悉如何保存mp4格式的视频。在今天的文章中小编为大家带来了Camtasia Studio 2023保存为mp4格式的视频的详细教程介绍。 1、 打开Camtasia Studio。 Camtasia Studio- 2023 win&#xff1a; https://souurl.cn/1JFEsn Camtasia …

06_本地方法接口+07_本地方法栈

一、本地方法&#xff1f; 本地方法就是Java调用非Java代码的接口。 本地方法的作用是融合不同的编程语言为Java所用&#xff0c;它的初衷是融合 C、C程序 二、为什么要使用Native Method? 三、本地方法栈 Java虚拟机栈用于管理Java方法的调用&#xff0c;而本地方法栈用于…

【Linux】Docker 基本管理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 Docker 基本管理 Docker 概述Docker 核心概念Docker 安装部署Docker 镜像操作Docker 容器操作 Docker 概述 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵…

python爬虫中通用的两种乱码解决方式(自用)

问题&#xff1a;在python爬虫爬取的时候&#xff0c;我们有时会遇到诸如以下的乱码&#xff1a; &#xfffd;װŮ&#xfffd;&#xfffd; &#xfffd;&#xfffd;Ů ˮ СϪ Ψ&#xfffd;&#xfffd;¡4k解决方法一&#xff1a;用utf-8来转码&#xff0c;具…

spring复习:(40)全注解的spring AOP

零、需要的依赖&#xff1a; <dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>1.8.9</version></dependency><dependency><groupId>org.aspectj</groupId><arti…

AHB协议理解

从小父亲就教育我&#xff0c;做一个对社会有用的人&#xff01; 目录 Chapter1 AHB Block Diagram Ginput signal lnput signals Output Signal Chapter3 Transfers AHB接口Overview Chapter6 Data Buses HWDATA HRDATA Chapter1 Introduction AHB: Advanced High-performanc…

QT写文件操作

在Qt中&#xff0c;可以使用QTextStream类来按照指定的格式将数据写入文件。以下是按照格式写入文件的一个示例&#xff1a; #include <QFile> #include <QTextStream>int main() {QString fileName "output.txt";QFile file(fileName);if (!file.open…

奇迹MU架设教程:SQL Server 2008数据库的安装教程

不管是搭建什么游戏&#xff0c;都是有数据库的&#xff0c;奇迹MU用的是SQL 数据库&#xff0c;根据服务器系统选择SQL server版本&#xff0c;我比较喜欢用Windows server 2008R2系统&#xff0c;所以我安装的是SQL server 2008。作为架设奇迹很重要的数据库程序&#xff0c;…

【Ubuntu】安装docker-compose

要在Ubuntu上安装Docker Compose&#xff0c;可以按照以下步骤进行操作&#xff1a; 下载 Docker Compose 二进制文件&#xff1a; sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/loc…

Modbus-TCP数据问询解析-小记

调试代码 a00 23 00 00 00 05 01 03 02 00 64 aa.replace(" ","") alen(a) print(a)b3d650000000b011000570002044203a800 print(len(b))c01 10 00 57 00 02 04 42 03 68 00 cc.replace(" ","") clen(c) print(c)运行结果&#xff1…

Docker操作

Docker操作 Docker基本操作 # 删除 docker 容器 docker rm 关键字&#xff08;名字、容器ID&#xff09; # 删除docker所有容器 docker ps -a -q | xargs docker rm docker rm -f $(docker ps -qa) # 删除 docker镜像 docker rmi 关键字&#xff08;名字、容器ID&#xff…

内核态、用户态概念

一、MTU MTU概念 MTU&#xff08;Maximum Transmission Unit&#xff0c;最大传输单元&#xff09;是指在网络中传输的数据包的最大长度限制&#xff0c;它是一个重要的网络参数&#xff0c;影响着网络的可靠性、稳定性和性能。在TCP/IP协议栈中&#xff0c;MTU涉及到内核态和…

Pthreads程序实现任务队列

主线程启动用户指定数量的线程&#xff0c;这些线程进入条件等待状态。 主线程生成一些任务&#xff08;一定计算量&#xff09;&#xff0c;每生成一个新的任务&#xff0c;就用条件变量唤醒一个线程&#xff0c;当这个唤醒线程执行完任务时&#xff0c;回到条件等待状态。 当…

ThreeJS打造自己的人物

hello&#xff0c;大家好&#xff0c;我是better&#xff0c;今天为大家分享如何使用Three打造属于自己的3D人物模型。 人物建模 当下有很多人物建模的网站&#xff0c;这里给大家分享的 Ready Player Me - Create a Full-Body 3D Avatar From a Photo 前往这个网址&#xff…

C#List转IList方法

最近工作中使用到了C#的List和IList。 这里参考百度上的资料&#xff0c;总结一下。 IList使用命名空间&#xff1a; using System.Collections; List<T>类:表示可通过索引访问的对象的强类型列表&#xff0c;提供用于对列表进行搜索、排序和操作的方法。 IList<T&…

​​Layui之用户管理实例(对数据的增删改查)

目录 ​编辑一、R工具介绍&#xff08;&#xff09; ​编辑二、数据表的增删改查 ​编辑2.1我们先得从查询数据库的语句入手 2.2优化dao类 2.4UserAction类 2.5前台的页面实现增删改查操作 2.6 userManage页面JS 2.7user新增、修改iframe层js 前言 上一篇我分享了…

试玩python的web框架 flask、fastapi、tornado、django

文章目录 一、Flask入门案例 [官网](https://flask.net.cn/quickstart.html) [其它参考](https://zhuanlan.zhihu.com/p/104273184?utm_id0)二、FastAPI入门案例 [官网](https://fastapi.tiangolo.com/zh/) [w3cschool教程](https://www.w3cschool.cn/fastapi/fastapi-feature…

无涯教程-Javascript - 运算符

让无涯教程用一个简单的表达式 4 59 。这里的4和5称为操作数&#xff0c;" "称为运算符&#xff0c;JavaScript支持以下类型的运算符。 算术运算符比较运算符逻辑运算符赋值运算符三元)运算符 算术运算符 JavaScript支持以下算术运算符&#xff0c;假设变量A10&a…

[RocketMQ] Broker CommitLogDispatcher 异步构建ConsumeQueue和IndexFile源码解析 (十四)

CommitLogDispatcherBuildConsumeQueue: 异步构建ConsumerQueue。CommitLogDispatcherBuildIndex: 异步构建IndexFile。 文章目录 1.CommitLogDispatcherBuildConsumeQueue构建ConsumeQueue1.1 putMessagePositionInfo写入消息位置信息1.2 findConsumeQueue查找ConsumeQueue1.2…

go初识iris框架(二) - get,post请求和数据格式

继初步了解iris后 文章目录 获取url路径获取数据get请求post请求获取JSON数据格式JSON返回值获取XML数据格式XML返回值 获取url路径 package mainimport "github.com/kataras/iris/v12"func main(){app : iris.New()app.Get("/hello",func(ctx iris.Conte…