简易TCP服务器通信、IO多路复用(select、poll、epoll)以及reactor模式。

网络编程学习

  • 简单TCP服务器通信
    • TCP三次握手和四次挥手
      • 三次握手(如下图)
        • 常见问题?
      • 四次挥手
    • client和server通信写法
      • server端
      • client端
    • 通信双方建立连接到断开连接的状态转换
    • 怎么应对多用户连接?
      • 缺点
  • IO多路复用
    • select
      • 优缺点
    • poll
      • poll写法和改进点
    • epoll(使用最多,重中之重)
      • epoll写法和改进点
      • LT模式和ET模式
        • **LT模式**
        • **ET模式**:
        • LT模式如何解决数据保存问题?
  • reactor模式
    • 针对IO处理的两种写法
  • 思考题
    • main函数如何被执行的?
    • time_wait怎么产生的?如果有大量的time_wait是为什么?
    • close_wait怎么产生的?如果有大量的close_wait是为什么?
    • epoll里是否使用了mmap?
    • epoll是否是线程安全的?

简单TCP服务器通信

TCP三次握手和四次挥手

三次握手(如下图)

  1. 首先由通信双方的某一方(后面就用client了,其实server和client都是可以的)发起连接请求,即发送一个带SYN标志位的TCP数据包给server端。
  2. server端收到数据包后,除了SYN位以外,还会增加一个ACK确认位,然后发送给clinet端。
  3. client端收到数据包后,给server端回复一个带ACK确认位的数据包给服务端,到此,通信双方就算建立连接了。
常见问题?
  • 为什么是三次握手而不是两次?
    • 因为TCP连接是安全可靠的,三次握手的话会保证双方都能接受到对方的数据,第一次服务端收到客户端消息后可以确定客户端发送数据没问题,然后第二次服务端发送给客户端消息,客户端可以确定服务端的收发都没有问题,但是服务端不知道客户端接收是否成功,所以,客户端还需要发一个数据包告诉服务端,它的接收也没有问题,因此,这里是三次连接而不是两次。

四次挥手

  1. 第一次挥手,是主动断开连接的一方,发送带有FIN和ACK标志位的TCP包。
  2. 第二次是被动断开连接一方在收到数据包后,立马回应一个ACK标志位的TCP包,告诉对方我收到了对方的包,但是数据还没有处理完成,还有些工作要处理。
  3. 第三次依然是被动断开连接一方在数据处理完成之后,会给对方发送FIN和ACK标志位的TCP包,告诉对方我数据处理完了,你可以关闭连接了。
  4. 第四次挥手,是主动断开方发送ACK标志位的TCP包,然后被动断开方就关闭连接了。

client和server通信写法

server端

  • 服务端编码步骤:
    1. 调用socket函数,获取一个sockfd(本质上是一个文件描述符,受到系统能打开文件数量上限限制),int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    2. 调用bind函数,将sockfd和ip、端口号绑定
      1. 创建一个sockaddr_in结构体变量,通过改结构体设置服务器的IP地址和端口号(这里要调用htonl和htons转换字节序)。
      2. 调用bind函数,绑定sockfd和ip、端口号;bind(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))
    3. 调用listen函数去监听sockfd上的请求。listen(sockfd, 8)
    4. 调用accept函数(默认为堵塞)去接收客户端请求。
      1. 创建两个变量,一个是sockaddr_in结构体变量,存储用来通信的客户端信息,一个是len代表结构体变量的大小。
      2. 调用accept函数,函数返回一个fd(可以用来读写数据的fd),int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
    5. 第五步就是一个while循环,然后循环里调用recv和send函数去读写数据。
  • 整体代码如下:
       int sockfd = socket(AF_INET, SOCK_STREAM, 0);
       struct sockaddr_in server_addr;
       memset(&server_addr, 0, sizeof(struct sockaddr_in));//赋值为空
       server_addr.sin_family = AF_INET;//IPV4协议
       server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址,如果有多个IP不知道设置为哪个就可以用这个参数。
       server_addr.sin_port = htons(2204);
       if (-1 == bind(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))) {
           perror("bind error");
           return -1;
       }
       if (-1 == listen(sockfd, 8)) {
           perror("listen error");
           return -1;
       }
       //accept
       struct sockaddr_in client_addr;
       int len = sizeof(struct sockaddr);
       //clientfd用于收发数据的
       int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
       printf("clientfd: %d, serverfd: %d\n", clientfd, sockfd);
       while (1) {
           //receive
           char buf[128] = {0};
           int recv_len = recv(clientfd, buf, 128, 0);
           printf("accept len: %d", recv_len);
           if (recv_len == -1){
               perror("recv error");
               break;
           } else if (recv_len == 0) {
               break;
           } else {
               printf("recv success\n");
           }
           //send
           printf("send");
           send(clientfd, buf, recv_len, 0);
           printf("send success");
       }
    

