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

news2024/12/22 19:13:07

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

  • 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);

演示效果:

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

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

相关文章

HTTP:HTTP报文

HTTP&#xff1a;HTTP报文 1. 报文流1.1 报文流入源端服务器1.2 报文向下游流动1.3 报文的组成部分1.3.1 报文的语法1.3.2 起始行1. 请求行2. 响应行3. 方法4. 状态码 如果说HTTP是因特网的信使&#xff0c;那么HTTP报文就是用它来搬东西的包裹了。 1. 报文流 HTTP报文是在…

最新国内免费使用GPT4教程,GPT语音对话使用,Midjourney绘画

一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画&#xff0c;相信对大家应该不感到陌生吧&#xff1f;简单来说&#xff0c;GPT-4技术比之前的GPT-3.5相对来说更加智能&#xff0c;会根据用户的要求生成多种内容甚至也可以和用户进行创作交流。 然而&#xff0c;GP…

PHP字符串解析特性绕过WAF

[RoarCTF 2019]Easy Calc 题目的突破点&#xff1a; 只能传入数字和运算符号&#xff0c;不能传入字符&#xff08;想办法绕过waf&#xff09; 方法1. php解析规则&#xff1a;当php进行解析时&#xff0c;如果变量名前面有空格&#xff0c;php会自动去掉前面的空格再进行…

轻松搭建知识付费小程序:让知识传播更便捷

明理信息科技saas知识付费平台 在当今数字化时代&#xff0c;知识付费已经成为一种趋势&#xff0c;越来越多的人愿意为有价值的知识付费。然而&#xff0c;公共知识付费平台虽然内容丰富&#xff0c;但难以满足个人或企业个性化的需求和品牌打造。同时&#xff0c;开发和维护…

打破Tomcat中的双亲委派机制:探讨与实践

目录 引言 1. 双亲委派机制概述 2. 打破双亲委派机制的场景 3. Tomcat中的类加载器体系 4. 打破双亲委派机制的方法 4.1 在catalina.properties中配置common.loader 4.2 在META-INF/context.xml中配置Loader元素 4.3 编写自定义的类加载器 5. 潜在的问题与解决方案 5…

Shell编程自动化之特殊Shell扩展变量

1.变量的处理 1.1 如果parameter变量值为空&#xff0c;那么返回str字符串。 ${parameter:-str} 1.2 如果parameter变量值为空&#xff0c;那么str替代变量值&#xff0c;且返回其值。 ${parameter:str} 1.3 如果parameter变量值为空&#xff0c;那么str当作stderr输出&am…

Django 简单图书管理系统

一、图书需求 1. 书籍book_index.html中有超链接&#xff1a;查看所有的书籍列表book_list.html页面 2. 书籍book_list.html中显示所有的书名&#xff0c;有超链接&#xff1a;查看本书籍详情book_detail.html(通过书籍ID)页面 3. 书籍book_detail.html中书的作者和出版社&…

现代雷达车载应用——第3章 MIMO雷达技术 3.2节 汽车MIMO雷达波形正交策略

经典著作&#xff0c;值得一读&#xff0c;英文原版下载链接【免费】ModernRadarforAutomotiveApplications资源-CSDN文库。 3.2 汽车MIMO雷达波形正交策略 基于MIMO雷达技术的汽车雷达虚拟阵列合成依赖于不同天线发射信号的可分离性。当不同天线的发射信号正交时&#x…

德人合科技 | 设计公司文件加密系统——天锐绿盾自动智能透明加密防泄密系统

设计公司文件加密系统——天锐绿盾自动智能透明加密防泄密系统 PC端访问地址&#xff1a; www.drhchina.com 一、背景介绍 设计公司通常涉及到大量的创意作品、设计方案、客户资料等重要文件&#xff0c;这些文件往往包含公司的核心价值和商业机密。因此&#xff0c;如何确保…

百年东芝“瞄准”汽车「芯」机遇

