C++ TinyWebServer项目总结(9. I/O 复用)

news2024/11/16 7:30:07

I/O 复用使得程序能够同时监听多个文件描述符,从而提高程序的性能。I/O 复用本身是阻塞的。Linux 下实现 I/O 复用的系统调用主要有 selectpollepoll

select 系统调用

select API

select系统调用:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件:

#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数:
  • nfds:指定监听的文件描述符的总数,通常是select监听的所有文件描述符中的最大值+1。(因为文件描述符从 0 开始计数)。
  • readfds, writefds, exceptfds:分别指向可读、可写、异常等事件对应文件描述符集合。应用调用select时,我们通过这3个参数传入自己感兴趣的文件描述符,当select函数返回时,内核将修改它们来通知应用进程哪些文件描述符已经就绪。
    • fd_set 结构体:
#include <typesizes.h>
#define __FD_SETSIZE 1024

#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef  __NFDBITS
#define __NFDBITS (8 * (int) sizeof(__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
	__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits);
#else
	__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits);
} fd_set;

fd_set能容纳的文件描述符数量由FD_SETSIZE指定

  • timeout:设置select函数的超时时间。
struct timeval{
	long tv_sec;	//秒数
	long tv_usec;	//微秒数
}  
返回值:

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

文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常:

下列情况socket可读

  1. socket内核接收缓冲区字节数大于等于低水位标记 SO_RCVLOWAT
  2. socket通信对方关闭连接,此时socket读操作返回0;
  3. 监听socket上有新的连接请求;
  4. socket 上有未处理错误,可用getsockopt读取和清除错误。

下列情况socket可写

  1. socket内核发送缓冲区可用字节数大于等于低水位标记 SO_SNDLOWAT
  2. socket通信写操作被关闭,对写操作关闭的socket执行写操作会触发SIGPIPE信号;
  3. socket使用非阻塞connect连接成功或失败(超时)之后;
  4. socket上有未处理错误,可用getsockopt读取和清除错误。

socket能处理的异常

  1. 只有一种情况:socket接收到带外数据。

处理带外数据

socket上接收到普通数据和带外数据都将使select函数返回,但 socket 处于不同的就绪状态:前者处于可读状态,后者处于异常状态。

下面的代码清单描述了 select 如何同时处理二者:

/*
#include ...
*/

int main(int argc, char *argv[]) {

    // 省略其他操作
    
    while (1) {
        memset(buf, '\0', sizeof(buf));

        // 设置fdset的位fd
        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;
        }

        // 可读事件,采用普通的recv函数读取数据
        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);
        } 
        // 异常事件,采用带MSG_OOB标志的recv函数读取带外数据
        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;
}

recv 调用的描述见:TCP 数据读写

poll 系统调用

在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数:
  • fds:指定所有我们感兴趣的文件描述符上发生的可读、可写、异常事件。
struct pollfd{
    int fd;			// 文件描述符
    short events;	// events成员告诉poll函数监听fd成员上的哪些事件,它是一系列事件的按位或
    short revents;	// 由内核修改,以通知应用程序fd上实际发生了哪些事件。
};
  • nfds:指定被监听事件集合fds参数数组的大小
typedef unsigned long int nfds_t
  • timeout:指定 poll 的超时值,单位是 ms。timeout == -1poll调用会一直阻塞,直到某个事件发生;当timeout == 0poll调用立刻返回。

poll 支持的事件类型:

自Linux内核2.6.17开始,GNU为poll系统调用增加了POLLRDHUP事件,它在socket上接收到对方关闭连接的请求后触发。

返回值:
  • 与 select 相同。

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

epoll 系列系统调用

内核事件表

epoll函数是Linux特有的I/O复用函数,它在实现和使用上与selectpoll函数有很大差异:

  • epoll函数使用一组函数来完成任务,而非单个函数;
  • epoll函数把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像selectpoll函数那样每次调用都要重复传入文件描述符集或事件集;
  • epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表,这个文件描述符使用epoll_create函数来创建:
#include <sys/epoll.h>
int epoll_create(int size);

该函数返回的文件描述符将用作其他所有epoll系统调用的第1个参数,以指定要访问的内核事件表。

操作内核事件表 epoll_ctl

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
  • fd:要操作的文件描述符;
  • op:指定操作类型。

