前面写了线程池,那么现在要考虑如何去使用该线程池了
注意,到目前为止,我们还是在解决web服务器的I/O处理单元
即负责处理客户连接,读写网络数据的部分
线程池属于 Web 服务器中的工作线程部分,Web 服务器通常使用线程池来管理并复用一组预先创建的工作线程,这些工作线程负责处理客户端的请求。
服务器通常要处理:I/O 事件、信号事件以及定时事件,对应着有两种高效的事件处理模式:Reactor 和 Proactor
同步 I/O 模型通常用于实现 Reactor 模式;
异步 I/O 模型通常用于实现 Proactor 模式;
下面来分别说明一下两种模式
Reactor模式是一种基于事件驱动的软件设计模式,用于处理高并发的IO操作。
其核心思想是将I/O操作转换为事件,并使用事件处理器来处理这些事件。
在Reactor模式中,有一个称为Reactor的组件,它负责监听I/O事件,例如连接到达、数据就绪等等。当一个事件发生时,Reactor会将其派发给一个或多个事件处理器来处理。
事件处理器通过回调函数来处理事件,并且通常会使用非阻塞I/O来执行特定的操作。这样,事件处理器可以同时处理多个事件,从而实现高并发性能。Reactor模式被广泛应用于网络编程中,例如UNIX平台上的select/poll/epoll机制,Java NIO框架中的Selector类等等。
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
Reactor组件通常是一个由事件驱动的循环,它不断监听I/O事件并将其派发给相应的事件处理器来处理。Reactor模式在C++中的实现通常基于操作系统提供的select、poll或epoll等系统调用来实现。
在具体实现上,Reactor组件可以是一个类,它封装了底层的系统调用,并提供了注册、注销事件处理器、以及等待I/O事件到来等方法。当有一个或多个I/O事件就绪时,Reactor对象会返回这些事件的相关信息,例如事件类型、套接字等等。
同时,Reactor组件需要维护一个或多个事件处理器,它们负责处理不同类型的事件。事件处理器通常也是一个类,它封装了特定类型的I/O操作,并实现了相应的回调函数,例如处理连接请求、读取数据、写入数据等等。当Reactor对象收到I/O事件后,它会根据事件类型选择相应的事件处理器,并将事件信息传递给它。
综上, Reactor组件和事件处理器共同实现了Reactor模式
Proactor模式是一种并发编程模式,它的目的是实现高效的I/O操作。Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。
在Proactor模式中,所有的I/O操作都由操作系统异步执行,并且当I/O操作完成时,操作系统会通知应用程序。这样,应用程序可以继续执行其他任务而不必阻塞在I/O操作上。
在C++中,Proactor模式通常与异步IO(Asynchronous IO)一起使用。在Windows平台上,Proactor模式可以通过使用IOCP(Input/Output Completion Port)来实现。在Linux平台上,Proactor模式可以通过使用epoll或者kqueue等机制来实现。
Proactor模式相对于Reactor模式来说,更加适合处理高并发的网络应用,因为它能够提供更高的吞吐量和更低的延迟。但是,相比于Reactor模式,Proactor模式的实现难度更大,需要更多的代码和调试工作。
使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
在 Linux 中,Proactor 模式可以使用 epoll 或者 libuv 等第三方库来实现。下面分别介绍一下这两种方式的实现方法。
Linux 提供了一个高效的异步 I/O 接口 epoll,可以用于实现 Proactor 模式。使用 epoll 需要遵循以下几个步骤:
epoll_create
函数创建一个 epoll 句柄;epoll_ctl
函数将需要进行异步 I/O 的文件描述符添加到 epoll 句柄中,同时设置好事件类型;epoll_wait
函数等待事件:主线程调用 epoll_wait
函数等待事件发生,并将发生事件的文件描述符返回给主线程;#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>
void handleEvent(int fd, uint32_t events)
{
// 处理事件
if (events & EPOLLIN)
{
char buffer[1024];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
std::cout << "Read " << bytesRead << " bytes from file" << std::endl;
}
}
int main()
{
int fd = open("myfile.txt", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
std::cerr << "Failed to open file" << std::endl;
return 1;
}
// 创建 epoll 句柄
int epollFd = epoll_create(1);
if (epollFd < 0)
{
std::cerr << "Failed to create epoll" << std::endl;
return 1;
}
struct epoll_event event = {0};
event.events = EPOLLIN | EPOLLET; // 监听可读事件,并设置边缘触发模式
event.data.fd = fd;
// 添加文件描述符到 epoll 句柄中
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event) < 0)
{
std::cerr << "Failed to add fd to epoll" << std::endl;
close(fd);
return 1;
}
// 调用 epoll_wait 函数等待事件
while (true)
{
struct epoll_event events[1];
int numEvents = epoll_wait(epollFd, events, sizeof(events) / sizeof(struct epoll_event), -1);
if (numEvents < 0)
{
std::cerr << "Failed to wait for events" << std::endl;
break;
}
for (int i = 0; i < numEvents; ++i)
{
handleEvent(events[i].data.fd, events[i].events);
}
}
close(epollFd);
close(fd);
return 0;
}
在上面的例子中,我们使用 epoll_create
函数创建了一个 epoll 句柄,然后使用 epoll_ctl
函数将文件描述符添加到了 epoll 句柄中,这里监听了可读事件,并设置了边缘触发模式。主线程调用 epoll_wait
函数等待事件的发生,当事件发生时会返回对应的文件描述符,并在回调函数中处理事件。
另外一种常用的第三方库是 libuv,它提供了一系列跨平台的异步 I/O 接口,可以用于实现 Proactor 模式。使用 libuv 需要遵循以下几个步骤:
uv_loop_init
函数初始化一个 event loop;uv_fs_open
函数创建一个打开文件的异步操作对象;Reactor模式:
Reactor模式适用于需要高并发处理的应用场景,例如Web服务器、消息队列等。
Proactor模式:
Proactor模式适用于需要处理复杂请求的应用场景,例如数据库、文件存储等。
在数据流处理方面,Reactor模式和Proactor模式的实现方式不同:
Reactor模式:
Proactor模式:
从实现方式上来说,Proactor模式比Reactor模式更加高效,因为它利用了操作系统的异步I/O机制,可以让线程在I/O操作等待期间执行其他任务,从而提高CPU利用率。但是,在处理简单请求时,Proactor模式会存在一些额外的开销,例如创建多个线程、使用回调函数等。
此外,Reactor模式和Proactor模式的区别还可以从以下几个方面进行考虑:
1、调用方式不同
在Reactor模式中,应用程序需要显式地调用I/O操作,而在Proactor模式中,应用程序只需要将I/O请求发送给操作系统,并在操作完成后收到通知。这使得Proactor模式更加透明,因为它隐藏了底层实现细节。(即Proactor模式隐式调用I/O操作)
2、处理方式不同
在Reactor模式中,事件发生后需要通过事件处理器对事件进行处理,而在Proactor模式中,操作系统会自动处理事件,并将操作结果传递给应用程序。这使得Proactor模式更加高效,因为它避免了事件处理器的开销。
3、数据复制方式不同
在Reactor模式中,当数据准备好时,它需要被复制到应用程序的缓冲区中。
而在Proactor模式中,数据可以直接从操作系统的缓冲区中读取,从而避免了数据复制的开销。
4、错误处理方式不同
在Reactor模式中,当出现错误时,应用程序需要及时处理并进行适当的回退操作。
而在Proactor模式中,操作系统会自动处理错误,并提供相应的错误码,使应用程序能够更好地处理异常情况。
5、实现细节不同
在Reactor模式中,事件处理器需要使用复杂的状态机来管理事件的生命周期。
而在Proactor模式中,操作系统提供了一套完整的异步I/O框架,使得应用程序能够更轻松地实现高效的异步I/O。
在阻塞I/O情况下,使用Reactor模式可以让程序发生阻塞,等待数据准备好后再进行处理。
在此期间,程序不能执行其他任务。
相反,使用Proactor模式,程序将无需阻塞等待数据准备好,而是通过回调函数等方式来通知应用程序数据已经准备好了,这样应用程序就可以继续执行其他任务。
在非阻塞I/O情况下,使用Reactor模式需要轮询I/O操作是否完成,如果完成则进行相应的处理;
而Proactor模式则利用系统提供的通知机制来异步地处理I/O操作完成事件,从而避免了轮询的开销。
在同步I/O情况下,Reactor模式适合于处理少量连接、并发度低的场景,因为每个连接都需要消耗一个线程,对于大量连接的情况,会导致资源浪费。Proactor模式则更适合高并发的情况,因为它可以异步地处理多个I/O事件,而不会创建过多线程。
在异步I/O情况下,Reactor模式仍然需要开启线程进行异步处理,但这种线程池的方式相对于同步I/O情况下的线程池规模要小得多。
而Proactor模式则利用系统提供的异步I/O机制,在完成I/O操作之后,通过回调函数等方式来通知应用程序进行处理,从而避免了大量线程的开销。
综上所述,使用Reactor和Proactor模式都可以实现高效的I/O事件处理,但在不同的场景中需要选择合适的模式来提高性能。
主要是因为Reactor模式在处理高并发情况下的性能更加出色。
在Reactor模式中,事件的响应和处理是由应用程序来完成的,当有大量请求时,这种方式能够更好地利用CPU资源,提高系统的效率。
另外,Reactor模式更容易实现,并且可以适用于各种不同类型的应用程序。相比之下,Proactor模式需要操作系统完成事件的响应和处理,这会增加系统的开销,特别是在高负载情况下。同时,Proactor模式需要对应用程序进行大量的重构,以适应异步I/O的编程模型,这也增加了开发成本和难度。
在写I/O处理单元之前我觉得有必要弄清楚工程中有哪些文件,以及每个文件是干什么的
目前,项目的结构如下(后续会更新)
root@ubuntu:/home/ag/webserver1.5# tree
.
├── a.out //你应该知道的
├── http_conn.cpp //存放着用于处理http响应的功能函数
├── http_conn.h //存放着一堆http_conn.cpp中函数的声明以及一些静态变量和宏定义
├── locker.h //有三个类互斥锁类locker、条件变量类cond、信号量类sem,库pthread和semaphore的封装
├── main.cpp //主文件,实现http服务器的核心功能(基于Reactor模式设计)
├── noactive
│ ├── a.out
│ ├── lst_timer.h
│ └── nonactive_conn.cpp
├── resources //存放静态页面资源,供客户端请求调用
│ ├── images
│ │ └── image1.jpg
│ └── index.html
└── threadpool.h //线程池,用于处理和响应http请求报文
注:
1、这里只是先对每个文件的作用进行大致说明
2、请先忽略noactive文件夹
上一篇我们讨论了什么是线程池以及如何实现一个线程池,即完成了对threadpool.h和locker.h文件的编写
根据本篇前面对于Reactor模式和Proactor模式的讨论,至少现在有一个共识:线程池是实现Reactor模式的一个组件
从目录树中可知,main.cpp是我们这个服务器工程的核心文件(使用socket实现服务器功能),我们通过组织和设计来调用socketAPI进而实现了服务器的基本功能。具体的设计模式就是Reactor模式
使用Reactor模式就意味着我们的代码中需要实现Reactor组件和事件处理器,一个一个来讲
使用同步I/O时,所谓的"Reactor组件"就是一个死循环配合阻塞函数,不断地检测文件描述符中是否有新的事件发生,一旦有则进行相关处理
实际上,这也是最常规的基于socketAPI编写服务器的形式
先把线程池初始化一下,之后有用
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
if(argc <= 1){
printf("按照如下格式运行: %s port_number\n", basename(argv[0]));
exit(-1);
}
//创建线程池,并初始化
//来一个任务之后,要封装成一个任务对象,交给线程池去处理
threadpool<http_conn>* pool = NULL;
try{
pool = new threadpool<http_conn>;
}catch(...){
exit(-1);
}
//创建一个数组用于保存所有的客户端信息
//每当有新连接进来时,都会在 users 数组中找到一个未使用的 http_conn 对象,进行初始化并保存该连接对应的信息
http_conn* users = new http_conn[MAX_FD];
}
在初始化线程池时,输入的参数是http_conn对象,这个就是用于处理任务的任务类,或者我们称为事件处理器,其实现后面会介绍
在main.cpp中,我们需要写一个基于Reactor模式的socket通信代码
Socket 是一种用于在网络上进行通信的编程接口(API)。它允许服务器应用程序通过与客户端建立连接来接收和发送数据。当一个客户端尝试连接到服务器时,服务器应用程序会创建一个 Socket 对象,该对象可用于在服务器和客户端之间传输数据。服务器应用程序可以通过读取和写入 Socket 对象来监听来自客户端的请求并向客户端返回响应。
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
if(argc <= 1){
printf("按照如下格式运行: %s port_number\n", basename(argv[0]));
exit(-1);
}
//获取端口号,转换成整数
int port = atoi(argv[1]);
//使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
int listenfd = socket(PF_INET, SOCK_STREAM, 0);//创建用于监听的socket文件描述符
int reuse = 1;//设置端口复用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));//让多个进程绑定同一个端口,从而实现负载均衡或者高可用等功能
//存放服务器的地址信息
struct sockaddr_in address;
address.sin_family = AF_INET;//使用IPv4协议
address.sin_addr.s_addr = INADDR_ANY;//监听所有网卡的连接请求
address.sin_port = htons(port);//将端口号(大端小端)转换为网络字节序,并保存到address结构体中
bind(listenfd, (struct sockaddr*)&address, sizeof(address));//绑定服务器的地址信息
listen(listenfd, 5);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);//创建epoll对象,通过该文件描述符对 epoll 进行控制和管理(监听)
//将监听的文件描述符添加到epoll对象中
addfd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;//赋值
}
这里我们需要把监听的文件描述符 listenfd 添加到 epoll 对象中,即将它加入到内核事件表中。
需要使用一个自定义函数addfd,由于还没有实现,先将其声明出来
//添加文件描述符到epoll中
extern void addfd(int epollfd, int fd, bool one_shot);
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
if(argc <= 1){
printf("按照如下格式运行: %s port_number\n", basename(argv[0]));
exit(-1);
}
//获取端口号,转换成整数
int port = atoi(argv[1]);
//使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
int listenfd = socket(PF_INET, SOCK_STREAM, 0);//创建用于监听的socket文件描述符
int reuse = 1;//设置端口复用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));//让多个进程绑定同一个端口,从而实现负载均衡或者高可用等功能
//存放服务器的地址信息
struct sockaddr_in address;
address.sin_family = AF_INET;//使用IPv4协议
address.sin_addr.s_addr = INADDR_ANY;//监听所有网卡的连接请求
address.sin_port = htons(port);//将端口号(大端小端)转换为网络字节序,并保存到address结构体中
bind(listenfd, (struct sockaddr*)&address, sizeof(address));//绑定服务器的地址信息
listen(listenfd, 5);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);//创建epoll对象,通过该文件描述符对 epoll 进行控制和管理(监听)
//将监听的文件描述符添加到epoll对象中
addfd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;//赋值
}
下面就要开始编写Reactor组件,前面我们提到过,该组件实际上就是一个while死循环
//添加文件描述符到epoll中
extern void addfd(int epollfd, int fd, bool one_shot);
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
...
//获取端口号,转换成整数
...
//使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
...
//存放服务器的地址信息
...
//将监听的文件描述符添加到epoll对象中
...
//Reactor组件
while(true){
/*
1、阻塞等待文件描述符监听到的事件
2、遍历事件数组,判断事件类型,进行对应处理
*/
}
}
在该循环中,使用epoll_wait获取监听socket的文件描述符所返回的事件数量
这里需要获取文件描述符监听到的所有事件,然后处理所有事件
这是符合Reactor模式的要求的
下面是具体实现
//添加文件描述符到epoll中
extern void addfd(int epollfd, int fd, bool one_shot);
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
...
//获取端口号,转换成整数
...
//使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
...
//存放服务器的地址信息
...
//将监听的文件描述符添加到epoll对象中
...
//Reactor组件
while(true){//死循环不断检测有无事件发生
//具体来说就是使用epoll_wait获取监听socket的文件描述符所返回的事件数量
int num = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(num < 0 && errno != EINTR){
printf("epoll failure\n");
break;
}
//循环遍历事件数组
for(int i = 0; i < num; i++){
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
//有客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlen = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);
if(http_conn::m_user_count >= MAX_FD){
//目前连接满了
printf("服务器正忙...\n");
close(connfd);
continue;
}
//将新的客户的数据初始化,放到数组中
users[connfd].init(connfd, client_address);
}else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
//对方异常断开或错误异常
users[sockfd].close_conn();
}else if(events[i].events & EPOLLIN){
//判断是否有读事件发生
if(users[sockfd].read()){//一次性读出数据, read()
//成功读完后要交给工作线程处理
//调用线程池,追加任务
//线程池执行run函数,不断从队列去取
//取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);
}else{//读失败,关闭
users[sockfd].close_conn();
}
}else if(events[i].events & EPOLLOUT){
if(!users[sockfd].write()){
users[sockfd].close_conn();
}
}
}
}
}
在死循环中(也就是Reactor组件),epoll_wait() 函数不断地检测文件描述符epollfd上是否有 I/O 事件发生。当有可读或可写事件发生时,epoll_wait() 函数会返回一个 events 数组,并将其中的事件信息填充到数组中,遍历数组中的所有事件,根据事件类型进行相应的处理。
- epoll_wait是一个系统调用函数,用于等待文件描述符上的I/O事件;
- epollfd是通过epoll_create函数创建的epoll实例的文件描述符,它用于管理需要监视的文件描述符集合;
- listenfd是服务器应用程序使用的套接字文件描述符,它与epollfd关联,并使用epoll_ctl函数将其添加到epollfd所管理的文件描述符集合中。
epollfd代表了一个epoll实例,负责管理需要监视的文件描述符集合,而listenfd则是需要被监视的文件描述符之一,它被添加到epollfd所管理的文件描述符集合中,以便在有新的客户端连接请求时能够及时通知服务器程序。当epoll_wait函数返回时,它会将事件列表填入events数组中,告诉服务器哪些文件描述符发生了I/O事件,然后服务器应用程序根据这些事件来执行相应的操作。
遍历事件数组时,我们需要处理以下几种事件(情况):
表示有新的客户端连接请求,需要通过accept函数接受客户端连接,并将新的客户端数据初始化并存储到http_conn数组中。
if(sockfd == listenfd){
//有客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlen = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);
if(http_conn::m_user_count >= MAX_FD){
//目前连接满了
printf("服务器正忙...\n");
close(connfd);
continue;
}
//将新的客户的数据初始化,放到数组中
users[connfd].init(connfd, client_address);
}
在服务器端监听到新的客户端连接时,使用accept函数接受客户端的连接请求,并获取客户端的IP地址和端口号等信息。其中,client_address是一个sockaddr_in类型的结构体变量,用于存储客户端的IP地址和端口号等信息。在accept函数中,通过传递参数(struct sockaddr*)&client_address及其长度,接收客户端的信息,并将其保存到client_address结构体中。这里的目的就是为了获取客户端的IP地址和端口号等信息,方便后续与客户端进行通信。
表示对方异常断开或出现错误异常,需要关闭该链接,并将http_conn数组中该客户端的状态设置为关闭。
else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
//对方异常断开或错误异常
users[sockfd].close_conn();
}
调用http_conn类的read()函数一次性读取完所有请求数据,如果读取成功则将该任务对象添加到线程池的任务队列中,否则关闭该链接。
else if(events[i].events & EPOLLIN){
//判断是否有读事件发生
if(users[sockfd].read()){//一次性读出数据, read()
//成功读完后要交给工作线程处理
//调用线程池,追加任务
//线程池执行run函数,不断从队列去取
//取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);
}else{//读失败,关闭
users[sockfd].close_conn();
}
}
调用http_conn类的write()函数将响应数据发送给客户端,如果发送成功则继续等待下一个写事件发生,否则关闭该链接。
else if(events[i].events & EPOLLOUT){
if(!users[sockfd].write()){
users[sockfd].close_conn();
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include "locker.h"
#include "threadpool.h"
#include <signal.h>
#include "http_conn.h"
#define MAX_FD 65535 //最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000 //一次监听的最大事件数
//添加信号捕捉
void addsig(int sig, void(handler)(int)){//信号处理函数
struct sigaction sa;//创建信号量
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = handler;
sigfillset(&sa.sa_mask);//设置信号临时阻塞等级
sigaction(sig, &sa, NULL);//注册信号
}
//模拟proactor模式,主线程监听事件
//当有读事件产生,在主线程中一次性读出来,封装成一个任务对象(用任务类)
//然后交给子线程(线程池队列中的工作线程),线程池再去取任务做任务
//添加文件描述符到epoll中
extern void addfd(int epollfd, int fd, bool one_shot);
//从epoll删除文件描述符
extern void removefd(int epollfd, int fd);
//修改文件描述符
extern void modfd(int epollfd, int fd, int ev);
int main(int argc, char* argv[]){
//判断参数个数,至少要传递一个端口号
if(argc <= 1){
printf("按照如下格式运行: %s port_number\n", basename(argv[0]));
exit(-1);
}
//获取端口号,转换成整数
int port = atoi(argv[1]);
//对SIGPIPE信号进行处理
addsig(SIGPIPE, SIG_IGN);
//创建线程池,并初始化
//任务类:http_conn
//来一个任务之后,要封装成一个任务对象,交给线程池去处理
threadpool<http_conn>* pool = NULL;
try{
pool = new threadpool<http_conn>;
}catch(...){
exit(-1);
}
//创建一个数组用于保存所有的客户端信息
//users 数组是一个存储 http_conn 对象的数组,每个 http_conn 对象代表一个客户端连接。
http_conn* users = new http_conn[MAX_FD];
//写网路通信的代码
//创建监听的套接字
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
//tcp服务端代码
//设置端口复用(一定要在绑定之前设置)
int reuse = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
//绑定
//绑定的作用是将服务器的端口号和 IP 地址与一个套接字绑定,
//使得客户端可以通过相应的 IP 地址和端口号访问到服务器。
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);//大端小端转换为网络字节序
bind(listenfd, (struct sockaddr*)&address, sizeof(address));
//监听
//将listenfd这个socket的状态设置为监听状态,等待客户端的连接请求。
listen(listenfd, 5);
//listenfd会触发一个可读事件,从而被加入到epoll对象中,并由events数组保存。
// 创建epoll对象,事件数组,添加监听的文件描述符
// events 数组的作用是存储 epoll_wait() 函数返回的事件
epoll_event events[MAX_EVENT_NUMBER];
// 用于事件管理的epoll对象的文件描述符
int epollfd = epoll_create(5);//创建epoll对象
//将监听的文件描述符添加到epoll对象中
addfd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;//赋值
while(true){//主线程不断循环检测有无事件发生
/*epoll_wait()会等待事件的发生,一旦事件发生,
会将该事件的相关信息存储到events数组中,
主线程会遍历该数组并处理所有发生的事件。*/
//num代表检测到几个事件
//调用 epoll_wait() 函数时,我们需要将一个用于存储事件的数组传递给该函数,
//函数会将检测到的事件存储到该数组中。
//遍历该数组可以获取到每个事件对应的文件描述符以及该事件所对应的事件类型,根据不同的事件类型,我们可以采取不同的处理方式。
int num = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(num < 0 && errno != EINTR){
printf("epoll failure\n");
break;
}
//循环遍历事件数组
//注意,此时num>0,意味着事件数组events中肯定有元素在,即检测到有事件发生
for(int i = 0; i < num; i++){
//从事件数组中取出epoll_wait()检测到的事件,即客户端的连接请求
/*当events中存储的事件为sockfd可读事件时,表示该socket有数据可读,
此时应该将读事件交由工作线程去处理。
当events中存储的事件为sockfd可写事件时,表示该socket可以写入数据,
此时应该将写事件交由工作线程去处理*/
int sockfd = events[i].data.fd;//sockfd只是一个名称,表示由epoll_wait()等待并检测到的事件
/*在服务器中,通常会使用一个监听socket(listenfd)来接受客户端的连接请求,
当有新的客户端连接到来时,服务器会使用accept函数创建一个新的连接socket(connfd),
这个新的socket会与客户端的socket建立起通信连接。*/
if(sockfd == listenfd){
//有新的客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlen = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);
if(http_conn::m_user_count >= MAX_FD){
//目前连接满了
printf("服务器正忙...\n");
close(connfd);
continue;
}
//将新的客户的数据初始化,放到数组中
/*每当有一个新的客户端连接请求到来时,
服务器会创建一个新的 http_conn 对象,并将该对象添加到 users 数组中,
以管理这个客户端连接。*/
users[connfd].init(connfd, client_address);
}else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
//对方异常断开或错误异常
users[sockfd].close_conn();
}else if(events[i].events & EPOLLIN){
//判断是否有读事件发生
if(users[sockfd].read()){//一次性读出数据, read()
//成功读完后要交给工作线程处理
//调用线程池,追加任务
//线程池执行run函数,不断从队列去取
//取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);//将 users + sockfd 所指向的 http_conn 对象追加到线程池的任务队列中。
/*users 数组中的每个元素都代表一个客户端连接,
数组的下标是该客户端的文件描述符 fd。
users + sockfd 就是获取到该客户端连接的 http_conn 对象的指针。
然后将该指针作为参数,调用线程池对象的 append 函数,
将该指针所指向的 http_conn 对象添加到线程池的任务队列中,
等待线程池的工作线程来处理。*/
}else{//读失败,关闭
users[sockfd].close_conn();
}
}else if(events[i].events & EPOLLOUT){
if(!users[sockfd].write()){
users[sockfd].close_conn();
}
}
}
}
close(epollfd);
close(listenfd);
delete[] users;
delete pool;
return 0;
}
在讨论遍历事件数组并对事件进行相应处理前,需要先明确一下epoll_wait()是如何利用listenfd的,这有助于理解事件处理机制
epoll_wait() 函数没有直接使用 listenfd 进行监听,而是使用了 events 数组来保存事件。
epoll_ctl() 函数(位于addfd函数中)将 listenfd 加入到 epoll 实例的监听队列之后,它会自动返回 EPOLLIN 事件,表示这个套接字已经准备好可以进行读操作,因此在 events 数组中添加了一个 epoll_event 结构体,其中包含了 EPOLLIN 事件和 listenfd 的文件描述符。
当有新的客户端连接请求时,内核会检测到 listenfd 文件描述符上的 EPOLLIN 事件,并将其加入到 events 数组中。随后,在调用 epoll_wait() 函数时,会检测到 events 数组中的 EPOLLIN 事件,并返回对应的文件描述符 connfd,即新建立的客户端连接套接字。
所以说,虽然 epoll_wait() 函数没有直接利用 listenfd 进行监听,但是它通过事件机制实现了对 listenfd 的间接监听,并且可以准确地处理新的客户端连接请求。
现在开始实现事件处理器,也就是工程中的http_conn.cpp和http_conn.h
本质上讲,http_conn.cpp中就是定义一堆在主函数main中会使用到的函数和参数变量
在进入Reactor组件的循环之前我们对线程池进行了初始化,将事件处理器对象(http_conn*)作为参数输入到线程池中
并且创建了一个users数组(http_conn* users)用于保存所有客户端信息,其中每个元素就是一个http_conn*,代表一个客户端连接
鉴于此,我们应该先关注事件处理器的初始化部分
// 初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in& addr){
m_sockfd = sockfd;
m_address = addr;
// 端口复用
int reuse = 1;
setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
addfd(m_epollfd, sockfd, true);//添加到epoll对象中
m_user_count++;//http_conn对象初始化后,总用户数加1
init();//为什么要分开?因为下面的init中的信息后面还要单独初始化使用
}
void http_conn::init(){
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE; // 初始状态为检查请求行
m_linger = false; // 默认不保持链接 Connection : keep-alive保持连接
m_method = GET; // 默认请求方式为GET
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
bzero(m_read_buf, READ_BUFFER_SIZE);//把m_read_buf全置为0
bzero(m_write_buf, READ_BUFFER_SIZE);
bzero(m_real_file, FILENAME_LEN);
}
初始化函数可以提供两个,一个接收初始化连接时的文件描述符作为参数,另一个函数是无参数的重载版本,仅用于初始化参数
在介绍 Reactor组件 的编写时,主循环中主要负责处理4种事件,具体落实处理操作的函数均定义在事件处理器中
下面就按照4种事件对事件处理器进行拆解分析,可以对照之前的来看
也就是listenfd有读事件发生呗
if(sockfd == listenfd){
//有新的客户端连接进来
}
在网络编程中,每个连接都会被分配一个唯一的标识符,这个标识符通常是一个整数,也就是套接字描述符(socket descriptor)。当服务器收到客户端的连接请求后,会创建一个新的套接字,生成一个新的套接字描述符,该描述符作为客户端连接的标识符。
//有新的客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlen = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlen);
此时我们已经通过accept函数接受客户端的连接请求,并返回一个文件描述符connfd。connfd
是一个整数类型的变量,它表示新创建的套接字描述符。该描述符用于建立与客户端之间的数据通信连接。
具体来说,当服务器程序调用accept函数时,它会从等待队列中取出一个已经完成三次握手协议的客户端连接请求,并创建一个新的套接字描述符以用于与该客户端进行通信。这个新的套接字描述符(也就是connfd
)可以视为服务器与客户端之间的一条虚拟电缆,通过它可以进行双向数据传输。服务器程序可以使用该描述符向客户端发送数据,也可以从该描述符读取客户端发送来的数据。
在程序开始运行时,主线程首先创建了一个 epoll 对象,并将监听套接字(
listenfd
)添加到该对象中。当有新的客户端连接请求到达服务器时,内核会将连接请求加入到未完成连接队列中,然后向客户端发送 SYN-ACK 响应包并等待客户端的 ACK 确认包。当客户端发送 ACK 确认包到达服务器时,内核将连接从未完成连接队列中移动到已完成连接队列中,此时主线程通过调用epoll_wait()
函数检测到了该事件,并调用accept()
函数从已完成连接队列中获取到了连接请求。因此,在我们的代码中,accept()
函数是通过监听套接字所在的 epoll 对象来获取到已完成连接队列中的客户端连接请求的。需要注意的是,未完成连接队列和已完成连接队列都是内核维护的队列,程序无法直接访问它们。而我们可以通过设置监听套接字和调用
accept()
函数来与这些队列进行交互。
users[connfd].init(connfd, client_address);
users
数组存储了所有已经连接的客户端信息。users[i]
表示第 i
个客户端的信息。其中,connfd
是当前发生事件的客户端套接字描述符,我们可以通过遍历 users
数组找到对应的客户端信息,然后进行相应的处理。具体来说,我们可以将 connfd
和 users
中的所有元素的 fd
进行比较,找到连接对应的客户端信息,并调用事件处理器中的初始化函数对该http_conn对象进行初始化。
else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){
//对方异常断开或错误异常
users[sockfd].close_conn();
}
events[i]
是一个epoll_event
结构体,它的events
成员变量存储着当前的事件类型,例如EPOLLIN、EPOLLOUT、 EPOLLRDHUP等
events[i].events
表示获取第i
个事件对象的事件类型集合
该情况中,如果发生了EPOLLRDHUP、EPOLLHUP或EPOLLERR事件,就执行对应的操作
EPOLLRDHUP表示TCP连接的远程端关闭或半关闭连接,即对方关闭了socket连接或者shutdown写操作。
EPOLLHUP表示挂起的连接或监听套接字已经关闭。它可能是一个错误,也可能是一个正常情况,因为它只代表文件描述符不再可用,而不是一定有错误。
EPOLLERR表示错误事件。例如:socket被对端重置(rst);对于udp的epoll来说,他可以支持多个端口绑定,当然你不能bind两次同一个端口,那么第二次就会返回-1并且errno会被设置为EADDRINUSE;还有就是当读取时没有数据则返回-1并且errno被设置为EAGAIN 。
读事件是指读取客户端发送到服务器的数据,也就是尝试对监听的文件描述符进行读取
else if(events[i].events & EPOLLIN){
//判断是否有读事件发生
if(users[sockfd].read()){//一次性读出数据, read()
//成功读完后要交给工作线程处理
//调用线程池,追加任务
//线程池执行run函数,不断从队列去取
//取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);//将 users + sockfd 所指向的 http_conn 对象追加到线程池的任务队列中。
/*users 数组中的每个元素都代表一个客户端连接,
数组的下标是该客户端的文件描述符 fd。
users + sockfd 就是获取到该客户端连接的 http_conn 对象的指针。
然后将该指针作为参数,调用线程池对象的 append 函数,
将该指针所指向的 http_conn 对象添加到线程池的任务队列中,
等待线程池的工作线程来处理。*/
}else{//读失败,关闭
users[sockfd].close_conn();
}
}
事件为EPOLLIN代表文件描述符可用,尝试对其进行读取
此时需要调用事件处理器中定义的bool http_conn::read()
// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read(){
// printf("一次性读完数据\n");
if(m_read_idx >= READ_BUFFER_SIZE){
return false;
}
int bytes_read = 0;//读取到的字节
while(true){//开始保存数据
// 数组起始位置(0)+已经读的数据位置(100)=下次开始写的位置(101)
// 从m_read_buf+ m_read_idx索引出开始保存数据,大小是READ_BUFFER_SIZE - m_read_idx
bytes_read = recv(m_sockfd, m_read_buf+ m_read_idx,
READ_BUFFER_SIZE - m_read_idx, 0);
if(bytes_read == -1){
if(errno == EAGAIN || errno == EWOULDBLOCK){
// 没有数据
break;
}
return false;
} else if(bytes_read == 0){ // 对方关闭连接
return false;
}
m_read_idx += bytes_read;//读到数据,更新索引
}
// printf("读到了数据:%s\n", m_read_buf);
return true;
}
该函数会从对应文件描述符(即m_sockfd)上读取数据,一次性读取尽可能多的数据,并将读取到的数据保存到m_read_buf中,返回值表示是否成功读取数据。
首先判断m_read_idx是否越界,若已经超过了缓冲区大小,则返回false;然后使用while循环不断读取数据,每次最多读取READ_BUFFER_SIZE - m_read_idx个字节,并将读取到的数据保存到m_read_buf+ m_read_idx位置处;如果读取失败,则判断错误码是否为EAGAIN或EWOULDBLOCK,如果是则表示暂时没有更多的数据可读,直接跳出循环,并返回true;如果是其他错误码或者读取到的数据长度为0,则表示连接出错或对方关闭了连接,直接返回false。最后如果成功读取到数据,则更新m_read_idx的值。
1、数据保存到m_read_buf + m_read_idx位置处是因为该位置代表了当前读取缓冲区的未被使用的空间。每次从套接字接收数据时,将数据存储到该位置,并将已读取数据的长度增加(bytes_read),更新m_read_idx的值,以便下一次接收数据时可以从新的位置开始存储数据。
这种方法的优点是,可以避免在读取缓冲区中重复使用已经读取过的数据,从而保证数据的完整性和正确性。
2、recv()是一个系统调用,用于从指定的套接字上接收数据。它的作用是从网络中读取一定长度的数据,并将其放入缓冲区中以供进一步处理。
其中,参数意义如下:
- sockfd:需要接收数据的套接字描述符。
- buf:接收数据缓冲区的地址。
- len:缓冲区的长度。
- flags:接收数据时的可选参数。
3、注意,read()函数是在事件处理器http_conn中定义的,因此读取到的数据保存在http_conn对象的缓冲区中
将数据读取到缓冲区之后,此时的http_conn对象就具有了完整的信息,主线程会将这个封装好的http_conn对象作为参数,调用线程池的append()函数,往任务队列中追加任务。
pool->append(users + sockfd);
线程池中的工作线程会不断从任务队列中取出任务,取到后就执行run()函数(没错,线程池在这里被用上了)
这里的run()函数会调用http_conn对象的process()函数来处理业务逻辑,例如解析HTTP请求、生成HTTP响应等操作。
http_conn::process()
函数的定义很简单
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process(){
// 解析HTTP请求
HTTP_CODE read_ret = process_read();
if(read_ret == NO_REQUEST){
modfd(m_epollfd, m_sockfd, EPOLLIN);
return;
}
// 生成响应
bool write_ret = process_write(read_ret);
if(!write_ret){
close_conn();
}
modfd(m_epollfd, m_sockfd, EPOLLOUT);
}
这里会去解析具体的HTTP请求并生成响应
HTTP_CODE是在http_conn.h中定义的一个枚举类型,代表了服务器在解析HTTP请求后应该提供的返回值
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
下面来单独说一下解析请求的函数process_read()
http_conn::process_read()
是一个HTTP请求处理函数,用于解析HTTP请求报文。主要功能如下:
http_conn::HTTP_CODE http_conn::process_read(){
//定义初始状态
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char* text = 0;
//解析一行数据,得到不同状态
//OK表示正常
//主状态机 && 从状态机 || 解析到一行完整数据(或者请求体)
//这里的主状态机指的是process_read()函数,从状态机指的是parse_line()
while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK))
|| ((line_status = parse_line()) == LINE_OK)){
// 获取一行数据
text = get_line();
m_start_line = m_checked_idx;
printf("获取到一行http数据: %s\n", text);
switch (m_check_state){
case CHECK_STATE_REQUESTLINE: {
//解析请求行,也就是GET中的GET
/*通过请求行的解析我们可以判断该HTTP请求的类型(GET/POST),
而请求行中最重要的部分就是URL部分,
我们会将这部分保存下来用于后面的生成HTTP响应*/
ret = parse_request_line(text);
if(ret == BAD_REQUEST){
return BAD_REQUEST;
}
break;//正常解析就break
}
case CHECK_STATE_HEADER: {
ret = parse_headers(text);//解析请求头,GET和POST中空行以上,请求行以下的部分
if(ret == BAD_REQUEST){
return BAD_REQUEST;
} else if(ret == GET_REQUEST){//遇到换行符就默认你解析完请求头,不管后面还有没有内容
return do_request();//解析具体的请求信息
}
break;
}
case CHECK_STATE_CONTENT: {
/*解析请求数据,对于GET来说这部分是空的,
因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;
只有POST的这部分是有数据的*/
ret = parse_content(text);
if(ret == GET_REQUEST){
/*do_request()需要做:
需要首先对GET请求和不同POST请求
(登录,注册,请求图片,视频等等)做不同的预处理,
然后分析目标文件的属性,若目标文件存在、对所有用户可读且不是目录时,
则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功。*/
return do_request();
}
line_status = LINE_OPEN;
break;
}
default: {
return INTERNAL_ERROR;
}
}
}
return NO_REQUEST;//主状态机请求不完整
}
这里要单独说一下主从状态机模式,http_conn::process_read()
就是根据这个模式设计的,用以应对不同的状态处理
在主从状态机模式中,一个状态机作为主要的控制器,而其他状态机则被设计为从状态机。主状态机负责协调和管理所有子状态机,并将它们之间的通信和事件处理进行协调。通常,主状态机是在系统启动时创建的,而从状态机则可以在系统运行时动态创建、注册或注销。
主状态机 process_read()
中进行循环,直到解析完整个 HTTP 请求数据。
具体地说,(line_status = parse_line())
表示调用从状态机 parse_line()
,并将返回值赋给变量 line_status
。如果解析成功,则 line_status
的值为 LINE_OK
,否则为其他值(如 LINE_OPEN
、LINE_BAD
等)。
而 m_check_state
表示当前 HTTP 请求的解析状态,有三种可能:CHECK_STATE_REQUESTLINE
表示正在解析请求行,CHECK_STATE_HEADER
表示正在解析请求头,CHECK_STATE_CONTENT
表示正在解析请求体。因此 m_check_state == CHECK_STATE_CONTENT
表示当前正在解析请求体。
当且仅当解析到请求体时,才需要进一步判断是否已经解析完整个 HTTP 请求数据。
因此 (m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)
表示如果当前正在解析请求体,并且从状态机已经成功解析了一行请求数据,则需要继续解析下一行请求数据。
而 while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)) || ...)
表示只要解析状态还处于请求体状态且从状态机已经成功解析出一行请求数据,或者从状态机还未成功解析出一行完整的请求数据,就需要不断循环调用从状态机、解析请求数据,直到解析完成整个 HTTP 请求数据。
之后的工作就是根据对应状态,编写处理函数,这部分都类似就没什么好说的了,主要就是一些文本解析的工作
写事件是指服务器向客户端“写”数据,也就是服务器向客户端发送数据(即 HTTP 响应报文)
这主要依靠http_conn::write()
来实现
// 写HTTP响应
bool http_conn::write(){
int temp = 0;
if(bytes_to_send == 0){
// 将要发送的字节为0,这一次响应结束。
modfd(m_epollfd, m_sockfd, EPOLLIN);
init();
return true;
}
while(1){
// 分散写
temp = writev(m_sockfd, m_iv, m_iv_count);
if(temp <= -1){
// 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
// 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
if(errno == EAGAIN){
modfd(m_epollfd, m_sockfd, EPOLLOUT);
return true;
}
unmap();
return false;
}
bytes_have_send += temp;//已发送的字节数
bytes_to_send -= temp;//剩余需要发送的字节数
if(bytes_have_send >= m_iv[0].iov_len){
m_iv[0].iov_len = 0;
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
m_iv[1].iov_len = bytes_to_send;
}
else{
m_iv[0].iov_base = m_write_buf+ bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - temp;
}
if(bytes_to_send <= 0){
// 没有数据要发送了
unmap();
modfd(m_epollfd, m_sockfd, EPOLLIN);
if(m_linger){
init();
return true;
}
else{
return false;
}
}
}
}
该函数实现了分散写的功能,也就是把要发送的数据分为多个部分进行传输
首先,在函数开始时,会判断是否存在字节需要发送,如果没有,则结束此次响应。
接着,通过 writev() 函数进行分散写操作,将 m_iv 数组中保存的多个缓冲区的数据写入到套接字中。其中,temp 表示本次写入的字节数量,bytes_have_send 记录已经发送的字节数,bytes_to_send 则表示剩余需要发送的字节数。
writev() 函数是 Linux 系统中的一个系统调用,其作用是将多个缓冲区的数据一起写入到文件描述符中(各个缓冲区的数据可以是不连续的)。它使用了分散写操作,即将要写入的数据分散在多个缓冲区中,通过一次系统调用实现写入操作。
在每次写入操作完成后,都会更新 m_iv 数组及 bytes_have_send、bytes_to_send 变量的值,以便继续下一轮写入操作。
如果在写入过程中出现错误,例如 TCP 写缓冲区满,会返回 false 并关闭连接;
如果写入成功,但还有数据未发送完,会继续进行分散写操作,直到所有数据都发送完毕。
最后,当所有数据都发送完毕时,会判断是否需要保持连接(即 Linger),如果需要则初始化连接并返回 true,否则返回 false 结束连接。
至此,事件处理器的主要流程介绍完毕,当然还有很多细枝末节的点没有说明,当然那个不是本文的重点,也应该交给更专业的书籍去介绍
以上是webserver中I/O处理单元和任务类的主要流程的代码介绍,主要介绍了服务器核心代码编写时的两种模式(如何去高效处理客户端发送过来的事件)
此外,我们重点讨论了在Reactor模式下如何设计一个服务器的I/O处理单元,如何编写用于处理请求事件的任务类(事件处理器)
服务器的基本功能已经完成了,但是这个服务器有很多点需要改进
比如,客户端如果不主动关闭,服务器是不会主动把连接关闭的,即使当前该连接上没有任何的数据传输行为
显然这不太合理,因此下一步我们需要对此进行改进
v4.7.1版本主要是一个Bug修复版本,没有向下不兼容改动。兼容了PHP8.1版本为SWOOLE_HOOK_CURL支持了CURLOPT_RESOLVE选项支持了形如HOST:PORT:ADDRESS、[+]HOST:PORT:ADDRESS、[-]HOST:PORT:ADDRESS和多地址的格式useSwoole\Coroutine; useSwoole\Runtime; Runtime::enableCoroutine(SWOOLE_HOOK_CURL); Coroutine\run(function(){ $host='httpbin.org'; $url='https://httpbin.org/get'; $ip=Coroutine::gethostbyname($host); $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); curl_setopt($ch,CURLOPT_RESOLVE,[
逻辑思维在现实生活中的作用是非常大的。培养逻辑思维,能够游刃有余的解决很多问题。在科技发展的今天,计算机也是有逻辑思维的,而且它的路逻辑思维和能力甚至比人类还要强大。逻辑运算符相信大家并不陌生,尤其对于程序员来说,在工作中时常会用到逻辑运算符。今天就来一起了解一下逻辑运算符是什么?逻辑运算符一、逻辑运算符涵义逻辑运算符,顾名思义,是逻辑运算或者逻辑命题中的重要连接符号。不难发现,在实际应用中,它的主要作用就是把简单的语句给连接到一起,从而形成一个相对比较复杂的语句,或者说一些简单的命题通过这种特殊的编程方式的组合,可以变成一个复杂的命题。两个语句也会因为对逻辑运算符的应用而变成复合语句。二、逻辑运算符的种类一般来说,常用的逻辑运算符有4种,在使用的过程中,要明确区分好所要使用的种类。第一种就是对操作数进行取反的逻辑非,逻辑非和其他三种逻辑运算符一样,都是可以应用于数值和字符的,也可以应用于表达式。第二种就是逻辑与,这一种的主要特点就是两个操作数必须要大于0。第三种是逻辑或,第四种是逻辑异或,异或的特点是两个操作数都不能等于0或者说是都要等于0。每一种逻辑运算符在应用的时候都是有区别的。
在营销圈,有句名言叫做‘contentistheking’,通过这句话我们就能知道内容的重要性不言而喻,那么我们怎么才能创造出高质量的网站内容呢?根据我的经验,大概可以从三个维度出发。可读性,有价值和符合搜索引擎抓取标准。1.可读性也可以叫做阅读体验,举个极端的例子,一篇长文章没有分段,也没有排版,试问有几个人愿意读下去。相反,合理运用标题,字体大小颜色,粗细,斜体等属性让你的文章看起来有条理,让人有阅读下去的欲望。a)添加内容目录如果你的文章内容比较长,段落比较清晰,可以为你的文章添加阅读目录(可以通过插件实现),文章的目录可以帮助用户很快找到他们想要的文字内容,这样就极大的提高了用户体验。b)合理使用排版工具合理的排版可以提高用户的阅读体验。比如重点的文字我们可以加粗,特殊的说明我们可以为文字添加颜色。对于标题合理使用H标签。c)图文混排一篇优秀的seo文章是离不开图片的陪衬的。图文混排不仅可以增加阅读体验,而且还能提高谷歌对文章的理解,有利于网站的排名。网站图片的命名和alt属性必须要合理添加《怎么写图片的alt属性》另外,就是要了解老外的思维方式,外国人思维比较理性,逻辑性比较
出品丨TeacherWhat关键字:Oracle、SQL、调优、诊断、手把手数据库入门、Database正文约2000字,建议阅读时间5分钟 目录结构:1.如何定位SQL问题2.SQL相关的问题类别3.诊断SQL性能问题需要的相关信息4.基本信息5.获取执行计划的主要方法和工具本公众号文章仅代表个人观点,与任何公司无关。 SQL调优和诊断(一)概述本系列文章将介绍OracleSQL调优和诊断的基本方法和相关工具的使用。本文作为概要,包括如何定位SQL问题、SQL相关的问题类别以及诊断SQL性能问题需要的相关信息。如何定位SQL问题我们在解决SQL相关问题时,需要像解决数据库全体性能问题时一样,自底(OS)向上一步一步进行缩小范围(NarrowDown),做到有的放矢。个人非常赞同ChristianAntognini在其TroubleshootingOraclePerformance一书中介绍的定位过程,如下图:▲摘自TroubleshootingOraclePerformance,2ndEditionChristianAntognini一般情况下,定位过程如下:1.首先排除数据库以外的
针对ARM-Linux程序的开发,主要分为三类:应用程序开发、驱动程序开发、系统内核开发,针对不同种类的软件开发,有其不同的特点。 今天我们来看看ARM-Linux开发和MCU开发的不同点,以及ARM-Linux的基本开发环境。1.ARM-Linux应用开发和单片机开发的不同 这里先要做一个说明,对于ARM的应用开发主要有两种方式:一种是直接在ARM芯片上进行应用开发,不采用操作系统,也称为裸机编程,这种开发方式主要应用于一些低端的ARM芯片上,其开发过程非常类似单片机,这里不多叙述。 还有一种是在ARM芯片上运行操作系统,对于硬件的操作需要编写相应的驱动程序,应用开发则是基于操作系统的,这种方式的嵌入式应用开发与单片机开发差异较大。ARM-Linux应用开发和单片机的开发主要有以下几点不同: (1)应用开发环境的硬件设备不同 单片机:开发板,仿真器(调试器),USB线; ARM-Linux:开发板,网线,串口线,SD卡; 对于ARM-Linux开发,通常是没有硬件的调试器的,尤其是在应用开发的过程中,很少使用硬件的调试器,程序的调试主要是通过串口进行调试的;但是需要说明的是,对于
已经有好些日子没有总结了,不是变懒了,而是我一直在奋力学习springboot的路上,现在也算是完成了第一阶段的学习,今天给各位总结总结。 之前在网上找过不少关于springboot的教程,都是一些比较粗糙的文章,就连百度百科也是少的可怜,所以进度一直跟不上计划。下面根据我这几天的学习和摸索,谈谈我对spring和springboot的区别,以及很多业界人士说它的快速开发,到底是快在哪儿,方便在哪儿?首先我认为在项目的架构搭建方面变得极其利索,不再需要像之前一样整合ssh或ssm那样进行一大堆的配置文件,他只是通过一个application入口类来配置所有的配置项,包括spring的一些默认配置项;其次springboot它没有太多自己的特性,没有完全颠覆之前的开发模式,反而提供了更加便捷的方式来集成了原来的开发模式,只能说换了一种快速的方式来提高开发速度。 废话我也不多说了,也说不了,后面我会把我这几天的整合过程详细给大家总结下来,初次接触如有地方有误的,望及时指正。项目结构详解:1、pom文件详解pom文件中各项依赖的作用见代码注释。<projectxmlns=&
1.接口描述接口请求域名:iecp.tencentcloudapi.com。 编辑边缘节点标签 默认接口请求频率限制:20次/秒。 APIExplorer提供了在线调用、签名验证、SDK代码生成和快速检索接口等能力。您可查看每次调用的请求内容和返回结果以及自动生成SDK调用示例。 2.输入参数以下请求参数列表仅列出了接口请求参数和部分公共参数,完整公共参数列表见公共请求参数。 参数名称 必选 类型 描述 Action 是 String 公共参数,本接口取值:ModifyEdgeNodeLabels。 Version 是 String 公共参数,本接口取值:2021-09-14。 Region 否 String 公共参数,本接口不需要传递此参数。 EdgeUnitId 是 Integer IECP边缘单元ID NodeId 是 Integer IECP边缘节点ID Labels.N 是 ArrayofKeyValueObj 标签列表 3.输出参数 参数名称 类型 描述 RequestId String 唯一请求ID,每次请
北漂程序员边城的幸福生活 边城 赵边城是我的发小,我们上了同样的小学和中学,又同时来到北京。 赵边城毕业后先后加入了BAT这种重量级别的互联网公司。 赵边城从十五年前,就开始写软件了。为了一个女人。 春节同学聚会相遇,相约同行回京,一路瞎扯,绿水青山不再,少年壮志未酬。 一、火车票 “把机票退了吧,明天和我一起坐火车回北京去!” 边城坐在沙发角上,眨眨眼殷切地望着我。KTV里中学同学聚会刚散去,满地的烟头和果壳,桌上杯盘狼藉。 “别纠结了,就我们俩回北京,都多少年了,再一起坐次火车嘛!” “我来帮你买车票,你赶紧退机票,整体还能便宜点。火车多费不了几个钟头的。我跟你讲,到了首都机场,排队等出租都得排上两个多钟头……快点快点,给你用下我做的软件,抢车票好方便的说……” 我还在犹豫,边城就已经麻利地从他的双肩背里掏出笔记本,用iPhone大方地开了个热点,他打开总挂在嘴边的抢票软件,要动真格买车票了。 “今天买明天去北京的车票,买不到的吧,返程高峰,早卖光了吧?” “你讲的对,的确不好抢,概率很小的,不过我做的这个抢票软件会一直持续刷退票的,正好碰下运气嘛,如果抢到了,就是天
题目描述 定义一个长为k的序列A1,A2,…,Ak的权值为:对于所有1≤i≤k,max(A1,A2,…,Ai)有多少种不同的取值。 给出一个1到n的排列B1,B2,…,Bn,求B的所有非空子序列的权值的m次方之和。 答案对109+7取模。 输入描述 第一行两个整数n、m。 接下来一行n个整数,第i个整数为Bi。 输出描述 输出一个整数,表示答案。 示例1 输入 32 132 输出 16 说明 在所有非空子序列中: (1),(3),(2),(3,2)权值为1, (1,3),(1,2),(1,3,2)权值为2。 那么所有非空子序列权值的2次方和为4×12+3×22=16。备注: 对于前10%的数据,n≤20。 对于前20%的数据,n≤100。 对于前40%的数据,n≤1000。 对于另外20%的数据,m=1。 对于所有数据,1≤n≤105,1≤m≤20,保证B是1到
Vector(向量):C++中的一种数据结构,确切的说是一个类。它相当于一个动态的数组,当程序员无法知道自己需要的数组的规模多大时,用其来解决问题可以达到最大节约空间的目的。 用法: 1.文件包含: 首先在程序开头处加上#include<vector>以包含所需要的类文件vector。 还有一定要加上usingnamespacestd; 2.变量声明: 2.1例:声明一个int向量以替代一维的数组:vector<int>a;(等于声明了一个int数组a[],大小没有指定,可以动态的向里面添加删除)。 2.2例:用vector代替二维数组.其实只要声明一个一维数组向量即可,而一个数组的名字其实代表的是它的首地址,所以只要声明一个地址的向量即可,即:vector<int*>a。同理想用向量代替三维数组也是一样,vector<int**>a;再往上面依此类推。 3.具体的用法以及函数调用: 3.1得到向量中的元素和数组一样,例如: vector<int*>a intb=5; a.push_back(b);//该函数下面有详解 cou
接触WSL2过程中整理沉淀的一些知识点,大纲如下,内容比较多,详细内容参考 https://www.yuque.com/wushifengcn/kb/mbg1b5 欢迎感兴趣者补充和提出问题,共同学习。 基础和背景 缘起 命令行/CLI WindowsCLI DOS命令行 PowerShell WSL Windows上运行linux程序的方式简介 虚拟机隔离模式 软件跨平台 Cygwin MinGW MSYS WSL WSL WSL版本 WSL1 WSL2 WSL2的文件系统 安装和使用 已有安装的WSL版本如何区分版本 安装条件 界面安装 命令行安装 LinuxKernel安装 LinuxDistribution Ubuntu20的运行说明 执行命令 关闭 更多用法 配置文件 wsl.conf .wslconfig 参考 互操作性 Windows访问Linux 文件 管道的使用 windows输出管道进入linux linux输出管道进windows windows访问linux的服务 MSTSC访问Linux桌面例子 使用浮动的IP 设置固定
硬盘的物理组成:由许许多多的圆形硬盘盘所组成。宜居硬盘盘能够容纳的数据量,而有所谓的单碟或者多碟。 首先,硬盘里一定会有所谓的磁头(Head)在进行该硬盘上面的读写动作,而磁头是固定在机械手臂上的,机械手臂上有多个磁头可以进行读取的动作。而当磁头固定不动,硬盘转一圈所画出来的圆就是所谓的磁道(Track)。而一个硬盘中可能具有多个硬盘盘,所有硬盘盘上面相同半径的那一个磁道就组成了所谓的磁柱(Cylinder)。 1、基本概念 在计算整个硬盘的存储量时,简单的计算公式就是:CylinderXHeadXSectorX512Bytes。另外,硬盘在读取时,主要是硬盘盘会转动,利用机械手臂将磁头移动到正确的数据位置。然后将数据依序读出。由于机械手臂上的磁头与硬盘盘的接触是很细微的空间,如果有抖动或者是脏污在磁头与硬盘盘之间时,就会造成数据的损毁或者是实体硬盘整个损毁。 硬盘的分隔(Partition),为什么要进行磁盘分隔?因为我们必须告诉操作系统,可以存储的区域是由A磁柱到BB磁柱,如此一来,操作系统才能够控制磁盘磁头去A-B范围内的磁柱存取数据。也就
1、安装Vim和Vim基本插件首先安装好Vim和Vim的基本插件。这些使用apt-get安装即可:sudoapt-getinstallvimvim-scriptsvim-doc 2.安装vim配置,将vim.vimrc文件中的全删除,换上下面的配置 怎么删除vim.vimrc中的配置呢? 首先打开文件,再按ctrl+h,找到vim.vimrc,删除里面的配置。 接下来打开终端,输入命令vim.vimrc,粘贴下面配置 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" "显示相关 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" "setshortmess=atI"启动的时候不显示那个援助乌干达儿童的提示 "winpos55"设定窗口位置 "setlines=40columns=155"设定窗口大小 "setnu"
首先要清楚棋盘行列表示方法。 注意要求按字典序输出,我的搜索方向是dx[],dy[]. dfs初步,具体看代码。 1#include<iostream> 2#include<stdio.h> 3#include<cstring> 4usingnamespacestd; 5intvisit[10][10]; 6 7structStep 8{ 9intx,y; 10}; 11Stepstep[100]; 12intp,q;//p是行(数字),q是列(字母) 13intdx[8]={-1,1,-2,2,-2,2,-1,1};//要满足字典序,顺序不能改 14intdy[8]={-2,-2,-1,-1,1,1,2,2}; 15booldfs(inti,intj,intno) 16{ 17visit[i][j]=1; 18step[no].x=i; 19step[no].y=j; 20if(no==p*q) 21returntrue; 22for(intk=0;k<8;k++) 23{ 24intxx=i+dx[k]; 25intyy=j+dy[k]
我要查找值为‘WSCOL1525’的字段。 declare@cloumnsvarchar(40)declare@tablenamevarchar(40)declare@strvarchar(40)declare@countsintdeclare@sqlnvarchar(2000)declareMyCursorCursorForSelecta.nameasColumns,b.nameasTableNamefromsyscolumnsa,sysobjectsb,systypescwherea.id=b.idandb.type='U'anda.xtype=c.xtypeandc.namelike'%char%'set@str=' declare@cloumnsvarchar(40)declare@tablenamevarchar(40)declare@strvarchar(40)declare@countsintdeclare@sqlnvarchar(2000)declareMyCursorCursorForSelecta.nameasColumns,b.nameasTableNamefro
外键映射 模式概要:把对象间的关联映射到表间的外键引用。 我的思考:外键映射适用于:1:1及1:N的关联关系,通常让非root实体持有root实体的标识域,比如唱片持有作者的标识域,曲目持有唱片的标识域。 项目实践:社区系统中的“帖子”实体持有“用户”实体的标识域,在数据库中则表现为Post表持有一个userNo字段。 关联表映射 模式概要:把关联保存为一个表,带有指向表的外键。 我的思考:外键映射适用于:N:N的关联关系,关联表通常对应一个值对象。关联表通常存在两个方向的查询入口,这两个入口跟关联表外键对应的实体表有关,那个在DDD中,该关联表就可以同时属于两个“聚合”中。比如用户体系系统中“用户账户关系表”(UserAccount),作为值对象,持有userNo和accountNo;存在根据userNo查询accounts的场景,也存在accountNo查询UserAccount的场景;可以看出UserAccount属于User和Account这两个“聚合”中。 项目实践:用户体系系统中“用户账户关系表”,作为值对象,持有userNo和accoun
在谷歌趋势中输入BigData关键字,你会发现从2012年开始,全球对大数据的关注程度呈指数级上升的态势,到2013年6月接近峰值100。从经验来看,当人们对某个领域的关注程度到这种程度的时候,该领域也就逐步从概念阶段进入到发展处相关成熟技术的阶段。现实的确如此,大数据已不仅仅是概念的讨论,而是已经在各行各业有了具体的应用场景。 8月份,笔者参加了一个由Forrester咨询公司主办的大数据研讨会,虽然会议中有很多是对他们公司大数据领域相关研究成果的推介,但有些结论依然值得分享: 1.由于业务的增长等因素,企业内部储存的数据呈爆炸式增长趋势,然而数据实际利用率却非常低,仅占全部数据的12%; 2.企业内部数据中,非结构化数据占绝大多数,这部分数据对企业的商业战略却最为重要。然而,传统的BI解决方案对非结构化数据的利用面临很大的困难; 3.基于数据仓库的传统BI结构已无法负载当前巨大的数据量,越来越多的公司将目光转向大数据分析技术,37%公司正在规划大数据项目,20%公司已有实际应用; 4.大数据解决方案将在数据的一致性和完整性与响应速度和灵活性之间进行权衡,以便实现对业务趋势的实时分析
编译原理163课堂http://mooc.study.163.com/learn/-1000002001?tid=1000003000#/learn/content?type=detail&id=1000024005&cid=1000019010 静态代码扫描(一)——PMD自定义规则入门 PMDfromhttp://pmd.sourceforge.net/ PMD能够扫描Java源代码,查找类似以下的潜在问题: 可能的bug——try/catch/finally/switch语句中返回空值。 死代码——未使用的局部变量、参数、私有方法。 不理想的代码——使用String/StringBuffer。 过于复杂的表达式——没有必要使用if语句、while循环可以代替for循环。 重复代码——复制/粘贴的代码引发的bug。 PMD集成了JDeveloper,Eclipse,JEdit,JBuilder,BlueJ,CodeGuide, NetBeans/SunJavaStudioEnterprise/Creator,IntelliJI
表结构 依次为主键ID,树名称,描述,父级节点,层级测试数据 211的父级节点是21,在上一级为2,在上一级为0实现需求:当我传递参数2时,查询2,21,211来。SQLwithsubqry(id,name,pid)as(selectDocCategoryID,DocCategoryName,DocParentIDfromdoc_DocumentCategorywhereDocCategoryID=2unionallselectdoc_DocumentCategory.DocCategoryID,doc_DocumentCategory.DocCategoryName,doc_DocumentCategory.DocParentIDfromdoc_DocumentCategory,subqrywheredoc_DocumentCategory.DocParentID=subqry.id--connectby)select*fromsubqry;结果集