Linux高性能服务器编程 学习笔记 第九章 IO复用

IO复用使程序能同时监听多个文件描述符,这可以提高程序的性能,通常网络程序在以下情况需要使用IO复用:
1.客户端进程需要同时处理多个socket。

2.客户端进程需要同时处理用户输入和网络连接。

3.TCP服务器要同时处理监听socket和连接socket。

4.服务器要同时处理TCP请求和UDP请求。

5.服务器要同时监听多个端口,或处理多种服务,如xinetd服务器。

IO复用能同时监听多个文件描述符,但它本身是阻塞的,且当多个文件描述符同时就绪时,如果不采取额外措施,进程只能按顺序依次处理其中的每个文件描述符,这使得服务器看起来像是串行工作的,如果要实现并发,只能用多进程或多线程等编程手段。

Linux下实现IO复用的系统调用主要有select、poll、epoll。

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件,它的原型如下:
在这里插入图片描述
nfds参数指定被监听的文件描述符中的最大值加1,因为文件描述符是从0开始计数的。

readfds、writefds、exceptfds参数分别指向可读、可写、异常事件对应的文件描述符集合,应用调用select时,我们通过这3个参数传入自己感兴趣的文件描述符,当select函数返回时,内核将修改它们来通知应用进程哪些文件描述符已经就绪。这3个参数是fd_set类型的指针:
在这里插入图片描述
由上图,fd_set结构中仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符,fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select函数能同时处理的文件描述符总量。

由于位操作过于繁琐,我们应使用以下宏来访问fd_set结构中的位:
在这里插入图片描述
timeout参数用来设置select函数的超时时间,它是一个timeval结构的指针,采用指针参数是因为内核将修改它以告诉应用select函数等待了多久(在Linux上,其他实现不这么做),但我们不能完全信任select函数返回后的timeout参数值,如调用失败时timeout参数值是不确定的。timeval结构的定义如下:
在这里插入图片描述
由上图,select函数给我们提供了一个微秒级的定时方式,如果给timeout参数的tv_sec成员和tv_usec成员都传递0,则select函数将立即返回,如果给timeout参数传递NULL,则select函数将一直阻塞,直到某个文件描述符就绪。

select函数成功时返回就绪(可读、可写、异常)文件描述符总数,如果在超时时间内没有任何文件描述符就绪,select函数将返回0,select函数失败时返回-1并设置errno,如果select函数等待期间,进程接收到信号,则select函数立即返回-1并将errno设为EINTR。

在网络编程中,以下情况认为socket可读:
1.socket内核接收缓存区中的字节数大于其低水位标记SO_RCVLOWAT,此时我们可以无阻塞地读该socket,且读操作返回的字节数大于0。

2.socket通信的对方关闭连接,此时对该socket的读操作将返回0。

3.监听socket上有新的连接请求。

4.socket上有未处理的错误,此时我们可用getsockopt函数来读取和清除该错误。

以下情况认为socket可写:
1.socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT,此时我们可以无阻塞地写该socket,且写操作返回的字节数大于0。

2.socket的写操作被关闭,对写操作被关闭的socket执行写操作将触发SIGPIPE信号。

3.socket使用非阻塞connect连接成功或失败(超时)后。

4.socket上有未处理的错误,此时我们可用getsockopt函数来读取和清除该错误。

网络程序中,select函数能处理的异常情况只有一种,socket上接收到带外数据。

socket上接收到普通数据和带外数据都将使select函数返回,以下代码同时处理这两者:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <libgen.h>int main(int argc, char *argv[]) {if (argc <= 2) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0) {printf("errno is: %d\n", errno);close(listenfd);}char buf[1024];fd_set read_fds;fd_set exception_fds;FD_ZERO(&read_fds);FD_ZERO(&exception_fds);while (1) {memset(buf, '\0', sizeof(buf));FD_SET(connfd, &read_fds);FD_SET(connfd, &exception_fds);ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);if (ret < 0) {printf("selection failure\n");break;}if (FD_ISSET(connfd, &read_fds)) {ret = recv(connfd, buf, sizeof(buf) - 1, 0);if (ret <= 0) {break;}printf("get %d bytes of normal data: %s\n", ret, buf);} else if (FD_ISSET(connfd, &exception_fds)) {ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB);if (ret <= 0) {break;}printf("get %d bytes of oob data: %s\n", ret, buf);}}close(connfd);close(listenfd);return 0;
}

poll系统调用和select系统调用类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者,poll函数原型如下:
在这里插入图片描述
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写、异常事件,pollfd结构定义如下:
在这里插入图片描述
pollfd结构的fd成员指定文件描述符;events成员告诉poll函数监听fd成员上的哪些事件,它是一系列事件的按位或;revents成员由内核修改,以通知应用进程fd成员上世纪发生了哪些事件。poll函数支持的事件类型如下:
在这里插入图片描述
在这里插入图片描述
上表中,POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND由XOPEN规范定义,它们实际上是将POLLIN事件和POLLOUT事件分得更细致,以区别对待普通数据和优先数据,但Linux并不完全支持它们。

