基于多反应堆的高并发服务器【C/C++/Reactor】(上)
(一)初始化服务器端用于监听的套接字
- Server.h
#pragma once
// 初始化监听的套接字
int initListenFd(unsigned short port);
- Server.c
int initListenFd(unsigned short port) {// 1.创建监听的fdint 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;}// 返回fdreturn 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_INET6src:需要转换的点分十进制的IP字符串dst:转换后的结果保存在这个里面// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);af:地址族: AF_INET AF_INET6src: 要转换的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 : ipv4AF_INET6 : ipv6AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)- type: 通信过程中使用的协议类型SOCK_STREAM : 流式协议SOCK_DGRAM : 报式协议- protocol : 具体的一个协议。一般写0- SOCK_STREAM : 流式协议默认使用 TCP- SOCK_DGRAM : 报式协议默认使用 UDP- 返回值:- 成功:返回文件描述符,操作的就是内核缓冲区。- 失败:-1int 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 : 未连接的和已经连接的和的最大值, 5int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接- 参数:- sockfd : 用于监听的文件描述符- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)- addrlen : 指定第二个参数的对应的内存大小- 返回值:- 成功 :用于通信的文件描述符- 失败 : -1int 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) {// 建立新连接 acceptacceptClient(lfd,epfd);}else{// 主要是接收对端的数据recvHttpRequest(fd,epfd);}}}return 0;
}
>>>>>>>>>>>>>>>>>>>>>>>>>>>>知识回顾>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1.epoll() 多路复用 和 两种工作模式
epoll是Linux内核中的一个事件驱动I/O机制,用于处理多个文件描述符上的事件。它是一个高效且强大的 I/O 多路复用工具,可以用于处理大量文件描述符的 I/O操作。epoll 的主要优点是它只占用较少的资源,并且比传统的 select 和 poll 更易于使用。
epoll的工作原理是通过一个事件表来跟踪所有需要监控的文件描述符,当某个文件描述符上有事件发生时,epoll会通知程序去处理这些事件。这种方式可以确保程序在等待某个文件描述符上有事件发生时只占用较少的资源,而不是像select和poll那样整个程序
----来自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博客https://heheda.blog.csdn.net/article/details/132695415WebServer 解析HTTP 响应报文-CSDN博客https://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// 请求行长度:29char 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) {// 文件不存在 -- 回复404sendHeadMsg(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// 自右向左查找 '.' 字符,如不存在返回NULLconst 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 0while (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");}}
#endifclose(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.jpgif(*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.创建监听的fdint 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;}// 返回fdreturn 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) {// 建立新连接 acceptacceptClient(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// 请求行长度:29char 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) {// 文件不存在 -- 回复404sendHeadMsg(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 0while (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");}}
#endifclose(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// 自右向左查找 '.' 字符,如不存在返回NULLconst 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.jpgif(*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);
演示效果:
相关文章:
基于多反应堆的高并发服务器【C/C++/Reactor】(上)
(一)初始化服务器端用于监听的套接字 Server.h #pragma once // 初始化监听的套接字 int initListenFd(unsigned short port); Server.c int initListenFd(unsigned short port) {// 1.创建监听的fdint lfd socket(AF_INET, SOCK_STREAM, 0);if(lf…...
腾讯云debian服务器的连接与初始化
目录 1. 远程连接2. 软件下载3. 设置开机自启动 1. 远程连接 腾讯云给的服务器在安装好系统之后,只需要在防火墙里面添加一个白名单(ip 或者域名)就能访问了。 浏览器打开https://www.ipip.net/,在左下角找到自己所用的WIFI的公…...
医保购药小程序:智能合约引领医疗数字革新
在医疗领域,医保购药小程序通过引入智能合约技术,为用户提供更为高效、安全的购药体验。本文将通过简单的智能合约代码示例,深入探讨医保购药小程序如何利用区块链技术中的智能合约,实现医保结算、购药监控等功能,为医…...
神经网络:深度学习优化方法
1.有哪些方法能提升CNN模型的泛化能力 采集更多数据:数据决定算法的上限。 优化数据分布:数据类别均衡。 选用合适的目标函数。 设计合适的网络结构。 数据增强。 权值正则化。 使用合适的优化器等。 2.BN层面试高频问题大汇总 BN层解决了什么问…...
Unity中Shader旋转矩阵(二维旋转矩阵)
文章目录 前言一、旋转矩阵的原理1、我们以原点为中心,旋转坐标轴θ度2、求 P~2x~:3、求P~2y~:4、最后得到 P~2~点 的点阵5、该点阵可以拆分为以下两个矩阵相乘的结果 二、在Shader中,使用该旋转矩阵实现围绕 z 轴旋转1、在属性面板定义 floa…...
前端面试题(计算机网络):options请求方法及使用场景
OPTIONS请求方法及使用场景 回答思路:什么是options请求-->options请求方法-->options使用场景什么是options请求?(浅入)扩展:常见的HTTP请求有什么?扩展:常见的HTTP请求的作用࿱…...
使用docker-compose管理docker服务
使用docker-compose管理docker服务 1,创建docker-compose.yml version: 3 services:javaapp:build: context: ./javaappdockerfile: Dockerfileports:- "9202:9202"- "19202:19202"goapp:build: context: ./goappdockerfile: Dockerfileports…...
Python_Tkinter和OpenCV模拟行星凌日传输光度测定
传输光度测定 在天文学中,当相对较小的天体直接经过较大天体的圆盘和观察者之间时,就会发生凌日。 当小物体移过较大物体的表面时,较大物体会稍微变暗。 最著名的凌日是水星和金星对太阳的凌日。 借助当今的技术,天文学家可以在…...
【安全】使用auparse解析auditd审计日志
使用auparse解析auditd审计日志 1 审计日志特点 查看auditd.log的日志,审计日志的格式如下: typeSYSCALL msgaudit(1703148319.954:11680975): archc000003e syscall2 successyes exit5 a01102430 a10 a21b6 a324 items1 ppid7752 pid7761 auid0 uid0…...
flink watermark 实例分析
WATERMARK 定义了表的事件时间属性,其形式为: WATERMARK FOR rowtime_column_name AS watermark_strategy_expression rowtime_column_name 把一个现有的列定义为一个为表标记事件时间的属性。该列的类型必须为 TIMESTAMP(3)/TIMESTAMP_LTZ(3),且是 sche…...
系列十二(面试)、Java中的GC回收类型有哪些?
一、Java中的GC回收类型 1.1、概述 Java中的GC回收类型主要包含以下几种,即:UseSerialGC、UseParallelGC、UseConcMarkSweepGC、UseParNewGC、UseParallelOldGC、UseG1GC。 1.2、源码...
华为数通方向HCIP-DataCom H12-831题库(多选题:201-220)
第201题 在多集群RR组网中,每个集群中部署了一台RR设备及其客户机,各集群的RR与为非客户机关系,并建立IBGP全连接。以下关于BGP路由反射器发布路由规则的描述,正确的有哪些? A、若某RR从EBGP对等体学到的路由,此RR会传递给其他集群的RR B、若某RR从非客户机IBGP对等体学…...
NLP论文阅读记录 - | 使用GPT对大型文档集合进行抽象总结
文章目录 前言0、论文摘要一、Introduction二.相关工作2.1Summarization2.2 神经网络抽象概括2.2.1训练和测试数据集。2.2.2 评估。 2.3 最先进的抽象摘要器 三.本文方法3.1 查询支持3.2 文档聚类3.3主题句提取3.4 语义分块3.5 GPT 零样本总结 四 实验效果4.1数据集4.2 对比模型…...
华为全屋wifi6蜂鸟套装标准
华为政企42 华为政企 目录 上一篇华为安防监控摄像头下一篇华为企业级无线路由器...
系列二十八、如何在Oracle官网下载JDK的api文档
一、官网下载JDK的api文档 1.1、官网地址 https://www.oracle.com/java/technologies/javase-jdk21-doc-downloads.html 1.2、我分享的api.chm 链接:https://pan.baidu.com/s/1Bf55Fz-eMTErmQDtZZcewQ?pwdyyds 提取码:yyds 1.3、参考 https://ww…...
STM32-ADC模数转换器
目录 一、ADC简介 二、逐次逼近型ADC内部结构 三、STM32内部ADC转换结构 四、ADC基本结构 五、输入通道 六、转换模式 6.1单次转换,非扫描模式 6.2连续转换,非扫描模式 6.3单次转换,扫描模式 6.4连续转换,扫描模式 七、…...
谷歌手机安装证书到根目录
1、前提你已经root,安装好面具 2,下载movecert模块,自动帮你把证书从用户证书移动成系统证书 视频教程,手机为谷歌手机 https://www.bilibili.com/video/BV1pG4y1A7Cj?p11&vd_source9c0a32b00d6d59fecae05b4133f22f06 软件下…...
代码随想录 322. 零钱兑换
题目 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 你可以认为每种硬币的数量是无限的。…...
【图的应用二:最短路径】- 用 C 语言实现迪杰斯特拉算法和弗洛伊德算法
目录 一、最短路径 二、迪杰斯特拉算法 三、弗洛伊德算法 一、最短路径 假若要在计算机上建立一个交通咨询系统,则可以采用图的结构来表示实际的交通网络。如下图所示,图中顶点表示城市,边表示城市间的交通联系。 这个咨询系统可以回答旅…...
Qt之判断一个点是否在多边形内部(射线法)
算法思想: 以被测点Q为端点,向任意方向作射线(一般水平向右作射线),统计该射线与多边形的交点数。如果为奇数,Q在多边形内;如果为偶数,Q在多边形外。计数的时候会有一些特殊情况。这种方法适用于任意多边形,不需要考虑精度误差和多边形点给出的顺序,时间复杂度为O(n)…...
压力测试过程中内存溢出(堆溢出、栈溢出、持久代溢出)情况如何解决
在压力测试过程中,可能会遇到内存溢出的问题,其中常见的包括堆内存溢出、栈内存溢出和持久代溢出。解决这类问题需要首先理解各种内存溢出的原因和特点。 堆内存溢出:这种情况通常发生在稳定性压测一段时间后,系统报错࿰…...
【工业智能】音频信号相关场景
【工业智能】音频信号相关场景 DcaseDcase introduction:dcase2024有10个主题的任务: ASD硬件设备产品商 方法制造业应用场景 zenodo音频事件检测 与计算机视觉CV相对应,计算机听觉computer audition,简称CA。 Dcase 这里推荐一个…...
(PC+WAP)装修设计公司网站模板 家装公司网站源码下载
(PCWAP)装修设计公司网站模板 家装公司网站源码下载 PbootCMS内核开发的网站模板,该模板适用于装修设计、家装公司类等企业,当然其他行业也可以做,只需要把文字图片换成其他行业的即可; PCWAP,同一个后台,…...
使用opencv实现图像中几何图形检测
1 几何图形检测介绍 1.1 轮廓(contours) 什么是轮廓,简单说轮廓就是一些列点相连组成形状、它们拥有同样的颜色、轮廓发现在图像的对象分析、对象检测等方面是非常有用的工具,在OpenCV 中使用轮廓发现相关函数时候要求输入图像是二值图像,这…...
补题与周总结:leetcode第 376 场周赛
文章目录 复盘与一周总结2967. 使数组成为等数数组的最小代价(中位数贪心 回文数判断)2968. 执行操作使频率分数最大(中位数贪心 前缀和 滑窗) 复盘与一周总结 wa穿了第3题,赛时其实想到了思路:中位数贪心…...
js指纹库,可跟踪用户唯一性
fingerprintjs官网 资料: Browserleaks - Check your browser for privacy leaks...
Shell三剑客:awk(内部变量)
一、$0 :完整的输入记录 [rootlocalhost ~]# awk -F: {print $0} passwd.txt root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/s…...
JVM中的虚拟机栈的动态链接部分存放到底是什么
在Java虚拟机(JVM)中,每个线程在执行一个方法时都会创建一个栈帧(Stack Frame),栈帧中包含了方法的运行时数据。栈帧通常包括局部变量表、操作数栈、动态链接、方法返回地址等部分。 动态链接 动态链接&a…...
Leetcode 55 跳跃游戏
题意理解: 非负整数数组 nums, 最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 需要跳到nums最后一个元素即为成功。 目标:是否能够跳到最后一个元素。 解题思路: 使用贪心算法来解题,需要理解…...
构建陪诊预约系统:技术实战指南
在医疗科技的飞速发展中,陪诊预约系统的应用为患者和陪诊人员提供了更为便捷和贴心的服务。本文将带领您通过技术实现,构建一个简单而实用的陪诊预约系统,以提升医疗服务的效率和用户体验。 技术栈选择 在开始之前,我们需要选择…...
网站被百度蜘蛛爬死了/湖人排名最新
预期服务器返回的数据类型。如果不指定,jQuery 将自动根据 HTTP 包 MIME 信息来智能判断,比如 XML MIME 类型就被识别为 XML。在 1.4 中,JSON 就会生成一个 JavaScript 对象,而 script 则会执行这个脚本。随后服务器端返回的数据会…...
成都网站建设服务密需湖南岚鸿案例/百度云搜索引擎官网入口
打开Excel->工具->宏->Viaual Basic编辑器在弹出来的窗口中对着VBAproject点右键->插入->模块下面会出现一个名为"模块1",点击在右边的空白栏中粘贴以下内容:Function getpychar(char)tmp 65536 Asc(char)If (tmp > 45217…...
建设婚恋网站/北京seo外包 靠谱
这是一个简单的jQuery代码段,每次加载时都会在您的网页上显示随机引用。 这对于在每次加载网页时显示不同的推荐信很有用,每次都会显示一个新的推荐信。 var a Math.random() "" var rand1 a.charAt(5) quotes new Array quotes[1] "…...
建设工程消防设计备案哪个网站/品牌营销策略研究
因为不知道客户端连接什么时候来,不知道客户端数据什么时候来,因此写死的服务端可能阻塞在accept,或者recv,因此必须用多进程,或者多线程处理已经连接客户端的通信。 对于多进程,当父进程获取到新的socket后,创建子进程…...
软件网站开发评估/seo关键词推广价格
饭卡 Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 514 Accepted Submission(s): 226Problem Description电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额。如果购买一个商品之前&…...
无锡新吴区建设局网站/百度公司名称
Qt 实现文件校验码生成器(内附源码) 该软件是基于 CertUtil 的一个文件文件校验码生成,旨在提高下载程序的一个安全系数,防止黑客攻击网站后,将携带病毒的程序放在下载链接上,当用户使用程序时,…...