client端

client写法步骤:
1. 调用socket函数,获取一个sockfd(本质上是一个文件描述符),int sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. 调用connect函数建立连接(这一步完成三次握手)
1. 创建一个sockaddr_in结构体变量,通过改结构体设置服务器的IP地址和端口号(这里要调用htonl和htons转换字节序)。
2. 调用connect函数,如果成功返回那么就可以直接用sockfd进行通信了。
3. 使用sockfd进行数据读写。

  • 整体代码如下:
    uint32_t lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {
        perror("create socket");
        exit(-1);
    }
    //connect 172.17.71.122
    uint32_t dst = 0;
    inet_pton(AF_INET, "8.141.4.79", &dst);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;//IPV4协议
    server_addr.sin_addr.s_addr = dst;
    server_addr.sin_port = htons(2204);
    if (connect(lfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        exit(-1);
    }
    while (1) {
        for (int i = 0; i < 20; ++i) {
            const char* str = std::to_string(i).c_str();
            int write_fd = write(lfd, str, sizeof(str));
            char receive[1024] = "";
            int len1 = read(lfd, receive, sizeof(receive));
            if (len1 < 0) {
                perror("client read");
                exit(-1);
            } else if (len1 == 0) {
                printf("server closed!!!");
                break;
            } else {
                printf(" client read success: %s\n", receive);
            } 
            sleep(1);
        }
        sleep(2);
    }

通信双方建立连接到断开连接的状态转换

在这里插入图片描述

  • 建立连接:
    • 服务端:
      1. 初始状态为closed状态,调用listen函数后进入LISTEN状态。
      2. 收到SYN标志位的TCP包,并向对方发送SYN和ACK标志位的TCP包后,变为SYN_RCVD状态。
      3. 收到对方带ACK标志位的包后变为ESTABLISTED状态,至此连接建立。(这里不是accept去完成三次握手的,三次握手由TCP协议栈完成,accept只是堵塞获取客户端信息的)。
    • 客户端:
      1. 初始状态为closed状态,调用connect函数,发送SYN标志位的TCP包后进入SYN_SENT状态。
      2. 由TCP协议栈完成后续的握手环节,当connect函数成功返回后,直接进入ESTABLISTED状态。
  • 断开连接:断开连接一般情况下可以是任意一方发起,这里以客户端主动断开为例
    • 服务端:
      1. 当客户端主动断开连接后,服务端收到FIN位的TCP包,此时服务端的协议栈会立刻发送给对方一个ACK确认位,此时服务端酒进入了CLOSED_WAIT状态。
      2. 此时recv函数会继续去处理数据,当读到数据长度为0,并且数据全部处理完后,服务端主动调用close函数关闭通信fd,这时服务端会给客户端发送FIN标志位的TCP包,然后服务端进入LAST_ACK状态。
      3. 当服务端收到客户端的ACK标志位的包后,服务端由LAST_ACK转变为CLOSED状态。
    • 客户端:
      1. 客户端主动调用closed函数关闭fd,那么TCP协议栈会给服务端发送一个带SYN标志位的TCP包,此后客户端进入FIN_WAIT1状态。
      2. 当收到服务端带ACK标志位的TCP包后,由FIN_WAIT1转变为FIN_WAIT2状态。
      3. 当收到服务端带FIN标志位的TCP包,并向服务端发送ACK标志位的TCP包后就进入了TIME_WAIT状态,该状态会持续2MSL,(MSL为网络包在网络中的最大存活时间。),当2MSL到达后,转换为CLOSED状态。
  • 注意:
    • TCP的状态转换是由TCP协议栈管理的,不是由代码管理。

怎么应对多用户连接?

  • 上面的简单通信过程只能一次处理一个用户,我们如果想要处理多用户连接可以采用多线程的方式。
  • 方法也比较简单,就是将IO操作以及业务处理逻辑放到一个函数里,然后accept只要返回了,那么就创建一个线程去执行回调函数。

缺点

  1. 当并发量特别大的时候,服务器会受到内存的影响,性能并不高,无法适应高并发场景。
  2. 为此,我们需要学习下面的IO多路复用,采用一个线程去处理多个请求。

IO多路复用

  • 简单来讲就是可以用一个线程去同时监听多个请求,并一起返回,同时处理,减少线程资源消耗。

select

  • select相当于一个代理,去帮我们检测有哪些fd有事件发生,然后返回给我们一个数组,我们需要遍历数组依次处理每个fd。

  • 实现步骤如下:

    1. 定义一个fd_set变量和maxfd变量。
      1. fd_set变量是一个结构体变量,内部存储了一个fd数组,然后我们调用FD_SET函数将sockfd添加到fd_set里。
      2. maxfd变量存储当前的最大fd值,初始值为sockfd,后续会及时更新。
    2. 调用select函数去监听事件。int ret_code = select(maxfd + 1, &r_set, NULL, NULL, NULL); 这里maxfd+1的目的是为了能够处理maxfd,因为select的实现比较老了。
    3. select函数返回后,我们需要写一个状态机,去判断是sockfd还是其他fd也有。两种不同的fd,会有不同的IO操作。
  • 代码如下:

    fd_set rf_set, r_set;
    FD_ZERO(&rf_set);//清空集合
    FD_SET(sockfd, &rf_set);//增加sockfd到集合
    
    int maxfd = sockfd;//设置最大fd
    while (1) {
        r_set = rf_set;
        int ret_code = select(maxfd + 1, &r_set, NULL, NULL, NULL);
    
        if (FD_ISSET(sockfd, &r_set)) {
            //accept
            struct sockaddr_in client_addr;
            int len = sizeof(struct sockaddr);
            int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
            FD_SET(client_fd, &rf_set);//增加新的fd到fd_set里
            maxfd = maxfd > client_fd ? maxfd : client_fd;//更新maxfd
        } else {
            for (int i = sockfd + 1; i < maxfd + 1; ++i) {
                if (FD_ISSET(i, &r_set)) {
                    //receive
                    char buf[128] = {0};
                    int recv_len = recv(i, buf, 128, 0);
                    printf("accept len: %d", recv_len);
                    if (recv_len == -1){
                        perror("recv error");
                        break;
                    } else if (recv_len == 0) {
                        FD_CLR(i, &rf_set);
                        close(i);//记得close,不然会一直在close_wait状态。
                        break;
                    } else {
                        printf("recv success\n");
                    }
                    //send
                    printf("send");
                    send(i, buf, recv_len, 0);
                    printf("send success");
                }
            }
        }
    }
    

优缺点

  • 优点:相较于多线程实现法,进行了极大的优化,减少线程资源消耗。
  • 缺点:
    1. 函数参数太多了,一共有五个参数,中间三个参数分别为r_set、w_set、e_set分别对应不同的事件。
    2. 使用select会频繁的进行内存拷贝,每当处理完数据就会将fd_set拷贝到内核态,而每次有事件发生都会将fd_set拷贝到用户态,频繁拷贝,浪费很多资源,性能很低。
    3. select能够处理的最大连接数为1024个。

poll

poll写法和改进点

  • 改进点:

    1. 优化了函数参数,将中间三个参数变为一个,增加了一个结构体,用来存储监听哪些事件(读写)、是否监听以及fd。用户定义一个数组,作为传出参数,传给poll函数,等函数返回后,用户需要遍历数组,对于数组每个元素,如果revents被改变,那么就是发生的对应的事件,用户可以进行相应的操作。
    2. 因为用户可以自定义数组大小,所以poll解决了select只能监听1024个文件描述符的问题。
  • 缺点:

    • 底层的内核监听过程和select类似,也需要把数组从用户态拷贝到内核态,只是把修改fdset换成了每个数组元素中revents的值,其他相差不大。
  • 写法代码如下:

    // struct pollfd {
    //     int fd;			/* File descriptor to poll.  */
    //     short int events;		/* Types of events poller cares about.  */
    //     short int revents;		/* Types of events that actually occurred.  */
    // };
    struct pollfd fds[1024];
    fds[sockfd].fd = sockfd;
    fds[sockfd].events = POLLIN;
    int maxfd = sockfd;
    while (1) {
        int ret_code = poll(fds, maxfd + 1, -1);
    
        if (fds[sockfd].revents & POLLIN) {
            struct sockaddr_in client_addr;
            int len = sizeof(struct sockaddr);
            int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
            fds[client_fd].fd = client_fd;
            fds[client_fd].events = POLLIN;
            maxfd = maxfd > client_fd ? maxfd : client_fd;
        } 
        for (int i = sockfd + 1; i < maxfd + 1; ++i) {
            if (fds[i].revents & POLLIN) {
                //receive
                char buf[128] = {0};
                int recv_len = recv(i, buf, 128, 0);
                printf("accept len: %d", recv_len);
                if (recv_len == -1){
                    perror("recv error");
                    break;
                } else if (recv_len == 0) {
                    fds[i].events = 0;
                    fds[i].fd = -1;
                    close(i);
                    break;
                } else {
                    printf("recv success\n");
                }
                //send
                printf("send");
                send(i, buf, recv_len, 0);
                printf("send success");
            }
        }
    }
    

epoll(使用最多,重中之重)

epoll写法和改进点

  • 改进点:

    1. epoll是在内核中申请一块缓存,存放一个节点,里面包含一个红黑树和双向链表,采用红黑树存储要监听的fd,然后将监听到的fd存储到一个双向链表里,这样加大了内核的查询效率,以及用户拿到的是被触发的结果集,不需要再去遍历所有的fd了,也就是不需要用户自己维护maxfd了。
    2. 虽然epoll也定义了一个数组,但是epoll的写法采用的共享内存的方式,而select和poll是单纯的拷贝。
  • 缺点:

    • 当服务遇到大量的短连接时,就会频繁的调用epoll_ctl系统调用,消耗系统资源。
  • 写法步骤:

    1. 调用epoll_create系统调用会在内核为该进程创建一个句柄,其中包含红黑树根节点和一个双向链表分别存储需要监听的节点和准备就绪的fd。
    2. 调用epoll_ctl系统调用,向红黑树节点中添加、删除、修改想要操作的fd,起初肯定是sockfd,并且注册一个回调函数,告诉内核如果有准备就绪的节点就添加到双向链表中。
    3. 调用epoll_wait系统调用,由内核去检测双向链表是否为空,如果为空,就sleep,直到链表里有数据时,epoll_wait被唤醒,将数据返回给用户,当然用户也可以自己设置超时时间,在该时间到达后,即使没有数据也会返回告知用户。
  • 代码如下:

    /*struct epoll_event {
            uint32_t events;	
            epoll_data_t data;	
        } __EPOLL_PACKED;
        */
        int epoll_fd = epoll_create(1);//这里只要大于0就可以,内部实现改为链表了。
    
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = sockfd;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
    
        struct epoll_event epoll_events[1024] = {};
    
        while (1) {
            int ret_code = epoll_wait(epoll_fd, epoll_events, 1024, -1);
    
            for (int i = 0; i < ret_code; ++i) {
                int connt_fd = epoll_events[i].data.fd;
                if (connt_fd == sockfd) {
                    struct sockaddr_in client_addr;
                    int len = sizeof(struct sockaddr);
                    int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
                    printf("clientfd: %d\n", client_fd);
                } else if (epoll_events[i].events & EPOLLIN){
                    //receive
                    char buf[10] = {0};
                    int recv_len = recv(connt_fd, buf, 10, 0);
                    printf("accept len: %d", recv_len);
                    if (recv_len == -1){
                        perror("recv error");
                        close(connt_fd);
                        continue;
                    } else if (recv_len == 0) {
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL);
                        close(i);
                        continue;
                    } else {
                        printf("recv success\n");
                    }
                    //send
                    printf("send");
                    send(connt_fd, buf, recv_len, 0);
                    printf("send success");
                }
            }
        }
    