操作类型有如下三种:

    • EPOLL_CTL_ADD:往事件表中注册fd上的事件。
    • EPOLL_CTL_MOD:修改fd上的注册事件。
    • EPOLL_CTL_DEL:删除fd上注册事件。
  • event:指定事件。
struct epoll_event{
	__uin32_t events;	// epoll事件
	epoll_data_t data;	// 用户数据
};

events 成员描述事件类型,epoll函数支持的事件类型和poll函数基本相同,表示epoll事件类型的宏是在poll对应的宏前加上E,如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型EPOLLETEPOLLONESHOTepoll_data_t定义如下:

typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

epoll_data_t是一个联合体,其4个成员中使用最多的是fd成员,它指定事件所从属的文件描述符。prt成员是指向用户定义数据的指针,但由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员。

返回值:

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

epoll_wait 函数

epoll系列系统调用的主要接口是epoll_wait,它在一段超时时间内等待一组文件描述符上的事件:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数:
  • timeout:设置epoll_wait函数的超时时间;
  • maxevents:指定最多监听多少个事件,必须大于 0;
  • eventsepoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中,这个数组只用于输出epoll_wait函数检测到的就绪事件,而不像selectpoll函数的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件,这样就极大地提高了应用进程索引就绪文件描述符的效率。

例如,我们要索引 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肯定就绪,直接处理
}
返回值:

成功时返回就绪的文件描述符个数,失败时返回-1并设置errno

LT 模式和 ET 模式

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在工作方式上的差异(这里我只给出了主要的代码段,想了解完整代码的请参阅 《Linux 高性能服务器编程》第九章 P154-157):

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");
        }
    }
}

ET模式下事件被触发的次数比LT模式下少很多。

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

EPOLLONESHOT 事件

即使我们使用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 1024

struct 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在不同时间可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

三组 I/O 复用函数的比较

事件集

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

select函数的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此select函数需要3个fd_set类型参数来分别传入和输出可读、可写、异常事件,这使得select函数不能处理更多类型的事件,另一方面,由于内核对fd_set集合的修改,应用进程下次调用select前不得不重置这3个fd_set集合。

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd{
    int fd;			// 文件描述符
    short events;	// events成员告诉poll函数监听fd成员上的哪些事件,它是一系列事件的按位或
    short revents;	// 由内核修改,以通知应用程序fd上实际发生了哪些事件。
};

poll函数的参数类型pollfd把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁地多,且内核每次修改的是pollfd结构体的revents成员,而events成员保持不变,因此下次调用poll时应用进程无需重置pollfd类型的事件集参数。

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

由于每次selectpoll函数都返回整个用户注册的事件集合(包括就绪和未就绪的),所以应用索引就绪文件描述符的时间复杂度为O(n)epoll则采用与selectpoll函数不同的方式来管理用户注册的事件,它在内核中维护一个事件表,并提供一个独立的系统调用epoll_ctl来往内核事件表中添加、删除、修改事件,这样,每次epoll_wait调用都直接从内核表中取得用户注册的事件,而无须反复从用户空间读入这些事件,epoll_wait函数的events参数仅用来返回就绪的事件,这使得应用进程索引就绪文件描述符的时间复杂度达到O(1)

最大支持的文件描述符数

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

工作模式

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

具体实现

从实现原理上说,selectpoll函数采用的都是轮询方式,即每次调用都要扫描整个注册文件描述符集合,因此它们检测就绪事件的算法时间复杂度是O(n)

epoll_wait函数采用回调的方式,内核检测到就绪的文件描述符时,将触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪事件队列,然后内核在适当的时机将该就绪事件队列中的内容拷贝到用户空间,因此epoll_wait函数无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度为O(1)

当活动连接比较多时,epoll_wait 的回调函数被触发的过于频繁,会导致 epoll_wait 的效率反而降低,因此 epoll_wait 适用于连接数量多,但是活动连接较少的情况。

I/O 复用的高级应用一:非阻塞 connect

connect系统调用的man手册中有如下一段内容:

这段话描述了connect函数出错时的一种errno值(EINPROGRESS),这种错误发生在对非阻塞的socket调用connect,而连接又没有立即建立时,此时,我们可以调用selectpoll等函数来监听这个连接失败的socket上的可写事件,当selectpoll等函数返回后,再利用getsockopt函数来读取错误码并清除该socket上的错误,如果错误码是0,表示连接成功建立,否则连接建立失败。

