基于多反应堆的高并发服务器【C/C++/Reactor】(上)

(一)初始化服务器端用于监听的套接字

  • Server.h
#pragma once 
// 初始化监听的套接字
int initListenFd(unsigned short port);
  • Server.c
int initListenFd(unsigned short port) {
    // 1.创建监听的fd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket");
        return -1;
    }
    // 2.设置端口复用
    int opt = 1;
    int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    if(ret == -1) {
        perror("setsockopt");
        return -1;
    }
    // 3.绑定
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=INADDR_ANY;
    ret = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1) {
        perror("bind");
        return -1;
    }
    // 4.设置监听
    ret = listen(lfd,128);
    if(ret == -1) {
        perror("listen");
        return -1;
    }
    // 返回fd
    return lfd;
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>知识回顾>>>>>>>>>>>>>>>>>>>>>>>>>>>>

1. socket 

// 套接字通信分两部分:
    - 服务器端:被动接受连接,一般不会主动发起连接
    - 客户端:主动向服务器发起连接

 2.字节序转换函数

    当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。
    解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端
    知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定
    是否对接收到的数据进行转换(小端机转换,大端机不转换)。
    
    网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统
    等无关,从而 可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端
    排序方式。
    
    BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的
    转换函数: htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
  • h - host 主机,主机字节序
  • to - 转换成什么
  • n - network 网络字节序
  • s - short unsigned short
  • l - long unsigned int
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); //  网络字节序 - 主机字节序

3.socket 地址

socket 地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中
需要使用到这个socket地址。客户端 -> 服务端(IP,Port)
#include <netinet/in.h>
struct sockaddr_in
{
    sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
    in_port_t sin_port; /* Port number. */
    struct in_addr sin_addr; /* Internet address. */
    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
                sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
    in_addr_t s_addr;
};

4. IP地址转换(字符串ip-整数 ,主机、网络 字节序的转换)

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用 十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字 符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    af:地址族: AF_INET AF_INET6
    src:需要转换的点分十进制的IP字符串
    dst:转换后的结果保存在这个里面
 
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af:地址族: AF_INET AF_INET6
    src: 要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方
    size:第三个参数的大小(数组的大小)
    返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

5. TCP通信流程

// TCP 和 UDP -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
 
 
                   UDP                                     TCP
是否创建连接       无连接                                  面向连接
是否可靠           不可靠                                   可靠的
连接的对象个数   一对一、一对多、多对一、多对多              支持一对一
传输的方式         面向数据报                              面向字节流
首部开销           8个字节                               最少20个字节
适用场景        实时应用(视频会议,直播)           可靠性高的应用(文件传输)

// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
    - 监听:监听有客户端的连接
    - 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    - 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
    - 接收数据
    - 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
    - 接收数据
    - 发送数据
4. 通信结束,断开连接

6. 套接字函数

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
    - 功能:创建一个套接字
    - 参数:
        - domain: 协议族
            AF_INET : ipv4
            AF_INET6 : ipv6
            AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
        - type: 通信过程中使用的协议类型
            SOCK_STREAM : 流式协议
            SOCK_DGRAM : 报式协议
    - protocol : 具体的一个协议。一般写0
            - SOCK_STREAM : 流式协议默认使用 TCP
            - SOCK_DGRAM : 报式协议默认使用 UDP
    - 返回值:
        - 成功:返回文件描述符,操作的就是内核缓冲区。
        - 失败:-1
 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
	- 功能:绑定,将fd 和本地的IP + 端口进行绑定
	- 参数:
		- sockfd : 通过socket函数得到的文件描述符
		- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
		- addrlen : 第二个参数结构体占的内存大小
		
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
	- 功能:监听这个socket上的连接
	- 参数:
		- sockfd : 通过socket()函数得到的文件描述符
		- backlog : 未连接的和已经连接的和的最大值, 5
		
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
	- 参数:
		- sockfd : 用于监听的文件描述符
		- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
		- addrlen : 指定第二个参数的对应的内存大小
	- 返回值:
		- 成功 :用于通信的文件描述符
		- 失败 : -1
		
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	- 功能: 客户端连接服务器
	- 参数:
		- sockfd : 用于通信的文件描述符
		- addr : 客户端要连接的服务器的地址信息
		- addrlen : 第二个参数的内存大小
	- 返回值:成功 0, 失败 -1
 
//读写数据
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

7.SIGCHLD信号

SIGCHLD的产生条件:

  • 子进程终止
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处于停止状态,接收到SIGCONT后唤醒

注意:通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。

(二)epoll 工作模型的雏形

  • Server.h
// 启动epoll
int epollRun(int lfd);
  • Server.c