通常,应用需要根据recv函数的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求,并做相应处理,但自Linux内核2.6.17开始,GNU为poll系统调用增加了POLLRDHUP事件,它在socket上接收到对方关闭连接的请求后触发,这为我们区分上述两种情况提供了一种更简单的方式。如果要使用POLLRDHUP事件,需要在代码最开始处定义_GNU_SOURCE。

nfds参数指定被监听事件集合fds参数数组的元素数,nfds参数类型nfds_t的定义如下:
在这里插入图片描述
timeout参数指定poll函数的超时值,单位是毫秒,当timeout参数为-1时,poll函数将永远阻塞,直到某个事件发生,当timeout参数为0时,poll函数将立即返回。

poll系统调用的返回值含义与select函数相同。

epoll函数是Linux特有的IO复用函数,它在实现和使用上与select、poll函数有很大差异,首先,epoll函数使用一组函数来完成任务,而非单个函数,其次,epoll函数把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll函数那样每次调用都要重复传入文件描述符集或事件集。epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表,这个文件描述符使用epoll_create函数来创建:
在这里插入图片描述
size参数只是给内核一个提示,告诉它事件表需要多大,该函数返回的文件描述符将用作其他所有epoll系统调用的第1个参数,以指定要访问的内核事件表。

以下函数用来操作epoll的内核事件表:
在这里插入图片描述
fd参数是要操作的文件描述符。op参数指定操作类型,操作类型有以下三种:
1.EPOLL_CTL_ADD:往事件表中注册fd上的事件。

2.EPOLL_CTL_MOD:修改fd上的注册事件。

3.EPOLL_CTL_DEL:删除fd上注册事件。

event参数指定事件,它是epoll_event结构指针类型:
在这里插入图片描述
events成员描述事件类型,epoll函数支持的事件类型和poll函数基本相同,表示epoll事件类型的宏是在poll对应的宏前加上E,如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型EPOLLET和EPOLLONESHOT,它们对于epoll的高效运作非常关键,后面再讨论它们。data成员用于存储用户数据,其类型epoll_data_t的定义如下:
在这里插入图片描述
epoll_data_t是一个联合体,其4个成员中使用最多的是fd成员,它指定要监视的文件描述符,prt成员是指向用户定义数据的指针,但由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员,因此,我们可以不使用epoll_data_t的fd成员,而在ptr成员指向的用户数据中包括fd。

epoll_ctl函数成功时返回0,失败时返回-1并设置errno。

epoll系列系统调用的主要接口是epoll_wait,它在一段超时时间内等待一组文件描述符上的事件:
在这里插入图片描述
epoll_wait函数成功时返回就绪的文件描述符个数,失败时返回-1并设置errno。

timeout参数与poll函数的timeout参数相同。maxevents参数指定最多监听多少事件,它必须大于0。

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定的)中复制到它的第二个参数events指向的数组中,这个数组只用于输出epoll_wait函数检测到的就绪事件,而不像select和poll函数的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件,这样就极大地提高了应用进程索引就绪文件描述符的效率,以下代码体现了这个差别:

// 索引poll函数返回的就绪文件描述符的过程
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
// 遍历所有已注册文件描述符并找到其中的就绪者(当然可用ret来稍做优化)
for (int i = 0; i < MAX_EVENT_NUMBER; ++i) {if (fds[i].revents & POLLIN) {    // 判断第i个文件描述符是否就绪int sockfd = fds[i].fd;// 处理socket}
}// 索引epoll返回的就绪文件描述符的过程
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
// 仅遍历就绪的ret个文件描述符
for (int i = 0; i < ret; ++i) {int sockfd = events[i].data.fd;// socketfd肯定就绪,直接处理
}

epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

对于采用LT工作模式的文件描述符,当epoll_wait函数检测到其上有事件发生并将此事件通知应用进程后,应用进程可以不立即处理该事件,这样,当应用进程下次调用epoll_wait时,epoll_wait函数还会再次向应用进程通告此事件,直到该事件被处理。而对于采用ET工作模式的文件描述符,当epoll_wait函数检测到其上有事件发生并将此事件通知应用进程后,应用进程应立即处理该事件,因为后续的epoll_wait调用将不再向应用进程通知这一事件。可见ET模式降低了同一个epoll事件被重复触发的次数,因此效率比LT模式高。以下代码体现了LT和ET在工作方式上的差异:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <libgen.h>#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10// 将文件描述符设为非阻塞的
int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 将文件描述符fd参数上的EPOLLIN注册到epollfd参数指示的内核事件表中
// 参数enable_et指定是否对fd参数启用ET模式
void addfd(int epollfd, int fd, bool enable_et) {epoll_event event;event.data.fd = fd;event.events = EPOLLIN;if (enable_et) {event.events |= EPOLLET;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}// LT模式的工作流程
void lt(epoll_event *events, int number, int epollfd, int listenfd) {char buf[BUFFER_SIZE];for (int i = 0; i < number; ++i) {int sockfd = events[i].data.fd;if (sockfd == listenfd) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);addfd(epollfd, connfd, false);} else if (events[i].events & EPOLLIN) {// 只要socket读缓存中还有未读出的数据,这段代码就被触发printf("event trigger once\n");memset(buf, '\0', BUFFER_SIZE);int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);if (ret <= 0) {close(sockfd);continue;}printf("get %d bytes of content: %s\n", ret, buf);} else {printf("something else happened\n");}}
}// ET模式的工作流程
void et(epoll_event *events, int number, int epollfd, int listenfd) {char buf[BUFFER_SIZE];for (int i = 0; i < number; ++i) {int sockfd = events[i].data.fd;if (sockfd == listenfd) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);addfd(epollfd, connfd, true);} else if (events[i].events & EPOLLIN) {// 这段代码不会被重复触发,所以需要循环读取数据printf("event trigger once\n");while (1) {memset(buf, '\0', BUFFER_SIZE);int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);if (ret < 0) {if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {printf("read later\n");break;}close(sockfd);break;} else if (ret == 0) {close(sockfd);} else {printf("get %d bytes of content: %s\n", ret, buf);}}} else {printf("something else happened\n");}}
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);assert(epollfd != -1);addfd(epollfd, listenfd, true);while (1) {int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if (ret < 0) {printf("epoll failure\n");break;}// 使用LT模式lt(events, ret, epollfd, listenfd);// 使用ET模式// et(events, ret, epollfd, listenfd);}close(listenfd);return 0;
}

可以运行以上代码,然后telnet到这个服务器上一次传输超过10字节(BUFFER_SIZE的大小)的数据,然后会发现,ET模式下事件被触发的次数比LT模式下少很多。

使用ET模式的文件描述符应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续事件而一直处于阻塞状态。

即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序中会引起问题,比如一个线程(或进程,下同)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另一个线程被唤醒来读取这些新数据,于是就出现了两个线程同时操作一个socket的局面,这不是我们所期望的,我们期望的是一个socket连接在任一时刻都只被一个线程处理,这可用EPOLLONESHOT事件实现。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写、异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件,这样,当一个线程在处理某个socket时,其他线程不可能有机会操作socket。注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下次可读时,其EPOLLIN事件能触发,从而让其他线程有机会处理这个socket。

以下代码展示了EPOLLONESHOT事件的使用:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024struct fds {int epollfd;int sockfd;
};int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 将fd参数上的EPOLLIN和EPOLLET事件注册到epollfd参数指示的内核事件表中
// 参数oneshot指定是否注册fd参数上的EPOLLONESHOT事件
void addfd(int epollfd, int fd, bool oneshot) {epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;if (oneshot) {event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}// 重置fd参数上的事件,这样操作后,可以再次触发fd参数上的事件
void reset_oneshot(int epollfd, int fd) {epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 工作线程
void *worker(void *arg) {int sockfd = ((fds *)arg)->sockfd;int epollfd = ((fds *)arg)->epollfd;printf("start new thread to receive data on fd: %d\n", sockfd);char buf[BUFFER_SIZE];memset(buf, '\0', BUFFER_SIZE);// 循环读取sockfd上的数据,直到遇到EAGAIN错误while (1) {int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);if (ret == 0) {close(sockfd);printf("foreiner closed the connection\n");break;} else if (ret < 0) {if (errno == EAGAIN) {reset_oneshot(epollfd, sockfd);printf("read later\n");break;}} else {printf("get content: %s\n", buf);// 休眠5s,模拟数据处理过程sleep(5);}}printf("end thread receiving data on fd: %d\n", sockfd);
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);assert(epollfd != -1);// 监听socket上不能注册EPOLLONESHOT事件,否则只能处理一个客户连接// 后续的连接请求将不再触发listenfd上的EPOLLIN事件addfd(epollfd, listenfd, false);while (1) {int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if (ret < 0) {printf("epoll failure\n");break;}for (int i = 0; i < ret; ++i) {int sockfd = events[i].data.fd;if (sockfd == listenfd) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address,  &client_addrlength);// 对每个非监听文件描述符都注册EPOLLONESHOT事件addfd(epollfd, connfd, true);} else if (events[i].events & EPOLLIN) {pthread_t thread;fds fds_for_new_worker;fds_for_new_worker.epollfd = epollfd;fds_for_new_worker.sockfd = sockfd;// 对每个客户请求都启动一个工作线程为其服务pthread_create(&thread, NULL, worker, (void *)&fds_for_new_worker);} else {printf("something else happened\n");}}}close(listenfd);return 0;
}

从工作线程函数worker来看,如果一个工作线程处理完某个socket上的一次请求(我们用休眠5秒来模拟此过程)之后,又接收到该socket上新的客户请求,则该线程将继续为这个socket服务,并且由于该socket上注册了EPOLLONESHOT事件,主线程中epoll_wait函数不会返回该描述符的可读事件,从而不会有其他线程读这个socket,如果工作线程等待5秒后仍没收到该socket上的下一批客户数据,则它将放弃为该socket服务,同时调用reset_oneshot来重置该socket上的注册事件,这将使epoll有机会再次检测到该socket上的EPOLLIN事件,进而使得其他线程有机会为该socket服务。

有了EPOLLONESHOT,尽管一个socket在不同时间可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

select、poll、epoll三组IO复用系统调用都能同时监听多个文件描述符,它们将等待由timeout参数指定的超时时间,直到一个或多个文件描述符上有事件发生时返回,返回值是就绪的文件描述符的数量,返回0表示没有事件发生。

这3组IO复用函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。select函数的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此select函数需要3个fd_set类型参数来分别传入和输出可读、可写、异常事件,这使得select函数不能处理更多类型的事件,另一方面,由于内核对fd_set集合的修改,应用进程下次调用select前不得不重置这3个fd_set集合。poll函数的参数类型pollfd则聪明一些,它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁地多,且内核每次修改的是pollfd结构体的revents成员,而events成员保持不变,因此下次调用poll时应用进程无需重置pollfd类型中我们关系的事件集。由于每次select和poll函数都返回整个用户注册的事件集合(包括就绪和未就绪的),所以应用索引就绪文件描述符的时间复杂度为O(n)。epoll则采用与select和poll函数不同的方式来管理用户注册的事件,它在内核中维护一个事件表,并提供一个独立的系统调用epoll_ctl来往内核事件表中添加、删除、修改事件,这样,每次epoll_wait调用都直接从内核表中取得用户注册的事件,而无须反复从用户空间读入这些事件,epoll_wait函数的events参数仅用来返回就绪的事件,这使得应用进程索引就绪文件描述符的时间复杂度达到O(1)。

poll和epoll_wait函数分别用nfds和maxevents参数指定最多监听多少文件描述符和事件,这两个数值都能达到系统允许打开的最大文件描述符数目,即65535(cat /proc/sys/fd/file-max)。而select函数允许监听的最大文件描述符数量通常有限制,虽然用户可以修改这个限制,但这可能导致不可预期的后果。

select和poll函数只能工作在相对低效的LT模式,而epoll函数能工作在高效的ET模式,且epoll函数还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写、异常事件触发的次数。

从实现原理上说,select和poll函数采用的都是轮询方式,即每次调用都要扫描整个注册文件描述符集合,因此它们检测就绪事件的算法时间复杂度是O(n)。而epoll_wait函数采用回调的方式,内核检测到就绪的文件描述符时,将触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪事件队列,然后内核在适当的时机将该就绪事件队列中的内容拷贝到用户空间,因此epoll_wait函数无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度为O(1)。
在这里插入图片描述
connect系统调用的man手册中有如下一段内容:
在这里插入图片描述
这段话描述了connect函数出错时的一种errno值(EINPROGRESS),这种错误发生在对非阻塞的socket调用connect,而连接又没有立即建立时,此时,我们可以调用select、poll等函数来监听这个正在进行连接的socket上的可写事件,当select、poll等函数返回后,再利用getsockopt函数来读取错误码并清除该socket上的错误,如果错误码是0,表示连接成功建立,否则连接建立失败。

通过非阻塞connect,我们就能同时发起多个连接并一起等待,以下代码使用非阻塞connect:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1023int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 执行非阻塞connect,ip参数是ip地址,port参数是端口号,time参数是超时时间(毫秒)
// 函数成功时返回处于连接状态的socket,失败时返回-1
int unblock_connect(const char *ip, int port, int time) {int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int sockfd = socket(PF_INET, SOCK_STREAM, 0);int fdopt = setnonblocking(sockfd);ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));if (ret == 0) {// 如果连接成功,恢复sockfd的属性,并立即返回sockfdprintf("connect with server immediately\n");fcntl(sockfd, F_SETFL, fdopt);return sockfd;} else if (errno != EINPROGRESS) {// 如果连接没有建立,只有当errno是EINPROGRESS才表示连接正在进行,否则出错返回printf("unblock connect not support\n");return -1;}fd_set readfds;fd_set writefds;struct timeval timeout;FD_ZERO(&readfds);FD_SET(sockfd, &writefds);timeout.tv_sec = time;timeout.tv_usec = 0;ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);if (ret <= 0) {// select函数超时或出错,立即返回printf("connection time out\n");close(sockfd);return -1;}if (!FD_ISSET(sockfd, &writefds)) {printf("no events on sockfd found\n");close(sockfd);return -1;}int error = 0;socklen_t length = sizeof(error);// 调用getsockopt来获取并清除sockfd上的错误if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0) {printf("get socket option failed\n");close(sockfd);return -1;}// 错误号不为0表示连接出错if (error != 0) {printf("connection failed after select with the error: %d\n", error);close(sockfd);return -1;}// 连接成功printf("connection ready after select with the socket: %d\n", sockfd);fcntl(sockfd, F_SETFL, fdopt);return sockfd;
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int sockfd = unblock_connect(ip, port, 10);if (sockfd < 0) {return 1;}close(sockfd);return 0;
}

但以上方法存在移植性问题,对于出错的socket,getsockopt函数在有些系统上(如Linux上)返回-1,而在有些系统上(如伯克利的UNIX)返回0。

像ssh这样的登录服务通常需要同时处理套接字描述符和用户输入输出描述符,这可用IO复用来实现,下面用poll函数为例实现一个简单的聊天室程序,该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两部分。客户端有两个功能:一是从标准输入终端读入用户数据,并将用户数据发送至服务器;二是往标准输出终端打印服务器发来的数据。服务器的功能是接收客户数据,并把客户数据发送给每个登录到该服务器上的客户端(数据发送者除外)。

客户端程序使用poll函数同时监听用户输入和网络连接,并利用splice函数将用户输入内容直接定向到网络连接上发送,从而实现数据零拷贝,提高了程序执行效率,客户端代码如下:

// 启用GNU扩展,其中包含一些非标准的函数和特性
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>#define BUFFER_SIZE 64int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);struct sockaddr_in server_address;bzero(&server_address, sizeof(server_address));server_address.sin_family = AF_INET;inet_pton(AF_INET, ip, &server_address.sin_addr);server_address.sin_port = htons(port);int sockfd = socket(PF_INET, SOCK_STREAM, 0);assert(sockfd >= 0);if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {printf("connection failed\n");close(sockfd);return 1;}pollfd fds[2];// 注册文件描述符0(标准输入)和文件描述符sockfd上的可读事件fds[0].fd = 0;fds[0].events = POLLIN;fds[0].revents = 0;fds[1].fd = sockfd;fds[1].events = POLLIN | POLLRDHUP;fds[1].revents = 0;char read_buf[BUFFER_SIZE];int pipefd[2];int ret = pipe(pipefd);assert(ret != -1);while (1) {ret = poll(fds, 2, -1);if (ret < 0) {printf("poll failure\n");break;}if (fds[1].revents & POLLRDHUP) {printf("server close the connection\n");break;} else if (fds[1].revents & POLLIN) {memset(read_buf, '\0', BUFFER_SIZE);recv(fds[1].fd, read_buf, BUFFER_SIZE - 1, 0);printf("%s\n", read_buf);}if (fds[0].revents & POLLIN) {// 使用splice函数将用户输入的数据直接写到sockfd上(零拷贝)splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);}}close(sockfd);return 0;
}

服务器使用poll函数同时管理监听socket和连接socket,且使用牺牲空间换取事件的策略来提高服务器性能:

#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <poll.h>// 最大用户数量
#define USER_LIMIT 5
// 读缓冲区的大小
#define BUFFER_SIZE 64
// 文件描述符数量限制
#define FD_LIMIT 65535// 客户信息:客户socket地址、待写到客户端的数据的位置、从客户端已读入的数据
struct client_data {sockaddr_in address;char *write_buf;char buf[BUFFER_SIZE];
};int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);// 分配FD_LIMIT个client_data对象,我们直接把socket的值用作索引// 这样socket和客户数据的关联比较简单client_data *users = new client_data[FD_LIMIT];// 虽然我们分配了足够多的client_data对象,但为了提高poll函数性能,仍然有必要限制用户数量pollfd fds[USER_LIMIT + 1];int user_counter = 0;// 初始化客户数据对象for (int i = 1; i <= USER_LIMIT; ++i) {fds[i].fd = -1;fds[i].events = 0;}fds[0].fd = listenfd;fds[0].events = POLLIN | POLLERR;fds[0].revents = 0;while (1) {ret = poll(fds, user_counter + 1, -1);if (ret < 0) {printf("poll failure\n");break;}for (int i = 0; i < user_counter + 1; ++i) {if ((fds[i].fd == listenfd) && (fds[i].revents & POLLIN)) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0) {printf("errno is: %d\n", errno);continue;}// 如果请求过多,则关闭新到的连接if (user_counter >= USER_LIMIT) {const char *info = "too many users\n";printf("%s", info);send(connfd, info, strlen(info), 0);close(connfd);continue;}// fds和users数组中新增连接,users[connfd]就是新连接的客户信息++user_counter;users[connfd].address = client_address;setnonblocking(connfd);fds[user_counter].fd = connfd;fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;fds[user_counter].revents = 0;printf("comes a new user, now have %d users\n", user_counter);} else if (fds[i].revents & POLLERR) {printf("get an error from %d\n", fds[i].fd);char errors[100];memset(errors, '\0', 100);socklen_t length = sizeof(errors);if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0) {printf("get socket option failed\n");}continue;} else if (fds[i].revents & POLLRDHUP) {// 如果客户端关闭连接,则服务器也关闭对应的连接,并将总用户数减1// 此处作者想把fds数组中,最后一个位置的元素放到此处正要关闭的位置// 但users数组的索引是fd,因此users数组不应做改变,此处应删除下一句代码users[fds[i].fd] = users[fds[user_counter].fd];    // delete thisclose(fds[i].fd);fds[i] = fds[user_counter];--i;--user_counter;printf("a client left\n");} else if (fds[i].revents & POLLIN) {int connfd = fds[i].fd;memset(users[connfd].buf, '\0', BUFFER_SIZE);ret = recv(connfd, users[connfd].buf, BUFFER_SIZE - 1, 0);printf("get %d bytes of client data %s from %d\n", ret, users[connfd].buf, connfd);if (ret < 0) {// 如果读出错,就关闭连接if (errno != EAGAIN) {close(connfd);// 此处关闭连接时,也不应移动users数组,因为users数组是按套接字索引的users[fds[i].fd] = users[fds[user_counter].fd];    // delete thisfds[i] = fds[user_counter];--i;--user_counter;}} else if (ret == 0) {} else {// 如果接收到客户数据,则通知其他socket连接准备写数据for (int j = 1; j <= user_counter; ++j) {// 跳过发来消息的客户if (fds[j].fd == connfd) {continue;}// 作者在干什么?可能是想关闭读,但关闭读应该是用&=// 如果关闭读,说明套接字处于写状态时不能读,感觉没必要,可以同时检测读和写fds[j].events |= ~POLLIN;fds[j].events |= POLLOUT;users[fds[j].fd].write_buf = users[connfd].buf;}}} else if (fds[i].revents & POLLOUT) {int connfd = fds[i].fd;if (!users[connfd].write_buf) {continue;}ret = send(connfd, users[connfd].write_buf, strlen(users[connfd].write_buf), 0);users[connfd].write_buf = NULL;// 写完数据后重新注册fds[i]上的可读事件,此处应使用&=fds[i].events |= ~POLLOUT;fds[i].events |= POLLIN;}}}delete[] users;close(listenfd);return 0;
}