LT模式和ET模式

LT模式
  • 是什么?
    • 我们在处理数据的时候,会有一个存储数据的buffer数组,如果buffer数组的长度小于要读取的数据长度,那么recv函数只会读取buffer长度的数据,然后等下次epoll_wait返回后,再继续读取数据,但是上次的数据需要保存,不然就丢失了。
  • 应用场景:
    • 可以采用LT模式解决粘包问题,先读取数据的长度,然后循环把数据全部读取出来。
ET模式
  • 是什么?
    • 内核针对每一个fd只返回一次(也就是说针对每次事件,epoll_wait只返回一次),后续就不再对该fd进行返回,这就使得用户必须一次性读完,否则只能等到下次fd有事件触发的时候才能接着读取上次的数据。
  • 应用场景:
    • 当处理大文件的时候,一次性读取所有数据。因为如果采用LT模式的话,文件太大会针对这个fd频繁调用epoll_wait系统调用。
LT模式如何解决数据保存问题?
  • 直接看代码(定义一个全局变量,存储每个fd对应的buffer,并且记录上次读到的位置idx):
    struct fd_events    
    {
        int fd;
        char r_buffer[128];
        char w_buffer[128];
        int r_idx;
        int w_idx;
        //这里还可以添加事件以及回调函数。
    };
    
    //解决LT模式下无法存数据的问题,每次读完后拼接到buffer后,并修改idx到数组末尾
    struct fd_events all_events[1024] = {};
    