int epollRun(int lfd) {
    // 1.创建epoll实例
    int epfd = epoll_create(1);
    if(epfd == -1) {
        perror("epoll_create");
        return -1;
    }
    // 2.添加监听fd lfd上树 对于监听的描述符来说只需要看一下有没有新的客户端连接
    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;// 委托epoll(内核)帮我们检测lfd的读事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    // 3.检测
    struct epoll_event evs[1024];
    // int size = sizeof(evs)/sizeof(epoll_event);
    int size = sizeof(evs)/sizeof(evs[0]);
    while(1) {
        int num = epoll_wait(epfd,evs,size,-1);
        if(num == -1) {
            perror("epoll_wait");
            return -1;
        }
        for(int i=0;i<num;++i) {
            int fd = evs[i].data.fd;
            if(fd == lfd) {
                // 建立新连接 accept
                acceptClient(lfd,epfd);
            }else{
                // 主要是接收对端的数据
                recvHttpRequest(fd,epfd);
            }

        }
    }
    return 0;
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>知识回顾>>>>>>>>>>>>>>>>>>>>>>>>>>>>

1.epoll() 多路复用 和 两种工作模式

epollLinux内核中的一个事件驱动I/O机制,用于处理多个文件描述符上的事件。它是一个高效且强大的 I/O 多路复用工具,可以用于处理大量文件描述符的 I/O操作epoll 的主要优点是它只占用较少的资源,并且比传统的 select 和 poll 更易于使用。

epoll的工作原理是通过一个事件表来跟踪所有需要监控的文件描述符,当某个文件描述符上有事件发生时,epoll会通知程序去处理这些事件。这种方式可以确保程序在等待某个文件描述符上有事件发生时只占用较少的资源,而不是像selectpoll那样整个程序

----来自CodeGeex

2.epoll API介绍

typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;
 
struct epoll_event {
	uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};
 
常见的Epoll检测事件:
	- EPOLLIN
	- EPOLLOUT
	- EPOLLERR
	
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- op : 要进行什么操作
				EPOLL_CTL_ADD: 添加
				EPOLL_CTL_MOD: 修改
				EPOLL_CTL_DEL: 删除
		- fd : 要检测的文件描述符
		- event : 检测文件描述符什么事情
 
// 检测函数----检测epoll树中是否有就绪的文件描述符
// 创建了epfd,设置好某个fd上需要检测事件并将该fd绑定到epfd上去后,就可以调用epoll_wait
// 检测事件了
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
	- 参数:
		- epfd : epoll实例对应的文件描述符
		- events : 传出参数,保存了发送了变化的文件描述符的信息
		- maxevents : 第二个参数结构体数组的大小
		- timeout : 阻塞时间
			- 0 : 不阻塞
			- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
			- > 0 : 阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数 > 0
		- 失败 -1
 
// 创建epoll实例,通过一棵红黑树管理待检测集合
// 参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。
int epoll_create(int size);
>>epoll_wait 缺点:
    ① epoll_wait 调用之后,需要将所有fd的event参数重新设置一遍,
      如果fd比较多的话,会比较消耗性能。----来自CodeGeeX
 
>>epoll_wait 优点:
    ① epoll_wait 调用之后,直接在event参数中拿到所有有事件就绪的fd,直接处理即可。
    ② 一般在fd数量比较多,但某段时间内,就绪事件fd数量较少的情况下,epoll_wait才会
    体现出它的优势,也就是说socket连接数量较大时而活跃连接较少时epoll模型更高效。

// epoll 的使用
// 操作步骤
// 在服务器使用 epoll 进行 IO 多路转接的操作步骤如下:
    1.创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
 
    2.设置端口复用(可选)
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
    3.使用本地的IP与端口和监听的套接字进行绑定
    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
 
    4.给监听的套接字设置监听
    listen(lfd, 128);
 
    5.创建 epoll 实例
    int epfd = epoll_create(100);
 
    6.将用于监听的套接字添加到 epoll 实例中
    struct epoll_event ev;
    ev.events = EPOLLIN; //检测lfd读缓冲区是否有数据
    ev.data.fd = lfd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
 
    接着创建一个数组,用于存储epoll_wait()返回的文件描述符
    struct epoll_event evs[1024];
 
    7.检测添加到epoll实例中的文件描述符是否已经就绪,并将这些已就绪的文件描述符进行处理
    int num = epoll_wait(epfd, evs, size, -1);
 
    ① 如果监听的是文件描述符,和新客户端建立连接,将得到的文件描述符添加到epoll实例中
    int cfd = accept(curfd,NULL,NULL);
    ev.events = EPOLLIN;
    ev.data.fd = cfd;
 
    新得到的文件描述符添加到epoll模型中,下一轮循环的时候就可以被检测了
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
 
    ② 如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除
    int len = recv(curfd,buf,sizeof(buf),0);
    if(len == 0) {
        // 将这个文件描述符从epoll实例中删除
        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
        close(curfd);
    }else if(len > 0) {
        send(curfd,buf,len,0);
    }
 
    8.重复第 7 步的操作

3.epoll 的两种工作模式 
Epoll 的工作模式:
	LT 模式 (水平触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
				b.用户只读了一部分数据,epoll会通知
				c.缓冲区的数据读完了,不通知
	
	LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这
	种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操
	作。如果你不作任何操作,内核还是会继续通知你的。
 
	ET 模式(边沿触发)
		假设委托内核检测读事件 -> 检测fd的读缓冲区
			读缓冲区有数据 - > epoll检测到了会给用户通知
				a.用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不通知了
				b.用户只读了一部分数据,epoll不通知
				c.缓冲区的数据读完了,不通知
 
	ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述
	符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,
	并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述
	符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成
	未就绪),内核不会发送更多的通知(only once)。
	
	ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll
	工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写
	操作把处理多个文件描述符的任务饿死。
 
 
综上所述:epoll的边沿模式下 epoll_wait检测到文件描述符有新事件才会通知,
如果不是新的事情就不通知,通知的次数比水平模式少,效率比水平模式高。

 【注意】 ET模式需要配合循环+非阻塞

>> epoll在边沿模式下非阻塞接收数据
    循环接收数据的处理方式:对于每次接收的buffer多小都不重要了,只不过我们需要多接收几次数据。
    效率相对来说低一些;如果说buffer稍微大一点,接收数据的次数就少一些,效率相对来说高一些;
    可以把recv写到一个while循环里,通过while循环,每次读取5个字节,直到把客户端发过来的数据全部都读到本地。
    【思考】这种方式的弊端在哪里?
    【思考】进行套接字通信时阻塞的还是非阻塞的?
 
    【回答】很显然默认情况下进行套接字通信,这个处理流程是阻塞的。如果是阻塞的,
    当这个服务器端循环接收客户端发过来的数据,假设客户端发来了100个字节的数据,
    在服务端接收了20次,就把客户端发过来的数据全部读到本地了,但是在做第21次读
    数据的时候,这个recv它还能读到数据吗?
    没有了,也就是说这个文件描述符对应的读缓冲区里边是空的。如果说这个文件描述符
    对应的读缓冲区里边是空的。这个recv再去接收数据的话,服务器端的线程或者服务器
    端的进程它就阻塞了。如果这个线程/进程阻塞了,就不能干别的事情了。如果说写的
    这个程序里边就是单线程或者单进程的程序,在这里阻塞了,就不能够去做其他的事情
    了,整个程序就停止在这里了。
 
    【问题】如何让while循环中的break起作用?
        修改文件描述符为非阻塞,而不是修改read/recv函数,因为这函数时基于文件描述符
        去进行数据的接收操作,所以说需要修改一下这个文件描述符的属性,把这个文件描述
        符的默认阻塞属性修改为非阻塞属性。再次调用recv/read函数的时候,它们也就不会阻塞了
 
    【思考】如何把这个文件描述符修改为非阻塞属性?
        解决阻塞问题,需要将套接字默认的阻塞行为修改为非阻塞,需要使用fcntl()函数进行处理
 
        // 设置完成之后,读写都变成了非阻塞模式
        int flag = fcntl(cfd,F_GETFL);
        flag |= O_NOBLOCK;
        fcntl(cfd,F_SETFL,flag);  

(三)和客户端建立新连接

  • Server.h
// 和客户端建立连接
int acceptClient(int lfd,int epfd);
  • Server.c
// 接受新连接,把得到的新的文件描述符cfd也添加到epoll树上
int acceptClient(int lfd,int epfd) {
    // 1.创建一个套接字(建立连接)
    int cfd = accept(lfd,NULL,NULL);
    if(cfd == -1) {
        perror("accept");
        return -1;
    }
    // 2.添加到epoll中
    int flag = fcntl(cfd,F_GETFL);// 设置非阻塞
    flag |= O_NONBLOCK;
    fcntl(cfd,F_SETFL,flag);

    // 3.cfd 添加到epoll中
    struct epoll_event ev;
    ev.data.fd=cfd;
    ev.events=EPOLLIN | EPOLLET;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    return 0;
}

(四)接收客户端的http请求消息

WebServer 解析HTTP 请求报文-CSDN博客icon-default.png?t=N7T8https://heheda.blog.csdn.net/article/details/132695415WebServer 解析HTTP 响应报文-CSDN博客icon-default.png?t=N7T8https://heheda.blog.csdn.net/article/details/132746046

  • Server.h
// 主要是接收对端的数据
int recvHttpRequest(int cfd,int epfd);
  • Server.c
int recvHttpRequest(int cfd,int epfd) {
    // 有了存储数据的内存之后,接下来就是读数据
    // 注意:前面已经把用于通信的文件描述符的事件改成了边缘非阻塞
    // 如果是边缘模式epoll检测到文件描述符对应的读事件之后,只会给我们通知一次
    // 因此需要得到这个通知之后,
    printf("开始接收数据了...\n");
    int len = 0,total = 0;
    char buf[4096] = {0};
    char tmp[1024] = {0};
    while((len = recv(cfd,tmp,sizeof tmp,0))>0) {
        if(total + len < sizeof buf)
            memcpy(buf+total,tmp,len);
        total += len;
    }
    
    // 判断数据是否接收完毕
    if(len == -1 && errno == EAGAIN) {
        // 说明服务器已经把客户端发过来的请求数据接收完毕了
        // 解析请求行
        char* pt = strstr(buf,"\r\n");
        int reqLen = pt - buf;
        buf[reqLen] = '\0';
        parseRequestLine(buf,cfd);
    }
    else if(len == 0) {
        // 说明客户端断开了连接
        int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
        if(ret == -1) {
            perror("epoll_ctl");
            return -1;
        }
        close(cfd);
    }else{
        perror("recv");
    }
    return 0;
}

(五)解析请求行

  • Server.h 
// 解析请求行
int parseRequestLine(const char* line,int cfd);
  • Server.c
int parseRequestLine(const char* line,int cfd){
    // 解析请求行 
    // 请求行格式:GET /index.html HTT   -------------------P/1.1
    // 解析出请求方法、请求路径、协议版本
    // 请求方法:GET
    // 请求路径:/index.html
    // 协议版本:HTTP/1.1
    // 请求方法GET、请求路径/index.html、协议版本HTTP/1.1
    // 请求行长度:GET /index.html HTTP/1.1
    // 请求行长度:29
    char method[12];
    char path[1024];
    sscanf(line,"%[^ ] %[^ ]",method,path);
    printf("method: %s,path: %s\n",method,path);
    if(strcasecmp(method,"get") != 0) {
        return -1;
    }
    decodeMsg(path,path);
    // 处理客户端请求的静态资源(目录或者文件)
    char* file = NULL; 
    if(strcmp(path,"/") == 0) {
        file = "./";
    }else {
        file = path+1;
    }
    // 获取文件属性
    struct stat st;
    int ret = stat(file,&st);
    if(ret == -1) {
        // 文件不存在 -- 回复404
        sendHeadMsg(cfd,404,"Not Found",getFileType(".html"),-1);
        sendFile("404.html",cfd);
        return 0;
    }
    // 判断文件类型
    if(S_ISDIR(st.st_mode)) {
        // 把这个目录中的内容发送给客户端
        sendHeadMsg(cfd,200,"OK",getFileType(".html"),-1);
        sendDir(file,cfd);
    }
    else {
        // 把文件的内容发送给客户端
        sendHeadMsg(cfd,200,"OK",getFileType(file),st.st_size);
        sendFile(file,cfd);
    }
    return 0;
}

(六)组织Http响应的数据块头

  • Server.h  
// 发送响应头(状态行 + 响应头)
int sendHeadMsg(int cfd,int status,const char* descr,const char* type,int length);
const char* getFileType(const char* name);
  • Server.c
int sendHeadMsg(int cfd,int status,const char* descr,const char* type,int length) {
    // 状态行
    char buf[4096] = {0};
    sprintf(buf,"http/1.1 %d %s\r\n",status,descr);
    // 响应头
    sprintf(buf+strlen(buf),"content-type: %s\r\n",type);
    sprintf(buf+strlen(buf),"content-length: %d\r\n\r\n",length);
    send(cfd,buf,strlen(buf),0);
    return 0;
}

const char* getFileType(const char* name) {
    // a.jpg a.mp4 a.html
    // 自右向左查找 '.' 字符,如不存在返回NULL
    const char* dot = strrchr(name,'.');
    if(dot == NULL) 
        return "text/plain; charset=utf-8";//纯文本
    if(strcmp(dot,".html") == 0 || strcmp(dot,".htm") == 0) 
        return "text/html; charset=utf-8";
    if(strcmp(dot,".jpg")==0 || strcmp(dot,".jpeg")==0) 
        return "image/jpeg";
    if(strcmp(dot,".gif")==0)
        return "image/gif";
    if(strcmp(dot,".png")==0)
        return "image/png";
    if(strcmp(dot,".css")==0) 
        return "text/css";
    if(strcmp(dot,".au")==0)
        return "audio/basic";
    if(strcmp(dot,".wav")==0)
        return "audio/wav";
    if(strcmp(dot,".avi")==0)
        return "video/x-msvideo";
    if(strcmp(dot,".mov")==0 || strcmp(dot,".qt")==0)
        return "video/quicktime";
    if(strcmp(dot,".mpeg")==0 || strcmp(dot,".mpe")==0)
        return "video/mpeg";
    if(strcmp(dot,".vrml")==0 || strcmp(dot,".wrl")==0)
        return "model/vrml";
    if(strcmp(dot,".midi")==0 || strcmp(dot,".mid")==0)
        return "audio/midi";
    if(strcmp(dot,".mp3")==0)
        return "audio/mpeg";
    if(strcmp(dot,".ogg") == 0) 
        return "application/ogg";
    if(strcmp(dot,".pac") == 0)
        return "application/x-ns-proxy-autoconfig";
    return "text/plain; charset=utf-8";//纯文本
}

(七)发送文件的两种方式

  • Server.h  
// 发送文件
int sendFile(const char* fileName,int cfd);
  • Server.c
int sendFile(const char* fileName,int cfd) {
    // 打开文件
    int fd = open(fileName,O_RDONLY);
    // assert(fd > 0);
    if(fd == -1){
        perror("open");
        return -1;
    }
#if 0
    while (1)
    {
        char buf[1024];
        int len = read(fd,buf,sizeof(buf));
        if(len > 0) {
            send(cfd,buf,len,0);
            usleep(10); // 这非常重要
        }
        else if(len == 0) {
            break;
        }
        else{
            perror("read");
        }
    }
#else
    // 把文件内容发送给客户端
    // int size = lseek(fd,0,SEEK_END);// 文件指针移动到了尾部
    // lseek(fd,0,SEEK_SET);
    // int ret = sendfile(cfd,fd,NULL,size);
    // off_t offset = 0;
    // while (offset < size){
    //     int ret = sendfile(cfd,fd,&offset,size- offset);
    //     printf("ret value: %d\n",ret);
    //     if (ret == -1 && errno == EAGAIN)
    //     {
    //         printf("没数据...\n");
    //     }
    // }
    struct stat st;
    fstat(fd,&st);
    off_t offset = 0;
    while (offset < st.st_size){
        int ret = sendfile(cfd,fd,&offset,st.st_size- offset);
        printf("ret value: %d\n",ret);
        if (ret == -1 && errno == EAGAIN)
        {
            printf("没数据...\n");
        }
    }
#endif
    close(fd);
    return 0;
}

(八)发送目录

  • Server.h
// 发送目录
int sendDir(const char* dirName,int cfd); 
  • Server.c
int sendDir(const char* dirName,int cfd) {
    char buf[4096] = {0};
    sprintf(buf,"<html><head><title>%s</title></head><body><table>",dirName);
    struct dirent** nameList;
    int num = scandir(dirName,&nameList,NULL,alphasort);
    for(int i=0;i<num;i++) {
        // 取出文件名 nameList 指向的是一个指针数组 struct dirent* tmp[]
        char* name = nameList[i]->d_name;
        struct stat st;
        char subPath[1024] = {0};
        sprintf(subPath,"%s/%s",dirName,name);
        stat(subPath,&st);
        if(S_ISDIR(st.st_mode)) {
            // 从当前目录跳到子目录里边,/
            sprintf(buf+strlen(buf),
                "<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
                name,name,st.st_size);
        }else{
            sprintf(buf+strlen(buf),
                "<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
                name,name,st.st_size);
        }
        send(cfd,buf,strlen(buf),0);
        memset(buf,0,sizeof(buf));
        free(nameList[i]); 
    }
    sprintf(buf,"</table></body></html>");
    send(cfd,buf,strlen(buf),0);
    free(nameList);
    return 0;
}
/*
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <table>
            <tr>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
            </tr>
        </table>
    </body>
</html>
*/

(八)解决浏览器无法访问带特殊字符的文件得到问题

  • Server.h
int hexToDec(char c);
void decodeMsg(char* to,char* from);
  • Server.c 
// 将字符转换为整型数
int hexToDec(char c){
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;
    return 0;
}

// 解码
// to 存储解码之后的数据, 传出参数, from被解码的数据, 传入参数
void decodeMsg(char* to,char* from) {
    for(;*from!='\0';++to,++from) {
        // isxdigit -> 判断字符是不是16进制格式, 取值在 0-f
        // Linux%E5%86%85%E6%A0%B8.jpg
        if(*from == '%' && isxdigit(from[1]) && isxdigit(from[2])){
            // 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
            // B2 == 178
            // 将3个字符, 变成了一个字符, 这个字符就是原始数据
            // *to = (hexToDec(from[1]) * 16) + hexToDec(from[2]);
            *to = (hexToDec(from[1]) << 4) + hexToDec(from[2]);

            // 跳过 from[1] 和 from[2] ,因此在当前循环中已经处理过了
            from += 2;
        }else{
            // 字符拷贝,赋值
            *to = *from;
        }
    }
    *to = '\0';
}

完整代码:

main.c

#include <stdio.h>
#include "Server.h"
#include "Server.c"
#include <unistd.h>
#include <stdlib.h>

int main(int argc,char* argv[]) {
    if(argc < 3) {
        printf("./a.out port path\n");
        return -1;
    }
    unsigned short port = atoi(argv[1]);
    // 切换服务器的工作路径
    chdir(argv[2]);
    // 初始化用于监听的套接字
    int lfd = initListenFd(port);
    // 启动服务器程序
    epollRun(lfd);
    return  0;
}

Server.c

#include "Server.h"
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <assert.h>
#include <ctype.h>
int initListenFd(unsigned short port) {
    // 1.创建监听的fd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket");
        return -1;
    }
    // 2.设置端口复用
    int opt = 1;
    int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    if(ret == -1) {
        perror("setsockopt");
        return -1;
    }
    // 3.绑定
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=INADDR_ANY;
    ret = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1) {
        perror("bind");
        return -1;
    }
    // 4.设置监听
    ret = listen(lfd,128);
    if(ret == -1) {
        perror("listen");
        return -1;
    }
    // 返回fd
    return lfd;
}