通过非阻塞connect,我们就能同时发起多个连接并一起等待。但是这种方法存在几处移植性问题。

I/O 复用的高级应用二:聊天室程序

像ssh这样的登录服务通常需要同时处理套接字描述符和用户输入输出描述符,这可用I/O复用来实现,下面用poll函数为例实现一个简单的聊天室程序,该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两部分。

客户端有两个功能

  • 一是从标准输入终端读入用户数据,并将用户数据发送至服务器;
  • 二是往标准输出终端打印服务器发来的数据。

服务器的功能是接收客户数据,并把客户数据发送给每个登录到该服务器上的客户端(数据发送者除外)。

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

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

I/O 复用的高级应用三:同时处理 TCP 和 UDP 服务

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

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

超级服务 xinetd

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

参考文章

  1. Linux高性能服务器编程 学习笔记 第九章 IO复用-CSDN博客
  2. Linux高性能服务器编程-游双——第九章 I/O复用_游双tcip-CSDN博客

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

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

相关文章

Java语言程序设计——篇十七(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

每日OJ_牛客_美国节日(日期模拟)

目录 牛客_美国节日&#xff08;日期模拟&#xff09; 解析代码 牛客_美国节日&#xff08;日期模拟&#xff09; 美国节日__牛客网 解析代码 题目表述很明白&#xff0c;难点在于要求一个月第N个星期W。那么面对这个问题&#xff0c;拆解的思路是&#xff0c;首先&#xff…

Navicat for MySQL:卓越的跨平台数据库管理开发工具

Navicat for MySQL是一款专为数据库管理员和开发人员设计的强大数据库管理开发工具&#xff0c;支持Mac和Windows操作系统&#xff0c;为用户提供了高效、便捷的数据库操作体验。无论是管理MySQL还是MariaDB数据库&#xff0c;Navicat for MySQL都能轻松胜任。 一、直观易用的…

DB-GPT开源项目文档入门

DB-GPT开源项目文档入门 (qq.com) 场景&#xff1a;服务中小金融机构、服务业小微商家 DB-GPT项目集成了多模型管理、多数据源管理、Text2SQL、增强检索RAG、生成式BI、多智能体&#xff0c;一个大而全的开源框架 项目基本信息 简介&#xff1a;一个原生数据应用开发框架 …

<数据集>流水线纸箱识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;1395张 标注数量(xml文件个数)&#xff1a;1395 标注数量(txt文件个数)&#xff1a;1395 标注类别数&#xff1a;2 标注类别名称&#xff1a;[GreenCarton,RedCarton] 序号类别名称图片数框数1GreenBox131728482R…

《计算机操作系统》(第4版)第9章 操作系统接口 复习笔记

第9章 操作系统接口 一、用户接口 1. 字符显示式联机用户接口 (1)命令行方式 该方式是以行为单位&#xff0c;输入和显示不同的命令。每行长度一般不超过256个字符&#xff0c;一般情况下&#xff0c;以回车符作 为一个命令的结束标记。通常&#xff0c;命令的执行采用的是间断…

抖音收回“作品代发布”能力?短视频矩阵工具未来何去何从?

相信不少朋友都看到过抖音开放平台的公示&#xff0c;从2024年7月20日开始&#xff0c;官方将要收回“代替用户发布内容到抖音”能力&#xff0c;如果你还在用原有抖音开放平台接口的矩阵系统&#xff0c;那可要注意及时调整了。 那未来的短视频矩阵工具还能用吗&#xff1f;矩…

数学建模~~描述性分析---RFM用户分层模型聚类

目录 1.RFM用户分层模型介绍 2.获取数据&#xff0c;标准化处理 2.1获取数据 2.2时间类型转换 2.3计算时间间隔 3.对于R,F,M的描述性分析 3.1代码分析 3.2分析结果说明 3.3对于F的描述性分析 3.4对于M的描述性分析 4.数据分箱--等级划分 4.1分箱概念 4.3分箱特点 …

区间合并+并查集

前言&#xff1a;写完这个题目的时候没意识到这个和区间合并是等价的&#xff0c;现在看起来确实是一个区间合并的题目 &#xff08;注意这个多米诺骨牌是可以连续推倒的&#xff09; #include<bits/stdc.h> using namespace std;#define int long long int t; int n,m; …

转发和重定向的区别

转发和重定向的区别 转发是一次请求。因此浏览器地址栏上的地址不会发生变化。 重定向是两次请求。因此浏览器地址栏上的地址会发生变化 转发的代码实现&#xff1a;request.getRequestDispatcher(“/index”).forward(request,response); 重定向的代码实现&#xff1a;resp…

学习 Java 和数据库:从前端到全栈的进阶之路

作为一名前端开发者&#xff0c;掌握 Java 和数据库知识不仅能提升你的技术水平&#xff0c;还能让你在全栈开发的道路上走得更远&#xff08;主要是涨薪&#xff09;。本文将为你提供一个详尽的学习大纲&#xff0c;帮助你从零开始学习 Java 和数据库&#xff0c;并解释为什么…

Windows离线安装openSSH服务实现远程访问

1、问题概述? 在企业的实际的开发环境中,我们的计算机可能是没有网络的,这种时候我们安装openSSH就需要通过离线的方式安装。 1.1、下载openSSH离线包 离线下载地址:https://github.com/PowerShell/Win32-OpenSSH/releases msi格式可以在windows中直接安装 下载之后直接…

机器人拾取系统关节机械臂通过NY-PN-EIPZ进行命令控制

关节机械臂是一种精密的机器&#xff0c;旨在模拟人类手臂在订单拣选操作中的运动。这些多功能机器人由多个关节组成&#xff0c;通常有 4 到 7 个轴&#xff0c;使它们能够高度自由地移动&#xff0c;并在仓库内以各种方向和位置接触物品。 制造工厂智能仓库系统中的关节机械臂…

centos7.9系统安装cloudpods(一)

1. 简介&#xff1a; Cloudpods 是一款简单、可靠的企业IaaS资源管理软件。帮助未云化企业全面云化IDC物理资源&#xff0c;提升企业IT管理效率。 Cloudpods 帮助客户在一个地方管理所有云计算资源。统一管理异构IT基础设施资源&#xff0c;极大简化多云架构复杂度和难度&…

Datawhale X 李宏毅苹果书 AI夏令营(深度学习进阶)taks1

深度学习的基础 常见的临界点的种类为局部最小值和鞍点 1.局部最小值 对于任何目标函数f(x)&#xff0c;如果在x处对应的f(x)值小于在x附近任意其他点的f(x)值&#xff0c;那么f(x)可能是局部最小值。如果f(x)在x处的值是整个域中目标函数的最小值&#xff0c;那么f(x)是全局…

如何将LaTeX数学公式嵌入到PowerPoint中

如何将LaTeX数学公式嵌入到PowerPoint中 简介 在学术演示或技术报告中&#xff0c;清晰且专业地展示数学公式是至关重要的。PowerPoint虽然提供了一些基本的公式编辑功能&#xff0c;但如果你需要更复杂或格式严格的公式&#xff0c;使用LaTeX生成公式并嵌入到PPT中是一个极佳…

huggingface下载model

0x00 背景 服务器的普通用户&#xff0c;不具有root权限服务器网络不稳定 0x01 解决办法 使用镜像【HF-Mirror】提供的工具hfd 方法三&#xff1a;使用 hfd hfd 是本站开发的 huggingface 专用下载工具&#xff0c;基于成熟工具 gitaria2&#xff0c;可以做到稳定下载不断…

安卓中 onClick(View v) 方法在主线程还是子线程运行

在Android开发中&#xff0c;onClick(View v) 方法通常是在主线程&#xff08;也称为UI线程&#xff09;中运行的。这个方法是在用户与界面元素&#xff08;如按钮&#xff09;进行交互时&#xff0c;由系统框架自动调用的。由于Android的UI操作必须是线程安全的&#xff0c;并…

MySQL 数据库深度解析:安装、语法与高级查询实战

一、引言 在现代软件开发和数据管理领域中&#xff0c;MySQL 数据库凭借其高效性、稳定性、开源性以及广泛的适用性&#xff0c;成为了众多开发者和企业的首选。无论是小型项目还是大型企业级应用&#xff0c;MySQL 都能提供可靠的数据存储和管理解决方案。本文将深入探讨 MyS…

QT Creator UI中文输入跳出英文

笔者用的是QQ拼音输入&#xff0c;发现只要在UI中加入了QTableWidget&#xff0c;输入多几次中文&#xff0c;就会跳入英文。 后面改用搜狗拼音稍微好一些&#xff0c;但是偶尔还是插入了空格。