reactor模式

针对IO处理的两种写法

  • 针对IO状态机处理
    • 上面的epoll示例就是针对fd去处理的,对sockfd和其他fd分别处理。
  • 针对事件状态机划分
    • 因为epoll_event里的events是有不同的事件的,我们可以针对不同的事件进行状态机判断,然后分别调用不同的回调函数。
    • 代码如下:
         if (epoll_events[i].events & EPOLLIN) {
             //xxx
         } else if (epoll_events[i].events & EPOLLOUT) {
      	   //xxx
         } else if (epoll_events[i].events & EPOLLERR) {
      	   //xxx
         }
      
  • reactor模式就是针对不同事件调用不同的回调函数的模式。

思考题

main函数如何被执行的?

  • 程序启动时,操作系统会创建一个进程,并为该进程分配一块内存空间,然后将可执行文件加载到内存中,然后操作系统会进行一些初始化准备工作,之后找到程序执行入口点main函数的地址,将控制权交给main函数的代码逻辑,程序开始执行,程序执行结束后,将控制权返回给操作系统,操作系统根据退出码来判断程序的运行状态。

time_wait怎么产生的?如果有大量的time_wait是为什么?

  • 首先time_wait状态是先断开连接的一方会产生的状态,四次挥手时,当最后一次挥手发送完后,就进入了time_wait状态。
  • 如果有大量的time_wait的原因是什么?
    1. 目前感觉是因为大量的短连接,并且是服务端先主动调用close函数造成的。