int epollRun(int lfd) {
    // 1.创建epoll实例
    int epfd = epoll_create(1);
    if(epfd == -1) {
        perror("epoll_create");
        return -1;
    }
    // 2.添加监听fd lfd上树 对于监听的描述符来说只需要看一下有没有新的客户端连接
    struct epoll_event ev;
    ev.data.fd = lfd;
    ev.events = EPOLLIN;// 委托epoll(内核)帮我们检测lfd的读事件
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    // 3.检测
    struct epoll_event evs[1024];
    // int size = sizeof(evs)/sizeof(epoll_event);
    int size = sizeof(evs)/sizeof(evs[0]);
    while(1) {
        int num = epoll_wait(epfd,evs,size,-1);
        if(num == -1) {
            perror("epoll_wait");
            return -1;
        }
        for(int i=0;i<num;++i) {
            int fd = evs[i].data.fd;
            if(fd == lfd) {
                // 建立新连接 accept
                acceptClient(lfd,epfd);
            }else{
                // 主要是接收对端的数据
                recvHttpRequest(fd,epfd);
            }

        }
    }
    return 0;
}

// 接受新连接,把得到的新的文件描述符cfd也添加到epoll树上
int acceptClient(int lfd,int epfd) {
    // 1.创建一个套接字(建立连接)
    int cfd = accept(lfd,NULL,NULL);
    if(cfd == -1) {
        perror("accept");
        return -1;
    }
    // 2.添加到epoll中
    int flag = fcntl(cfd,F_GETFL);// 设置非阻塞
    flag |= O_NONBLOCK;
    fcntl(cfd,F_SETFL,flag);

    // 3.cfd 添加到epoll中
    struct epoll_event ev;
    ev.data.fd=cfd;
    ev.events=EPOLLIN | EPOLLET;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
    if(ret == -1) {
        perror("epoll_ctl");
        return -1;
    }
    return 0;
}