以上讨论的服务器程序只监听一个端口,实际应用中,有些服务器程序能同时监听多个端口,如超级服务器inetd和android的调试服务adbd。

从bind系统调用的参数来看,一个socket只能与一个socket地址绑定,即一个socket只能用来监听一个端口,因此,如果服务器要同时监听多个端口,就必须创建多个socket,并将它们分别绑定到各个端口上,这样,服务器就需要同时管理多个监听socket,这可使用IO复用技术实现。另外,即使是同一个端口,如果服务器要同时处理该端口上的TCP和UDP请求,也需要创建两个不同的socket,一个是流socket,另一个是数据报socket,并将它们都绑定到该端口上。以下回射服务器能同时处理同一端口上的TCP和UDP请求:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <libgen.h>#define MAX_EVENT_NUMBER 1024
#define TCP_BUFFER_SIZE 512
#define UDP_BUFFER_SIZE 1024int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}void addfd(int epollfd, int fd) {epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);// 创建TCP socket,并将其绑定在端口port上int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);// 创建UDP socket,并将其绑定到端口port上bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int udpfd = socket(PF_INET, SOCK_DGRAM, 0);assert(udpfd >= 0);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);assert(epollfd != -1);// 注册TCP socket和UDP socket上的可读事件addfd(epollfd, listenfd);addfd(epollfd, udpfd);while (1) {int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if (number < 0) {printf("epoll failure\n");break;}for (int i = 0; i < number; ++i) {int sockfd = events[i].data.fd;if (sockfd == listenfd) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);addfd(epollfd, connfd);} else if (sockfd == udpfd) {char buf[UDP_BUFFER_SIZE];memset(buf, '\0', UDP_BUFFER_SIZE);struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);ret = recvfrom(udpfd, buf, UDP_BUFFER_SIZE - 1, 0, (struct sockaddr *)&client_address,&client_addrlength);if (ret > 0) {sendto(udpfd, buf, UDP_BUFFER_SIZE - 1, 0, (struct sockaddr *)&client_address,client_addrlength);}} else if (events[i].events & EPOLLIN) {char buf[TCP_BUFFER_SIZE];while (1) {memset(buf, '\0', TCP_BUFFER_SIZE);ret = recv(sockfd, buf, TCP_BUFFER_SIZE - 1, 0);if (ret < 0) {if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {break;}close(sockfd);break;} else if (ret == 0) {close(sockfd);} else {send(sockfd, buf, ret, 0);}}} else {printf("something else happened\n");}}}close(listenfd);return 0;
}