close_wait怎么产生的?如果有大量的close_wait是为什么?

  • close_wait是在四次挥手中收到对方的断开连接请求,并且向对方发送ack确认后就进入了close_wait状态,是被断开一方产生的状态。
  • 如果有大量的close_wait是为什么?
    1. 忘记调用close函数,没有去发送finTCP包就会一直处于close_wait状态。
    2. 代码逻辑有问题,可能跳过了close的执行。

epoll里是否使用了mmap?

  • epoll使用的共享内存的方式实现,否则和select、poll一样还是需要进行大量的数据拷贝。

epoll是否是线程安全的?

  • 首先epoll本身是linux下对IO事件进行监听的机制,可以用来处理高并发连接,如果是多进程使用一个epoll_fd实例,那么需要一定的线程同步机制来保证数据一致性。如果是每个线程对应不同的实例,则是线程安全的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

物联网学习2、MQTT 发布/订阅模式介绍

MQTT 发布/订阅模式 发布订阅模式&#xff08;Publish-Subscribe Pattern&#xff09;是一种消息传递模式&#xff0c;它将发送消息的客户端&#xff08;发布者&#xff09;与接收消息的客户端&#xff08;订阅者&#xff09;解耦&#xff0c;使得两者不需要建立直接的联系也不…

算法题2两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 开…