int recvHttpRequest(int cfd,int epfd) {
    // 有了存储数据的内存之后,接下来就是读数据
    // 注意:前面已经把用于通信的文件描述符的事件改成了边缘非阻塞
    // 如果是边缘模式epoll检测到文件描述符对应的读事件之后,只会给我们通知一次
    // 因此需要得到这个通知之后,
    printf("开始接收数据了...\n");
    int len = 0,total = 0;
    char buf[4096] = {0};
    char tmp[1024] = {0};
    while((len = recv(cfd,tmp,sizeof tmp,0))>0) {
        if(total + len < sizeof buf)
            memcpy(buf+total,tmp,len);
        total += len;
    }
    
    // 判断数据是否接收完毕
    if(len == -1 && errno == EAGAIN) {
        // 说明服务器已经把客户端发过来的请求数据接收完毕了
        // 解析请求行
        char* pt = strstr(buf,"\r\n");
        int reqLen = pt - buf;
        buf[reqLen] = '\0';
        parseRequestLine(buf,cfd);
    }
    else if(len == 0) {
        // 说明客户端断开了连接
        int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
        if(ret == -1) {
            perror("epoll_ctl");
            return -1;
        }
        close(cfd);
    }else{
        perror("recv");
    }
    return 0;
}
int parseRequestLine(const char* line,int cfd){
    // 解析请求行 
    // 请求行格式:GET /index.html HTT   -------------------P/1.1
    // 解析出请求方法、请求路径、协议版本
    // 请求方法:GET
    // 请求路径:/index.html
    // 协议版本:HTTP/1.1
    // 请求方法GET、请求路径/index.html、协议版本HTTP/1.1
    // 请求行长度:GET /index.html HTTP/1.1
    // 请求行长度:29
    char method[12];
    char path[1024];
    sscanf(line,"%[^ ] %[^ ]",method,path);
    printf("method: %s,path: %s\n",method,path);
    if(strcasecmp(method,"get") != 0) {
        return -1;
    }
    decodeMsg(path,path);
    // 处理客户端请求的静态资源(目录或者文件)
    char* file = NULL; 
    if(strcmp(path,"/") == 0) {
        file = "./";
    }else {
        file = path+1;
    }
    // 获取文件属性
    struct stat st;
    int ret = stat(file,&st);
    if(ret == -1) {
        // 文件不存在 -- 回复404
        sendHeadMsg(cfd,404,"Not Found",getFileType(".html"),-1);
        sendFile("404.html",cfd);
        return 0;
    }
    // 判断文件类型
    if(S_ISDIR(st.st_mode)) {
        // 把这个目录中的内容发送给客户端
        sendHeadMsg(cfd,200,"OK",getFileType(".html"),-1);
        sendDir(file,cfd);
    }
    else {
        // 把文件的内容发送给客户端
        sendHeadMsg(cfd,200,"OK",getFileType(file),st.st_size);
        sendFile(file,cfd);
    }
    return 0;
}