在汽车“新四化”大变革的驱动下&#xff0c;汽车半导体市场进入需求暴涨的新周期。 “智能电动汽车所需要的半导体种类和数量正在急剧增加。” 东芝电子分立器件应用技术部经理成栋表示&#xff0c;东芝电子正在加大汽车半导体市场的布局&#xff0c;从而满足汽车电动化、智能…

[node]Node.js 中REPL简单介绍

[node]Node.js 中REPL简单介绍 什么是REPL为什么使用REPL如何使用REPL 命令REPL模式node的全局内容展示node全局所有模块查看全局模块具体内容其它命令 实践 什么是REPL Node.js REPL(Read Eval Print Loop:交互式解释器) 表示电脑的环境&#xff0c;类似 Windows 系统的终端或…

在x64上构建智能家居(home assistant)(二)(新版Debain12)连接Postgresql数据库

新版数据库安装基本和旧版相同,大部分可以参考旧版本在x64上构建智能家居(home assistant)&#xff08;二&#xff09;连接Postgresql数据库_homeassist 数据库-CSDN博客 新版本的home assistant系统安装,我在原来写的手顺上直接修改了,需要的可以查看在x64上构建智能家居(home…

vivado 主时钟分析

主时钟 主时钟是通过输入端口或千兆位进入设计的板时钟收发器输出引脚&#xff08;例如恢复的时钟&#xff09;。主时钟只能由create_clock命令定义。主时钟必须附加到网表对象。此网表对象表示中的点所有时钟边沿源自其并在时钟树上向下游传播的设计。换句话说&#xff0c;主…

深入理解 Rust 中的容器类型及其应用

Rust 作为一种系统编程语言&#xff0c;提供了丰富的容器类型来处理各种数据结构和算法。这些容器类型不仅支持基本的数据存储和访问&#xff0c;还提供了高效的内存管理和安全性保障。本文将详细介绍 Rust 中的几种主要容器类型&#xff0c;包括它们的用法、特点和适用场景&am…

使用 Taro 开发鸿蒙原生应用 —— 探秘适配鸿蒙 ArkTS 的工作原理

背景 在上一篇文章中&#xff0c;我们已经了解到华为即将发布的鸿蒙操作系统纯血版本——鸿蒙 Next&#xff0c;以及各个互联网厂商开展鸿蒙应用开发的消息。其中&#xff0c;Taro作为一个重要的前端开发框架&#xff0c;也积极适配鸿蒙的新一代语言框架 —— ArkTS。 本文将…

力扣每日一题day37[113.路径总和ii]

给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum 22 输出&a…

推荐一个vscode看着比较舒服的主题:Dark High Contrast

主题名称&#xff1a;Dark High Contrast &#xff08;意思就是&#xff0c;黑色的&#xff0c;高反差的&#xff09; 步骤&#xff1a;设置→Themes→Color Theme→Dark High Contrast 效果如下&#xff1a; 感觉这个颜色的看起来比较舒服。

jetbrains idea 报错 java.lang.ClassNotFoundException 之后自动搜索包导入包

-- 搜索类所在的包 导入包 搜索包 mac环境 pom中右键或者 cmdn

CSS:盒子模型

CSS&#xff1a;盒子模型 盒子模型盒子模型的组成盒子内容边框 border内边距 padding盒子实际大小计算CSS3的盒子类型content-boxborder-box 外边距 margin外边距合并相邻块元素垂直外边距合并嵌套块元素垂直外边距塌陷 行内元素的内外边距 盒子相关属性圆角边框盒子阴影 盒子模…

Linux基本内容学习

Linux 命令 文件命令 命令释义语法格式lslist&#xff0c;用于显示目录中文件及其属性信息ls [参数名] [文件名]cdchange directory&#xff0c;用于更改当前所处的工作目录&#xff0c;路径可以是绝对路径&#xff0c;也可以是相对路径&#xff0c;若省略不写则会跳转至当前…