动态规划之方格取数

方格取数 动态规划&#xff0c;数字三角形模型 题目链接 https://www.luogu.com.cn/problem/P1004 题目描述 解法一 O ( n 4 ) O(n^4) O(n4) #include<bits/stdc.h> using namespace std; int n, i, j, l, k, x, y, s; int d[55][55], f[55][55][55][55]; int main()…

计算机视觉的应用27-关于VoVNetV2模型的应用场景,VoVNetV2模型结构介绍

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用27-关于VoVNetV2模型的应用场景&#xff0c;VoVNetV2模型结构介绍。VoVNetV2&#xff08;Visual Object-Driven Representation Learning Network Version 2&#xff09;是一种深度学习模型&#x…

python+requests的接口自动化测试框架实例详解教程

前言 Python是一种简单易学、功能强大的编程语言&#xff0c;广泛应用于各种软件开发和测试场景中。requests是Python中流行的HTTP库&#xff0c;支持发送HTTP请求和处理HTTP响应&#xff0c;它也是开发API自动化测试框架的重要组件之一。在本文中&#xff0c;我们将介绍如何使…

蓝桥集训之游戏

蓝桥集训之游戏 核心思想&#xff1a;博弈论 区间dp 设玩家1的最优解为A 玩家2的最优解为B 1的目标就是使A-B最大 2的目标就是使B-A最大 当玩家1取L左端点时 右边子区间结果就是玩家2的最优解B-A 即当前结果为w[L] – (B-A) 当玩家1取R右端点时 左边子区间结果就是玩家2的最…

【豫都故郡·领航新篇】Springer独立出版 |第二届先进无人飞行系统国际会议(ICAUAS 2024)

会议简介 Brief Introduction 2024年第二届先进无人飞行系统国际会议(ICAUAS 2024) 会议时间&#xff1a;2024年6月14日-16日 召开地点&#xff1a;中国南昌 大会官网&#xff1a;ICAUAS 2024-2024 2nd International Conference on Advanced Unmanned Aerial Systems2024 2nd …

大模型融合方法-DARE

LLM在SFT之后会产生大量的冗余参数(delta参数)&#xff0c;阿里团队提出DARE方法来消除delta参数&#xff0c;并将其合并到PRE模型中&#xff0c;从而实现多源模型能力的吸收。 DARE无需GPU重新训练&#xff0c;其思路非常简单&#xff0c;就跟dropout类似&#xff1a; m t ∼…

视频素材大全无水印哪里有?7个高清视频素材app推荐

在视频创作的领域里&#xff0c;获取可用的高质量素材是每位创作者追求的目标。全球各地的视频素材网站以其独特的资源和视角&#xff0c;为我们提供了丰富的选择。下面是一系列精选的网站&#xff0c;不仅提供可以自由使用的素材&#xff0c;还涵盖了从动态城市风光到壮丽自然…

知识竞赛中加时赛环节如何设计较好