int sendFile(const char* fileName,int cfd) {
    // 打开文件
    int fd = open(fileName,O_RDONLY);
    // assert(fd > 0);
    if(fd == -1){
        perror("open");
        return -1;
    }
#if 0
    while (1)
    {
        char buf[1024];
        int len = read(fd,buf,sizeof(buf));
        if(len > 0) {
            send(cfd,buf,len,0);
            usleep(10); // 这非常重要
        }
        else if(len == 0) {
            break;
        }
        else{
            perror("read");
        }
    }
#else
    // 把文件内容发送给客户端
    // int size = lseek(fd,0,SEEK_END);// 文件指针移动到了尾部
    // lseek(fd,0,SEEK_SET);
    // int ret = sendfile(cfd,fd,NULL,size);
    // off_t offset = 0;
    // while (offset < size){
    //     int ret = sendfile(cfd,fd,&offset,size- offset);
    //     printf("ret value: %d\n",ret);
    //     if (ret == -1 && errno == EAGAIN)
    //     {
    //         printf("没数据...\n");
    //     }
    // }
    struct stat st;
    fstat(fd,&st);
    off_t offset = 0;
    while (offset < st.st_size){
        int ret = sendfile(cfd,fd,&offset,st.st_size- offset);
        printf("ret value: %d\n",ret);
        if (ret == -1 && errno == EAGAIN)
        {
            printf("没数据...\n");
        }
    }
#endif
    close(fd);
    return 0;
}