Linux因特网服务inetd是超级服务,它同时管理着多个子服务,即监听多个端口,现在Linux上使用的inetd服务程序通常是其升级版本xinetd,xinetd程序的原理与inetd的相同,但增加了一些控制选项,并提高了安全性。

xinetd采用/etc/xinetd.conf主配置文件和/etc/xinetd.d目录下的子配置文件来管理所有服务。主配置文件包含的是通用选项,这些选项将被所有子配置文件继承,但子配置文件可以覆盖这些选项,每一个子配置文件用于配置一个子服务的参数,如telnet子服务的配置文件/etc/xinetd.d/telnet的典型内容如下:
在这里插入图片描述
在这里插入图片描述
xinetd配置文件的内容很丰富,不止上图这些,可通过其man文档获得更多信息。

xinetd管理的子服务中有的是标准服务,如时间日期服务daytime、回射服务echo、丢弃服务discard,xinetd服务器在内部直接处理这些服务,但还有的子服务需要调用外部服务器程序来处理,xinetd通过调用fork和exec来加载运行这些服务器程序,如telnet、ftp都是需调用的外部服务器程序。下面以telnet服务为例探讨xinetd的工作流程。

首先查看xinetd守护进程的PID:
在这里插入图片描述
然后开启两个终端分别使用以下命令telnet到本机:
在这里插入图片描述
然后使用ps命令查看与进程9543相关的进程:
在这里插入图片描述
由上图可见,我们每次运行telnet登录到xinetd服务,它都创建一个子进程为该telnet客户服务,子进程运行in.telnetd程序,这是在/etc/xinetd.d/telnet配置文件中定义的。每个子进程都处于自己独立的进程组和会话中。我们可用lsof命令查看子进程都打开了哪些文件描述符:
在这里插入图片描述
上图中lsof命令的-p选项的作用是查看进程ID为9810的进程的所有打开的文件(包括普通文件、目录、设备文件和网络套接字等)。