加时赛是知识竞赛活动中要考虑的一个环节&#xff0c;尽管它很多时候可能用不到&#xff0c;但一般一定要有&#xff0c;除非你要其他方法再对重分的选手进行排名。下面介绍加时赛环节设计注意事项及具体方法。 第一&#xff1a;加赛题环节要干净利落 主办者一定要明白&#…

leetcode二叉树相关题目

目录 二叉树的建立整数数组转二叉树Object数组转二叉树 二叉树的遍历leetcode94.二叉树的中序遍历leetcode144.二叉树的前序遍历 二叉树的建立 整数数组转二叉树 下面只是一个简单的示例&#xff0c;没考虑某个子树为空的情况。把{1, 2, 3, 21, 22, 31, 32} 转变为一个二叉树…

如何制作Word模板并用Java导出自定义的内容

1前言 在做项目时会按照指定模板导出word文档,本文讲解分析需求后,制作word模板、修改模板内容,最终通过Java代码实现按照模板自定义内容的导出。 2制作word模板 2.1 新建word文档 新建word文档,根据需求进行编写模板内容,调整行间距和段落格式后将指定替换位置留空。…

18.8K星开源免费的跨平台密码管理器:KeePassXC

KeePassXC&#xff1a;您的跨平台密码守护神&#xff0c;安全存储&#xff0c;随心所欲&#xff0c;无论何处皆可信手拈来! - 精选真开源&#xff0c;释放新价值。 概览 当你面临一堆应用需要填写各种各样的密码的时候、当你需要记忆各种各样的密码或是需要保存保密文件或私密…

全国青少年软件编程(Scratch)等级考试二级考试真题2023年12月——持续更新.....

青少年软件编程(图形化)等级考试试卷(二级) 分数:100 题数:37 一、单选题(共25题,共50分) 1.在制作推箱子游戏时,地图是用数字形式储存在电脑里的,下图是一个推箱子地图,地图表示如下: 第一行(111111) 第二行(132231) 第三行(126621) 第四行( ) 第五行(152…

数独——拥有一定难度的回溯练习题,值得一看

数独相信大家都玩过&#xff0c;也都拥有不同的策略&#xff0c;那么放到C中又是怎样的呢&#xff1f;其实它就是回溯算法。话不多说&#xff0c;直接用例题来讲解&#xff1a; Description 数独是根据99盘面上的已知数字&#xff0c;推理出所有剩余空格的数字&#xff0c;并…

3-zookeeper之ZAB协议

Zookeeper ZAB协议 概述 ZAB(Zookeeper Automic Broadcast)是一套专门为Zookeeper设计的用于进行原子广播和崩溃恢复的协议ZAB协议主要包含了两个功能 原子广播&#xff1a;保证数据一致性崩溃恢复&#xff1a;保证集群的高可用 ZAB协议本身是基于2PC算法来进行的设计&#…

【js刷题:数据结构数组篇之有序数组的平方】

有序数组的平方 一、题目二、解题方法1、暴力解法2、双指针思路代码 一、题目 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 二、解题方法 1、暴力解法 class Solution {sortedSquares(…

数据结构与算法 双链表有序排列运算与循环单链表基本运算

一、实验内容 1.有一个带头结点的双链表L&#xff08;至少有一个数据结点&#xff09;&#xff0c;设计一个算法使其元素递增有序排列。 2. 编写一个程序clinklist.cpp,实现循环单链表的各种基本运算和整体建表算法&#xff08;假设循环单链表的元素类型ElemType为char&#…

OSCP靶场--Zipper

OSCP靶场–Zipper 考点(php zip:// rce[文件上传] CVE-2021-4034提权7z 通配符提权) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.249.229 -sV -sC -Pn --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-29 07:40 EDT …

11-设计模式:Go常用设计模式概述

设计模式是啥呢&#xff1f;简单来说&#xff0c;就是将软件开发中需要重复性解决的编码场景&#xff0c;按最佳实践的方式抽象成一个模型&#xff0c;模型描述的解决方法就是设计模式。使用设计模式&#xff0c;可以使代码更易于理解&#xff0c;保证代码的重用性和可靠性。 …