int sendHeadMsg(int cfd,int status,const char* descr,const char* type,int length) {
    // 状态行
    char buf[4096] = {0};
    sprintf(buf,"http/1.1 %d %s\r\n",status,descr);
    // 响应头
    sprintf(buf+strlen(buf),"content-type: %s\r\n",type);
    sprintf(buf+strlen(buf),"content-length: %d\r\n\r\n",length);
    send(cfd,buf,strlen(buf),0);
    return 0;
}

const char* getFileType(const char* name) {
    // a.jpg a.mp4 a.html
    // 自右向左查找 '.' 字符,如不存在返回NULL
    const char* dot = strrchr(name,'.');
    if(dot == NULL) 
        return "text/plain; charset=utf-8";//纯文本
    if(strcmp(dot,".html") == 0 || strcmp(dot,".htm") == 0) 
        return "text/html; charset=utf-8";
    if(strcmp(dot,".jpg")==0 || strcmp(dot,".jpeg")==0) 
        return "image/jpeg";
    if(strcmp(dot,".gif")==0)
        return "image/gif";
    if(strcmp(dot,".png")==0)
        return "image/png";
    if(strcmp(dot,".css")==0) 
        return "text/css";
    if(strcmp(dot,".au")==0)
        return "audio/basic";
    if(strcmp(dot,".wav")==0)
        return "audio/wav";
    if(strcmp(dot,".avi")==0)
        return "video/x-msvideo";
    if(strcmp(dot,".mov")==0 || strcmp(dot,".qt")==0)
        return "video/quicktime";
    if(strcmp(dot,".mpeg")==0 || strcmp(dot,".mpe")==0)
        return "video/mpeg";
    if(strcmp(dot,".vrml")==0 || strcmp(dot,".wrl")==0)
        return "model/vrml";
    if(strcmp(dot,".midi")==0 || strcmp(dot,".mid")==0)
        return "audio/midi";
    if(strcmp(dot,".mp3")==0)
        return "audio/mpeg";
    if(strcmp(dot,".ogg") == 0) 
        return "application/ogg";
    if(strcmp(dot,".pac") == 0)
        return "application/x-ns-proxy-autoconfig";
    return "text/plain; charset=utf-8";//纯文本
}