上图省略了一些无关的输出,可见子进程9810关闭了其标准输入、标准输出、标准错误,而将socket文件描述符dup到它们上面,即telnet服务器程序将网络连接上的输入当作标准输入,并把标准输出和标准错误定向到同一个网络连接上。

对xinetd进程使用lsof命令:
在这里插入图片描述
上图说明xinetd在监听telnet连接请求,因此in.telnetld子进程只处理连接socket,而不处理监听socket,这是子配置文件中的wait参数所定义的行为。

以下是wait选项的值是no时,xinetd的工作流程:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

配置OSPF路由

OSPF路由 1.OSPF路由 1.1 OSPF简介 OSPF(Open Shortest Path First&#xff0c;开放式最短路径优先&#xff09;路由协议是另一个比较常用的路由协议之一&#xff0c;它通过路由器之间通告网络接口的状态&#xff0c;使用最短路径算法建立路由表。在生成路由表时&#xff0c;…

【通意千问】大模型GitHub开源工程学习笔记(2)--使用Qwen进行推理的示例代码解析,及transformers的库使用

使用Transformers来使用模型 如希望使用Qwen-chat进行推理,所需要写的只是如下所示的数行代码。请确保你使用的是最新代码,并指定正确的模型名称和路径,如Qwen/Qwen-7B-Chat和Qwen/Qwen-14B-Chat 这里给出了一段代码 from transformers import AutoModelForCausalLM, Aut…

机器学习笔记 - 基于强化学习的贪吃蛇玩游戏

一、关于深度强化学习 如果不了解深度强化学习的一般流程的可以考虑看一下下面的链接。因为这里的示例因为在PyTorch 之上实现深度强化学习算法。 机器学习笔记 - Deep Q-Learning算法概览深度Q学习是一种强化学习算法,它使用深度神经网络来逼近Q函数,用于确定在给定状态下采…

ROS2 中的轻量级、自动化、受控回放

一、说明 这篇文章描述了一种在 ROS2 中实现受控重播器的轻量级方法。用以测试中将现象重新播放一遍&#xff0c;以实现调参或故障定位的目的。所有源代码都可以在这里找到。该帖子也可在此处获得。 二、问题&#xff1a;不同步重播 任何曾经认真开发过 ROS2 的人都会知道这个问…

springboot和vue:八、vue快速入门

vue快速入门 新建一个html文件 导入 vue.js 的 script 脚本文件 <script src"https://unpkg.com/vuenext"></script>在页面中声明一个将要被 vue 所控制的 DOM 区域&#xff0c;既MVVM中的View <div id"app">{{ message }} </div…

uboot启动流程涉及reset汇编函数

一. uboot启动流程中函数 之前了解了uboot链接脚本文件 u-boot.lds。 从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的 _start。 本文了解 一下&#xff0c;uboot启动过程中涉及的 reset 函数。本文继上一篇文章学习&#xff0c;地址如下&#xff…

统计模型----决策树

决策树 &#xff08;1&#xff09;决策树是一种基本分类与回归方法。它的关键在于如何构建这样一棵树。决策树的建立过程中&#xff0c;使用基尼系数来评估节点的纯度和划分的效果。基尼系数是用来度量一个数据集的不确定性的指标&#xff0c;其数值越小表示数据集的纯度越高。…

揭秘:机构招生电子传单制作的五个黄金法则

机构招生微传单制作一直都是让很多人在意的事情。一款好的微传单不仅可以吸引更多的学生&#xff0c;还可以省去很多招生工作的时间和精力。但是&#xff0c;很多人却不知道如何制作一款精美的微传单。下面就让我们来学习一下如何制作一款机构招生的微传单吧。 首先&#xff0c…

Egg 封装接口返回信息

中间件封装 代码 const msgArr {"200":成功,"401":token失效 } module.exports (option, app) > {return async function(ctx, next) {try{//成功是返回的信息ctx.emit(code,data,msg)>{console.log(1111,code,data,msg)ctx.body {code,data:dat…

springboot 简单配置mongodb多数据源

准备工作&#xff1a; 本地mongodb一个创建两个数据库 student 和 student-two 所需jar包&#xff1a; # springboot基于的版本 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>&l…

C++之std::atomic解决多线程7个问题(二百四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

竞赛选题 多目标跟踪算法 实时检测 - opencv 深度学习 机器视觉

文章目录 0 前言2 先上成果3 多目标跟踪的两种方法3.1 方法13.2 方法2 4 Tracking By Detecting的跟踪过程4.1 存在的问题4.2 基于轨迹预测的跟踪方式 5 训练代码6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习多目标跟踪 …

uniapp使用scroll-into-view实现锚点定位和滚动监听功能【楼层效果 / 侧边导航联动效果】

大佬网址&#xff1a; https://blog.csdn.net/weixin_47136265/article/details/132303570 效果 代码 <template><!-- 这里面有2个bug&#xff0c;已经解决&#xff0c;需要知道的地方1.methods里的scrollEvt(e)方法里面的 this.tabIndex index ! -1 ? index :…

MySQL - DML数据增删改

功能介绍&#xff1a; DML&#xff08;Data Manipulation Language&#xff09;数据操作语言&#xff0c;用来对数据库中表的数据记录进 行增、删、改操作。 添加数据&#xff08;INSERT&#xff09; 基本语法&#xff1a;insert into 表名(字段列表) values (值列表); …

el-collapse 嵌套中 el-checkbox作为标题,选中复选框与el-tree联动

<el-drawertitle"应用授权":visible.sync"menuDrawer"><el-collapse accordion style"padding: 15px"><el-collapse-item v-for"item in platList"><template slot"title"><el-checkbox v-model…

Mysql各种锁

一.不同存储引擎支持的锁机制 Mysql数据库有多种数据存储引擎&#xff0c;Mysql中不同的存储引擎支持不同的锁机制 MyISAM和MEMORY存储引擎采用的表级锁 InnoDB存储引擎支持行级锁&#xff0c;也支持表级锁&#xff0c;默认情况下采用行级锁 二.锁类型的划分 按照数据操作…

postgresql-管理数据表

postgresql-管理数据表 创建表数据类型字段约束表级约束模式搜索路径 修改表添加字段删除字段添加约束删除约束修改字段默认值修改字段数据类型重命名字段重命名表 删除表 创建表 在 PostgreSQL 中&#xff0c;使用 CREATE TABLE 语句创建一个新表&#xff1a; CREATE TABLE …

深度学习笔记_1、定义神经网络

1、使用了PyTorch的nn.Module类来定义神经网络模型;使用nn.Linear来创建全连接层。(CPU) import torch.nn as nn import torch.nn.functional as F from torchsummary import summary# 定义神经网络模型 class Net(nn.Module):def __init__(self):super(Net, self).__init__()…

湖南软件测评公司简析:软件功能测试和非功能测试的联系和区别

一、软件功能测试   软件功能测试旨在验证软件是否按照需求规格说明书的要求正常工作。具体而言&#xff0c;功能测试会对软件的所有功能进行测试&#xff0c;以确保其满足用户的需求和预期。在进行功能测试时&#xff0c;根据需求规格说明书编写测试用例&#xff0c;并在测试…

std::initializer_list详解

std::initializer_list介绍 initializer_list是C11提供的一种新类型&#xff0c;其定义于头文件<initializer_list>中&#xff0c;此头文件是工具库的一部分&#xff0c; <initializer_list>定义如下&#xff1a; namespace std {template<class E> class…