/*
<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <table>
            <tr>
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
            </tr>
        </table>
    </body>
</html>
*/

int sendDir(const char* dirName,int cfd) {
    char buf[4096] = {0};
    sprintf(buf,"<html><head><title>%s</title></head><body><table>",dirName);
    struct dirent** nameList;
    int num = scandir(dirName,&nameList,NULL,alphasort);
    for(int i=0;i<num;i++) {
        // 取出文件名 nameList 指向的是一个指针数组 struct dirent* tmp[]
        char* name = nameList[i]->d_name;
        struct stat st;
        char subPath[1024] = {0};
        sprintf(subPath,"%s/%s",dirName,name);
        stat(subPath,&st);
        if(S_ISDIR(st.st_mode)) {
            // 从当前目录跳到子目录里边,/
            sprintf(buf+strlen(buf),
                "<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
                name,name,st.st_size);
        }else{
            sprintf(buf+strlen(buf),
                "<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
                name,name,st.st_size);
        }
        send(cfd,buf,strlen(buf),0);
        memset(buf,0,sizeof(buf));
        free(nameList[i]); 
    }
    sprintf(buf,"</table></body></html>");
    send(cfd,buf,strlen(buf),0);
    free(nameList);
    return 0;
}

// 将字符转换为整型数
int hexToDec(char c){
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;
    return 0;
}

// 解码
// to 存储解码之后的数据, 传出参数, from被解码的数据, 传入参数
void decodeMsg(char* to,char* from) {
    for(;*from!='\0';++to,++from) {
        // isxdigit -> 判断字符是不是16进制格式, 取值在 0-f
        // Linux%E5%86%85%E6%A0%B8.jpg
        if(*from == '%' && isxdigit(from[1]) && isxdigit(from[2])){
            // 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
            // B2 == 178
            // 将3个字符, 变成了一个字符, 这个字符就是原始数据
            // *to = (hexToDec(from[1]) * 16) + hexToDec(from[2]);
            *to = (hexToDec(from[1]) << 4) + hexToDec(from[2]);

            // 跳过 from[1] 和 from[2] ,因此在当前循环中已经处理过了
            from += 2;
        }else{
            // 字符拷贝,赋值
            *to = *from;
        }
    }
    *to = '\0';
}

Server.h

#pragma once 
// 初始化监听的套接字
int initListenFd(unsigned short port);
// 启动epoll
int epollRun(int lfd);
// 和客户端建立连接
int acceptClient(int lfd,int epfd);
// 主要是接收对端的数据
int recvHttpRequest(int cfd,int epfd);
// 解析请求行
int parseRequestLine(const char* line,int cfd);
// 发送文件
int sendFile(const char* fileName,int cfd);
// 发送响应头(状态行 + 响应头)
int sendHeadMsg(int cfd,int status,const char* descr,const char* type,int length);
const char* getFileType(const char* name);

// 发送目录
int sendDir(const char* dirName,int cfd); 
int hexToDec(char c);
void decodeMsg(char* to,char* from);

演示效果:

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

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

相关文章

案例135:基于微信小程序的房屋租赁管理系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

Unity-生命周期

Unity 中,有一个特别重要的知识点,生命周期函数。这些东西全部都是系统定义好的,运行时自动调用,但需要继承 MonoBehaviour 类才能使用。这个类是从 Unity 中创建脚本就自动继承了。 正是因为继承了 MonoBehaviour 这个类,Unity 才能依次调用我们的脚本代码,执行游戏逻辑…

多相机系统通用视觉 SLAM 框架的设计与评估

Design and Evaluation of a Generic Visual SLAM Framework for Multi-Camera Systems PDF https://arxiv.org/abs/2210.07315 Code https://github.com/neufieldrobotics/MultiCamSLAM Data https://tinyurl.com/mwfkrj8k 程序设置 主要目标是开发一个与摄像头系统配置无关…

Linux笔记---文件和目录操作

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux学习 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 命令 ls (List): pwd (Print Working Directory): cp (Copy): mv (Move): rm (Remove): 结语 我的其他博客 前言 学习Linux命令…

vscode打开多个标签页配置

前言 如果其中一个标签的文件没有修改&#xff0c;再打开一个文件时之前的打开的标签页就会被替换掉。 在工作中使用很不方便。 解决办法 文件-首选项--设置 下图取消勾选 取消之后如下 再去打开标签就会一致显示了

Ajax Search Pro Live WordPress网站内容实时搜索插件

点击阅读Ajax Search Pro Live WordPress网站内容实时搜索插件原文 Ajax Search Pro Live WordPress网站内容实时搜索插件是 WordPress 最好的实时搜索引擎插件。高度可定制&#xff0c;具有许多功能和选项&#xff0c;可提供最佳结果&#xff01;用更美观、更高效的搜索引擎替…

55.0/CSS 的应用(详细版)

目录 55.1.1 设计边框样式 55.1.2 调整边框的粗细 55.1.3 边框颜色 55.1.4 复合设置边框 55.2 模块的边距 55.3 模块的内边距 55.4 层的应用 55.4.1 层的建立 55.4.2 浮动——float 55.4.3 清除浮动 55.4.4 层的定位 55.4.5 设置层的溢出——overflow 55.4.6 设置鼠…

数字人解决方案——ER-NeRF实时对话数字人模型推理部署带UI交互界面

简介 这个是一个使用ER-NeRF来实现实时对话数字人、口播数字人的整体架构&#xff0c;其中包括了大语言回答模型、语音合成、成生视频流、背景替换等功能&#xff0c;项目对显存的要求很高&#xff0c;想要达到实时推理的效果&#xff0c;建议显存在24G以上。 实时对话数字人 …

selenium 报错

selenium 报错 开始学自动化测试&#xff0c;&#xff0c;环境配了一天TAT 安装好selenium之后 运行python脚本 # codingutf-8 from selenium import webdriver import timedriver webdriver.Chrome() driver.get("https://www.baidu.com") time.sleep(3) driver.…

大模型之二十一-小语言模型塞道开启

当前提到大语言模型&#xff0c;大家想到的都是动辄百亿规模以上参数量的模型&#xff0c;13B、70B都是稀疏平常入门级的&#xff0c;但是目前从模型层面来看&#xff0c;模型参数量的规模两极分化已经来临&#xff0c;早期各大公司为了效果怼上去&#xff0c;采取了简单粗暴的…

Hive执行计划

Hive提供了explain命令来展示一个查询的执行计划&#xff0c;这个执行计划对于我们了解底层原理&#xff0c;Hive 调优&#xff0c;排查数据倾斜等很有帮助。 使用语法如下&#xff1a; explain query;在 hive cli 中输入以下命令(hive 2.3.7)&#xff1a; explain select s…

Flink系列之:背压下的检查点

Flink系列之&#xff1a;背压下的检查点 一、Checkpointing under backpressure二、缓冲区 Debloating三、非对齐 Checkpoints四、对齐 Checkpoint 的超时五、限制六、故障排除 一、Checkpointing under backpressure 通常情况下&#xff0c;对齐 Checkpoint 的时长主要受 Che…

使用Pycharm一键将.ui文件生成.py文件配置教程、一键打开QTDesigner教程

2df3621a-7ffd-4f18-9735-b86464b83a5b 前言 我痛恨所有将白嫖归为理所应当的猪&#x1f416;。 教程 打开pycharm之后&#xff0c;依次点击File->Settings->Tools->External Tools&#xff0c;进入如下界面&#xff1a; 1、配置快捷打开Qt Designer 点击号&…

基于深度学习的森林火焰烟雾检测系统(含UI界面,yolov8、Python代码,数据集)

项目介绍 项目中所用到的算法模型和数据集等信息如下&#xff1a; 算法模型&#xff1a;     yolov8 yolov8主要包含以下几种创新&#xff1a;         1. 添加注意力机制&#xff08;SE、CBAM等&#xff09;         2. 修改可变形卷积&#xff08;DySnake-主干c…

gem5 RubyPort: mem_request_port作用与连接 simple-MI_example.py

简介 回答这个问题&#xff1a;RubyPort的口下&#xff0c;一共定义了六个口&#xff0c;分别是mem_request_port&#xff0c;mem_response_port&#xff0c;pio_request_port&#xff0c;pio_response_port&#xff0c;in_ports, interrupt_out_ports&#xff0c;他们分别有什…

YOLOv8改进 | 主干篇 | 利用MobileNetV2替换Backbone(轻量化网络结构)

一、本文介绍 本文给大家带来的改进机制是MobileNetV2&#xff0c;其是专为移动和嵌入式视觉应用设计的轻量化网络结构。其在MobilNetV1的基础上采用反转残差结构和线性瓶颈层。这种结构通过轻量级的深度卷积和线性卷积过滤特征&#xff0c;同时去除狭窄层中的非线性&#xff…

Circulation:室性早搏会增加不良心血管事件|UK Biobank周报(12.14)

欢迎报名2023年郑老师团队课程&#xff01; 郑老师科研统计培训&#xff0c;包括临床数据、公共数据分析课程等&#xff0c;欢迎报名 英国生物银行&#xff08;UK Biobank&#xff0c;UKB&#xff09;是英国迄今以来规模最大的有关致病或预防疾病的基因和环境因子的信息资源库。…

【案例】图片预览

效果图 如何让图片放大&#xff0c;大多数的UI组件都带有这种功能&#xff0c;今天给大家介绍的这个插件除了放大之外&#xff0c;还可以旋转、移动、翻转、旋转、二次放大&#xff08;全屏&#xff09; 实现 npm i v-viewer -Smain.js 中引入 import viewerjs/dist/viewer.c…

java并发编程六 共享模型之内存

文章目录 Java 内存模型可见性解决方法 有序性解决方法 Java 内存模型 JMM 即 Java Memory Model&#xff0c;它定义了主存、工作内存抽象概念&#xff0c;底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。 JMM 体现在以下几个方面 原子性 - 保证指令不会受到线程上…

前端ICON库

前端ICON库 1.mingcute mingcute 2.lordicon lordicon 3.字节iconpark&#xff08;推荐&#xff09; 字节iconpark 4.iconbuddy iconbuddy.app/ 5.商标寻找youicons 免费下载数百万个徽标以获得设计灵感 | YouIcons.com 还有一堆工具