当前位置: 首页 > news >正文

逐步构建高性能http服务器及聊天室服务器

目录

如何拿到浏览器发来的http请求

如何给浏览器发送响应

响应基本原理

给浏览器发送一个网页作为响应

给浏览器发送一个图片作为响应

接下来我们要做什么

完善业务逻辑

浏览器如何访问特定文件

访问根目录下的文件

访问子文件夹下的文件

习惯性目录结构

GET请求带参

服务器长久运行

认识POST请求

总结

逐步构建高性能http服务器

CGI机制

什么是CGI机制

单进程版本http服务器

        http_conn.hpp

        http_conn.cpp

        LOG.hpp

        LOG.cpp

        main.cpp

        wwwroot/calculator.hpp

        wwwroot/index.html

        wwwroot/404.html

        wwwroot/500.html

宏观逻辑如下图:

代码分析

signal(SIGPIPE, SIG_IGN)在干什么

cgi机制中为什么要建立父子通讯管道

cgi机制中为什么要使用环境变量

cgi机制中为什么GET请求通过环境变量传递参数,POST请求通过管道传递参数

cgi机制中为什么要进行重定向到标准输入输出

问题分析

发送问题

接收问题

效率问题

多线程版本http服务器

        main.cpp

问题分析

多进程版本的http服务器

代码分析

问题分析

错误检查

ls -l /proc/pid/fd

        错误一:下图已更正

        错误二:下图已更正

线程池版本的http服务器

        目录结构

        http_conn.cpp

        http_conn.hpp

        LOG.hpp

        LOG.cpp

        common.hpp

        locker.hpp

        threadpool.hpp

        main.cpp

进程池版本的cgi服务器

        服务器目录结构

         processpool.h

       server.cpp

         cgi.h

         test.cpp

        进程池代码分析

        此处的进程池单例真的是个单例吗?

        子进程继承的文件描述符

        统一事件源

        SIGCHLD信号

        父子通信管道

总结

逐步构建高性能聊天室服务器

服务器逻辑:

基于epoll的多线程版本聊天室服务器

        服务器目录结构

        客户端目录结构

        chatserver.cpp

        chatclient.cpp

        common.hpp

  代码分析

        ET + EPOLLONESHOT模式下

问题分析

线程池版本的聊天室服务器

        服务器目录结构

        chatserver.cpp

        threadpool.hpp

总结


如何拿到浏览器发来的http请求

        我们将设计一个简单的程序,让大家先看到如何从服务器拿到浏览器发来的http请求。示例代码如下:

#include <stdlib.h>// atoi 
#include <iostream>
#include <sys/types.h>      // socket()
#include <sys/socket.h>     // socket()
#include <assert.h>         // assert()
#include <arpa/inet.h>       // struct sockaddr_in
#include <string.h>         //bzero#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;return 0;
}

        在浏览器输入 服务器ip:8080  后输出如下

GET / HTTP/1.1
Host: 49.233.89.193:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

        此时我们已经拿到了http请求的全部内容,如果大家不清楚http请求的格式,应当先去学习一下。

如何给浏览器发送响应

响应基本原理

        浏览器给我们发送请求的大部分目的都是为了得到资源,我们接下来将围绕这个话题来探讨如何给浏览器发送一个网页作为响应。

        资源都在服务器的磁盘里,浏览器想要得到那个文件资源呢?

  • 答案在请求行的url里,url是一个文件路径,它告诉浏览器,我想要哪个资源
  • 当在浏览器输入框输入 ip + : + port后(49.233.89.193:8080),浏览器自动补全为 ip + : + port + / (49.233.89.193:8080/)。  即自动请求根目录
  • 这里的根目录,指的是服务器运行的目录
  • 我们可以通过在输入框输入 ip + : + port + / +资源名(49.233.89.193:8080/文件夹/index.html)来有目标的获取特定资源文件

        服务器发送给浏览器目标资源后,需要告诉浏览器:这个资源文件的类型和大小

  • 这是为了浏览器去识别文件,然后正确的解释文件。比如说服务器给浏览器发送了一个图片,底层数据是二进制字节流,服务器就不能按照识别文本字符的方式去把字节流的一个个字节解释成字符,而应该将其解释成图片。再比如服务器给浏览器发送了一个网页,浏览器就应当将其解释成网页。

  • 服务器通过在请求头中添加Content-type字段,用于标识资源类型。例如网页html文件对应“Content-Type: text/html"。对于更多资源类型,大家可以搜索Content-Type对照表查询即可。

        服务器发送给浏览器目标资源时,需要检查一下该目标资源是否存在,是否可以被访问

  •  服务器通过给浏览器发送 “状态码” 和  “状态码描述” 来说明浏览器请求的目标资源的情况。例如:“200 OK” 来表示资源正常,响应成功;“404  Not Found”来表示资源未找到,其余情况大家可以查询http响应状态码和状态码描述表。

        协议版本

  • 浏览器给服务器发送协议版本号,来告诉服务器,你应该采用这种版本的规则,去理解去解析我给你发的这些信息。
  • 服务器给浏览器发送协议版本号,来告诉浏览器,你应该采用这种版本的规则,去理解去解析我给你发的这些信息。
  • 两者意图是一样的。
  • 在我们目前实际编写服务器的过程中,协议版本号的存在感很低,我们只要加上这个固定信息即可。

给浏览器发送一个网页作为响应

        这里我们简化一下,无论浏览器给我们发送什么文件,我们都给其回应一个我们准备的这个简易index1.html文件。

        访问方式

  1. 在浏览器搜索框输入:49.233.89.193:8080。注意你要输入自己的服务器地址,或者本地127.0.0.1环回地址
  2. 每次运行,如果8080端口运行不起来,就换一个端口。

        index.html(放在服务器运行目录下)

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF - 8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>简单的HTML网页</title>
</head><body><h1>欢迎来到我的网页</h1><p>这是一个简单的段落,用于展示基本的http响应流程。</p>
</body></html>

        源文件

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;// 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open("index.html", O_RDONLY);  //以只读方式打开// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;// 构建响应状态行std::string firsr_line;// 构建响应头std::string header_line;firsr_line += "HTTP/1.1 200 OK\r\n";header_line += "Content-Length: " + std::to_string(file_size) + "\r\n";header_line += "Content-Type: text/html\r\n";header_line += "\r\n";// 构建响应正文,也就是index.html文件内容// 将文件内容读取到用户层发送缓冲区中char write_buffer[BUFFER_SIZE]; //在这里我们的index.html文件很小的,4096字节肯定装的下了read(fd,write_buffer,file_size);// 将数据发送给浏览器send(connfd,firsr_line.c_str(),firsr_line.size(),0);send(connfd,header_line.c_str(),header_line.size(),0);send(connfd,write_buffer,file_size,0);close(fd);close(connfd);close(listenfd);return 0;
}

        网页结果

给浏览器发送一个图片作为响应

        1.jpg(跟程序放在同一个文件夹下)

        源代码

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';// cout << read_buffer;// 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open("1.jpg", O_RDONLY);  //以只读方式打开// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << file_size << endl;// 构建响应状态行std::string firsr_line;// 构建响应头std::string header_line;firsr_line += "HTTP/1.1 200 OK\r\n";header_line += "Content-Length: " + std::to_string(file_size) + "\r\n";header_line += "Content-Type: image/jpeg\r\n";header_line += "\r\n";// 构建响应正文,也就是1.jpg文件内容cout << header_line << endl;// 将文件内容读取到用户层发送缓冲区中// char write_buffer[BUFFER_SIZE]; //在这里我们的index.html文件很小的,4096字节肯定装的下了char* p_jpg = new char[1024 * 1024];read(fd,p_jpg,file_size);// 将数据发送给浏览器send(connfd,firsr_line.c_str(),firsr_line.size(),0);send(connfd,header_line.c_str(),header_line.size(),0);cout << send(connfd,p_jpg,file_size,0) << endl;delete[] p_jpg;close(fd);close(connfd);close(listenfd);return 0;
}

        不了解sendfile的可以看如下文章:

  • 高级IO函数之sendfile_sendfile 函数-CSDN博客

接下来我们要做什么

        刚才小试牛刀,我们用不到100行代码,就足以构建一个逻辑闭环的http服务器。那么我们为什么还要去写更复杂的服务器处理逻辑呢?总体可以分为两个宏观原因。

  1. 完善业务处理,能够应对更多的业务场景。这部分不是我们关注的重点,但是如果我们不清晰业务逻辑,会让我们在构建服务器时产生很多困扰。
  2. 提高服务器的性能。我们将编写多版本的服务器并分析,逐步构建更高效更理想的服务器。

完善业务逻辑

        在这部分我们将给大家演示说明浏览器与服务器之间如何交互。我们会用尽可能最简单的代码进行演示,以便大家能够更轻松的理解交互逻辑。

浏览器如何访问特定文件

访问根目录下的文件

        文件夹结构如下

         访问方式如下:

  1. 在浏览器输入框输入:49.233.89.193:8080/index.html。便可以获取根目录下的index.html网页文件。
  2. 在浏览器输入框输入:49.233.89.193:8080/1.jpg。便可以获取根目录下的1.jpg图片文件。   

        服务器代码如下:

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE);   // 读缓冲区ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 请求行std::string request_header; // 请求头std::string request_content; // 请求正文std::string request_method; // 请求方法std::string requset_url;    // 请求的urlstd::string request_version; // 请求协议版本std::string suffix;          // 目标资源后缀,用于构建响应Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法没有请求正文,我们在本代码中不构建请求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url = request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open(requset_url.c_str(), O_RDONLY);  //以只读方式打开cout << "fd: " << fd << endl;// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 构建响应状态行std::string repose_line;// 构建响应头std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 将文件内容读取到用户层发送缓冲区中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 将数据发送给浏览器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);close(listenfd);return 0;
}

访问子文件夹下的文件

        文件夹结构如下

        只需要创建source文件夹,然后把两个资源文件移动到里面即可。代码不需要重新编译可直接运行。

习惯性目录结构

        通常我们不会把资源直接放在服务器运行目录下,而是创建一个文件夹wwwroot,然后将资源放在这个文件夹下。如下图所示

  1. 在浏览器输入框输入:49.233.89.193:8080/index.html。代表要获取wwwroot目录下的index.html网页文件。
  2. 在浏览器输入框输入:49.233.89.193:8080/1.jpg。代表要获取wwwroot目录下的1.jpg图片文件。

        为此我们需要稍微改进一下源代码

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE);   // 读缓冲区ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 请求行std::string request_header; // 请求头std::string request_content; // 请求正文std::string request_method; // 请求方法std::string requset_url;    // 请求的urlstd::string request_version; // 请求协议版本std::string suffix;          // 目标资源后缀,用于构建响应Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法没有请求正文,我们在本代码中不构建请求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open(requset_url.c_str(), O_RDONLY);  //以只读方式打开cout << "fd: " << fd << endl;// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 构建响应状态行std::string repose_line;// 构建响应头std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 将文件内容读取到用户层发送缓冲区中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 将数据发送给浏览器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);close(listenfd);return 0;
}

GET请求带参

          访问方式如下:

  1. 在浏览器输入框输入:49.233.89.193:8080/index.html?x=1&y=2&z=3。便可以获取根目录下的index.html网页文件,并将参数x=1&y=2&z=3传递给服务器。
  2. 在浏览器输入框输入:49.233.89.193:8080/1.jpg?x=1&y=2&z=3。便可以获取根目录下的1.jpg图片文件,并将参数x=1&y=2&z=3传递给服务器。
  3. 以后我们约定根目录就是wwwroot

        参数与url一起形成一个大url字符串

        我们拿到带参GET请求,并打印一下看看,源代码如下。我们用的是文章最开头的程序:

#include <stdlib.h>// atoi 
#include <iostream>
#include <sys/types.h>      // socket()
#include <sys/socket.h>     // socket()
#include <assert.h>         // assert()
#include <arpa/inet.h>       // struct sockaddr_in
#include <string.h>         //bzero#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;return 0;
}

        打印结果为:

GET /index.html?x=1&y=2&z=3 HTTP/1.1
Host: 49.233.89.193:8081
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.520.400 QQBrowser/19.2.6473.400
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Scheme: http

        这些参数有什么用?

  1. 我们可以对参数做计算,然后把结果返回给浏览器,从而相当于创建了一个计算服务
  2. 我们可以将参数传递给cgi程序,让cgi程序去处理。后文会详细讲解演示cgi机制

服务器长久运行

        在上面的例子中,我们都是回应一次http请求后,就直接结束了。这显然不符合服务器的运行逻辑。我们的服务器应当长久运行,持续响应多个http请求。

        源代码如下:

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );while(1){cout << "******************************************开始********************************************" << endl;struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE);   // 读缓冲区ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 请求行std::string request_header; // 请求头std::string request_content; // 请求正文std::string request_method; // 请求方法std::string requset_url;    // 请求的urlstd::string request_version; // 请求协议版本std::string suffix;          // 目标资源后缀,用于构建响应Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法没有请求正文,我们在本代码中不构建请求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open(requset_url.c_str(), O_RDONLY);  //以只读方式打开cout << "fd: " << fd << endl;// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 构建响应状态行std::string repose_line;// 构建响应头std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 将文件内容读取到用户层发送缓冲区中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 将数据发送给浏览器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);cout << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

        至此大家就可以多次访问了。

认识POST请求

        大家应当先简单的自行了解一下,前端html文件中的form表单都是大概什么意思。这里我们重在讲解交互逻辑。

       目录结构

        index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF - 8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>简单的HTML网页</title>
</head><body><form action="/1.jpg" method="POST">First name:<br><input type="text" name="firstname" value="Mickey"><br>Last name:<br><input type="text" name="lastname" value="Mouse"><br><br><input type="submit" value="Submit"></form> <p>如果您点击提交,表单数据会被发送到名为 1.jpg 的页面。</p>
</body></html>

        说明

  • action用于指定这次请求的目标资源
  • method用于指定这次请求的方法
  • name和value是正文数据
  • 点击submit按钮浏览器会重新发送请求。请求方法由method指定;请求正文由name和value指定

        操作方法

        1.在浏览器输入49.233.89.193:8080/index.html后来到这个页面

        2.你可以修改输入框内容

        3.点击提交按钮相当于再次向浏览器发送一个请求。action用于指定这次请求的目标资源。请求方法由method指定;请求正文由name和value指定。点击提交

        4.服务器会打印出这次请求的内容

        源代码如下

#include <stdlib.h>
#include <iostream>
#include <sys/types.h>      
#include <sys/socket.h>     
#include <assert.h>         
#include <arpa/inet.h>       
#include <string.h>         
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );while(1){cout << "******************************************开始********************************************" << endl;struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE);   // 读缓冲区ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 请求行std::string request_header; // 请求头std::string request_content; // 请求正文std::string request_method; // 请求方法std::string requset_url;    // 请求的urlstd::string request_version; // 请求协议版本std::string suffix;          // 目标资源后缀,用于构建响应Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; if(request_method == "POST")	// 如果是POST请求则构建请求正文,GET请求没有请求正文{request_content = read_buffer.substr(read_buffer.rfind('\n') + 1);cout << "request_content: " << request_content << endl; }// 打开这个文件,会创建一个文件描述符fd,并且内核为该fd关联一个内核级缓冲区,里面放的是这个文件的内容int fd = open(requset_url.c_str(), O_RDONLY);  //以只读方式打开cout << "fd: " << fd << endl;// 获取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 构建响应状态行std::string repose_line;// 构建响应头std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 将文件内容读取到用户层发送缓冲区中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 将数据发送给浏览器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);cout << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

总结

        至此我们已经简单认识了,网页请求和响应的基本逻辑。接下来进入文章重点内容,逐步构建高性能的服务器。

逐步构建高性能http服务器

CGI机制

什么是CGI机制

  1. 浏览器与服务器交互大致可以总结为两种逻辑。第一种逻辑:浏览器向服务器请求资源。第二种:浏览器给服务器发送数据,服务器fork()一个子进程去处理数据,子进程将处理结果返回给父进程,父进程返回给浏览器。第二种就是CGI机制。
  2. 为什么要有CGI机制:难道父进程不可以自己处理数据吗,还偏偏非要fork一个子进程去处理数据。这大概有这么几个原因:1.服务器业务逻辑与数据处理逻辑解耦,方便后台人员的协同开发。2.服务器会处理很多类型数据,这些数据的处理逻辑不尽相同,那么要想完成数据处理任务,服务器的代码就会非常庞大,编译效率、运行效率都会受到影响,造成资源浪费。
  3. 稍后我们会在代码中详细讲解CGI机制

单进程版本http服务器

        经过上面的讲解,我们已经明白了浏览器和服务器交互的大致逻辑,对于cgi机制我们会在本小节代码中详细分析。下面是一个完整的初级http服务器。

        http_conn.hpp

#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include "LOG.hpp"
#define WEB_ROOT "wwwroot"			 // Web根目录
#define HOME_PAGE "index.html"		 // 默认资源文件
#define BAD_REQUEST_PAGE "400.html"	 // 请求错误
#define NOT_FOUND_PAGE "404.html"	 // 资源不存在
#define SERVER_ERROR_PAGE "500.html" // 服务器出错
#define SPACE " "					 // 空格
#define END_OF_LINE "\r\n"			 // 行尾回车换行enum StatusCode
{CLOSE = 0,		   // 连接关闭OK = 200,		   // 正常情况BAD_REQUEST = 400, // 请求错误NOT_FOUND = 404,   // 资源不存在SERVER_ERROR = 500 // 服务器出错
};static std::unordered_map<int, std::string> statusDescMap ={{200, "OK"},{400, "BAD_REQUEST"},{404, "NOT_FOUND"},{500, "SERVER_ERROR"},
};static std::unordered_map<std::string, std::string> suffixMap ={{"html","text/html; charset=UTF-8"},{"css", "text/css"},{"js", "application/javascript"},{"jpg", "image/jpeg"},{"xml", "application/xml"},{"cgi", "text/html; charset=UTF-8"},};class http_conn
{
private:int _sock; // 用于获得这个客户连接的socket
public:std::string _request_line;											// 请求行std::vector<std::string> _request_v_header;							// 请求头数组std::unordered_map<std::string, std::string> _request_uomap_header; // 请求头哈希表std::string _request_content;										// 请求正文std::string _request_method;  // 请求方法std::string _request_url;	  // 请求urlstd::string _request_url_arg; // 请求参数std::string _requst_version;  // 请求版本std::string _request_suffix;  // 资源类型
public:std::string _response_line;					 // 响应行std::vector<std::string> _response_v_header; // 响应头数组std::string _response_blankline;			 // 响应空行std::string _response_content;				 // 响应正文
public:bool _cgi;					   // 是否采用cgi机制
public:// 构造函数http_conn(int sock) : _sock(sock), _cgi(false), _request_suffix("html"){}~http_conn(){if(_sock >= 0) close(_sock);}public:
void h_run()
{StatusCode ret = h_read();h_write(ret);
}private:
StatusCode h_read();
void h_write(StatusCode ret);private:// 构建请求行_request_line// 失败则返回:CLOSE// 成功则返回:OKStatusCode h_build_reqline();// 构建请求方法_request_method// 失败则返回:BAD_REQUEST// 成功则返回:OKStatusCode h_build_reqmethod();// 判断请求方法是否正确// 我们只处理"GET""POST"请求// 其余请求返回:BAD_REQUEST// 这两个请求返回:OKStatusCode h_parse_method();// 构建url,顺带着构建参数_request_url_arg// 失败则返回:BAD_REQUEST// 成功则返回:OKStatusCode h_build_requrl();// 判断url是否合法// 非法情况:资源不存在,资源无权读,资源无权执行// 设置_cgi// 失败则返回:NOT_FOUND// 成功则返回:OKStatusCode h_parse_url();StatusCode h_build_reqsuffix();// 构建_request_line// 任务不存在出错情况// 只返回:OKStatusCode h_build_reqversion();// 构建请求头数组_request_v_header// 读取错误返回:CLOSE// 正确则返回:OKStatusCode h_build_reqvheader();// 构建请求头哈希表_request_uomap_header// 请求头格式错误则返回:BAD_REQUEST// 正确则返回:OKStatusCode h_build_requomapheader();// 构建请求正文_request_content// 读取错误返回:CLOSE// 正确则返回:OKStatusCode h_build_reqcontent();// 执行cgi程序StatusCode h_cgi();private:void HandlerERROR(const std::string &page, int ret);void HandlerOK();private:// 从tcp接收缓冲区拿走一行数据// 这里我们阻塞式读取,直到遇到 "\r\n"结束// 所以要么我们取走一行完整数据,要么对方关闭连接;h_recv_line才会返回int h_recv_line(int _sock, std::string &str);// 按照sep切分字符串bool cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep);
};

        http_conn.cpp

#include "http_conn.hpp"
// 从tcp接收缓冲区拿走一行数据
// 这里我们阻塞式读取,直到遇到 "\r\n"结束
// 所以要么我们取走一行完整数据,要么对方关闭连接,要么读取出错;h_recv_line才会返回
int http_conn::h_recv_line(int _sock, std::string &str)
{char ch = 'X';while (ch != '\n'){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){if (ch == '\r'){recv(_sock, &ch, 1, MSG_PEEK);if (ch == '\n'){recv(_sock, &ch, 1, 0);}else{ch = '\n';}}str.push_back(ch);}else if (s == 0){return 0;}else{return -1;}}return str.size();
}// 按照sep切分字符串
bool http_conn::cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep)
{size_t pos = str.find(sep);if (pos != std::string::npos){leftOut = str.substr(0, pos);rightOut = str.substr(pos + sep.size());return true;}return false;
}// 构建请求行
// 失败则返回:CLOSE
// 成功则返回:OK
StatusCode http_conn::h_build_reqline()
{if (h_recv_line(_sock, _request_line) > 0){_request_line.pop_back();LOG(INFO, "h_build_reqline success--->_request_line: " + _request_line);return OK;}else{LOG(ERROR, "h_build_reqline failed");return CLOSE;}
}// 构建请求方法
// 失败则返回:BAD_REQUEST
// 成功则返回:OK
StatusCode http_conn::h_build_reqmethod()
{int n = _request_line.find(SPACE);if (n != std::string::npos){_request_method = _request_line.substr(0, n);LOG(INFO, "h_build_reqmethod success--->_request_method: " + _request_method);return OK;}else{LOG(ERROR, "_request_method failed");return BAD_REQUEST;}
}// 判断请求方法是否正确
// 我们只处理"GET""POST"请求
// 其余请求返回:BAD_REQUEST
// 这两个请求返回:OK
StatusCode http_conn::h_parse_method()
{if (_request_method == "GET" || _request_method == "POST"){LOG(INFO, "h_parse_method success");return OK;}else{LOG(ERROR, "h_parse_method false");return BAD_REQUEST;}
}// 构建url,顺带着构建参数_request_url_arg
// 失败则返回:BAD_REQUEST
// 成功则返回:OK
StatusCode http_conn::h_build_requrl()
{int n1 = _request_line.find(SPACE);n1 = _request_line.find(SPACE, n1 + 1);if (n1 != std::string::npos){int n2 = _request_line.find('?');if (n2 != std::string::npos) // 这意味着带参数{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n2 - _request_line.find(SPACE) - 1);_request_url_arg = _request_line.substr(n2 + 1, n1 - n2 - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: " + _request_url_arg);return OK;}else // 这意味着不带参数{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n1 - _request_line.find(SPACE) - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: NO");return OK;}}else{LOG(ERROR, "h_build_requrl failed");return BAD_REQUEST;}
}// 判断url是否合法
// 非法情况:资源不存在,资源无权读,资源无权执行
// 设置_cgi
// 失败则返回:NOT_FOUND
// 成功则返回:OK
StatusCode http_conn::h_parse_url()
{struct stat fileStat;if (stat(_request_url.c_str(), &fileStat)) // 文件不存在{LOG(ERROR, "path " + _request_url + " NOT_FOUND, err:" + std::string(strerror(errno)));return NOT_FOUND;}else{if (S_ISDIR(fileStat.st_mode)) // 文件存在但是是一个目录,应该访问其中的默认页面{_request_url += '/';_request_url += HOME_PAGE;stat(_request_url.c_str(), &fileStat);}if (_request_method == "GET" && _request_url_arg.size() == 0) // 这代表想要获取资源,应当对目标文件具有读权限{_cgi = false;if (fileStat.st_mode & S_IRUSR || fileStat.st_mode & S_IRGRP || fileStat.st_mode & S_IROTH) // 是否具有读权限{LOG(INFO, "h_parse_url success");return OK;}else{LOG(ERROR, "h_parse_url false NO read");return NOT_FOUND;}}else // 这代表想要运行cgi程序,应当具有执行权限{_cgi = true;if ((fileStat.st_mode & S_IXUSR) || (fileStat.st_mode & S_IXGRP) || (fileStat.st_mode & S_IXOTH)) // 是否具有执行权限{LOG(INFO, "_request_url: true");return OK;}else{LOG(ERROR, "_request_url: false NO executable");return NOT_FOUND;}}}
}// 构建资源类型
// 只返回OK
StatusCode http_conn::h_build_reqsuffix()
{int n = _request_url.find('.');if (n == std::string::npos){LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}else{_request_suffix = _request_url.substr(n + 1);LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}
}
// 构建_request_line
// 任务不存在出错情况
// 只返回:OK
StatusCode http_conn::h_build_reqversion()
{_requst_version = _request_line.substr(_request_line.rfind(SPACE) + 1);LOG(INFO, "h_build_reqversion success--->_requst_version: " + _requst_version);return OK;
}// 构建请求头数组_request_v_header
// 读取错误返回:CLOSE
// 正确则返回:OK
StatusCode http_conn::h_build_reqvheader()
{std::vector<std::string> &header = _request_v_header;std::string tmp;while (true){int length = h_recv_line(_sock, tmp);if (length <= 0){LOG(ERROR, "h_build_reqheader failed");return CLOSE;}if (length == 1){break;}tmp.pop_back();header.push_back(tmp);tmp.clear();}LOG(INFO, "h_build_reqheader success");return OK;
}// 构建请求头哈希表_request_uomap_header
// 请求头格式错误则返回:BAD_REQUEST
// 正确则返回:OK
StatusCode http_conn::h_build_requomapheader()
{std::string key;std::string value;for (auto &str : _request_v_header) // 多行请求报头接收到了vector中{// 请求报头中的KV是以 冒号+空格 分隔的if (cut_string(str, key, value, ": ")){_request_uomap_header.insert({key, value});}else{LOG(ERROR, "h_build_requomapheader failed");return BAD_REQUEST;}}LOG(INFO, "h_build_requomapheader success");return OK;
}// 构建请求正文_request_content
// 读取错误返回:CLOSE
// 正确则返回:OK
StatusCode http_conn::h_build_reqcontent()
{if(_request_method == "POST" && _request_uomap_header["Content-Length"] != "0"){cout << 66666<<endl;int content_len = std::stoi(_request_uomap_header["Content-Length"]);char ch = 0;while (content_len--){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){_request_content.push_back(ch);}else // 读取正文对端写关闭或者读取失败-------------------------------------{LOG(ERROR, "h_build_reqcontent failed");return CLOSE;}}LOG(INFO, "h_build_reqcontent success--->_request_content: " + _request_content);return OK;}else{LOG(INFO, "h_build_reqcontent sucess--->_request_content: NULL");return OK;}
}// 执行cgi程序
StatusCode http_conn::h_cgi()
{/* cgi整体流程:创建子进程,让cgi进程替换掉子进程,父进程将request中的参数传给cgi进程,cgi进程处理完后将结果返回给父进程 */// 走这里request一定是POST或者带参的GET方法,那么此时request请求中的path就是所需要的可执行程序的路径,此时就要让这个可执行程序替换掉这里的子进程// auto &path = _request_url; // 可执行程序的路径// 想要让父子进程通信,直接用管道来实现int input[2]; // 站在父进程角度的输入和输出,父进程用input[0]读,用output[1]写int output[2];if (pipe(input) < 0) // 创建input管道{					 // 创建input管道失败LOG(ERROR, "create input pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}if (pipe(output) < 0) // 创建output管道{					  // 创建output管道失败LOG(ERROR, "create output pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}// 走到这里管道就创建好了,此时创建一个子进程就可以进行父子进程通信了// 创建子进程pid_t pid = fork();// 子进程// 设置环境变量:"QUERY_STRING="、"CONTENT_LENGTH="、"METHOD="// 重定向:将标准输入文件描述符定向到管道output读端output[0];将标准输出文件描述符定向到管道input写端input[1]// cgi程序替换子进程if (pid == 0){close(input[0]);close(output[1]);/* **************************************************设置环境变量********************************************** */if (_request_method == "GET"){std::string queryString_env = "QUERY_STRING=" + _request_url_arg;if (putenv((char *)queryString_env.c_str()) != 0){LOG(ERROR, "putenv queryString failed");exit(1);}}else if (_request_method == "POST"){std::string contentLength_env = "CONTENT_LENGTH=" + _request_uomap_header["Content-Length"];if (putenv((char *)contentLength_env.c_str()) != 0){LOG(ERROR, "POST method, putenv CONTENT_LENGTH failed");exit(1);}}std::string method_env = "METHOD=" + _request_method;if (putenv((char *)method_env.c_str()) != 0){LOG(ERROR, "putenv method failed");exit(1);}/* **************************************************设置环境变量********************************************** */dup2(output[0], 0);dup2(input[1], 1);close(output[0]);close(input[0]);execl(_request_url.c_str(), _request_url.c_str(), nullptr);LOG(ERROR, "execl failed");exit(1);}// 父进程else if (pid > 0){close(input[1]);close(output[0]);if (_request_method == "POST"){size_t total = 0, size_singleTime = 0;size_t size = _request_content.size();while (total < size){size_singleTime = write(output[1], (void *)(_request_content.c_str() + total), size - total);if (size_singleTime <= 0) // 给子进程发送数据失败{int k = kill(pid, SIGTERM); // 给子进程发送结束信号if (k == 0){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else if (k == -1 && errno == ESRCH){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else{LOG(FATAL, "kill(pid, SIGTERM) failed");return SERVER_ERROR;}}else{total += size_singleTime;}}}// 发送完数据之后就接收CGI进程返回的结果char ch;while (read(input[0], &ch, 1) > 0){_response_content.push_back(ch);}cout << "-->" << "server get result:\n"<< _response_content << endl;int status = 0;pid_t _pid = waitpid(-1, &status, 0);if (pid == _pid){if (WIFEXITED(status)){if (WEXITSTATUS(status) != 0){cout << "exit code ::" << WEXITSTATUS(status) << endl;return BAD_REQUEST;}else{cout << "exit code ::" << WEXITSTATUS(status) << endl;return OK;}}else{LOG(WARNING, "WIFEXITED(status) is false");return SERVER_ERROR;}}else{LOG(WARNING, "waitpid is false");return SERVER_ERROR;}close(input[0]);close(output[1]);// 子进程替换之后会执行完毕,此时替换进程整个空间都会被回收,所以替换进程中可以不需要手动关input[1]和output[0]}else{LOG(WARNING, "fork is false");return SERVER_ERROR;}return OK;
}/* ****************************************************事务入口************************************************************ *//* 构建请求 */
StatusCode http_conn::h_read()
{StatusCode ret = OK;ret = h_build_reqline();if (ret != OK)return ret;ret = h_build_reqmethod();if (ret != OK)return ret;ret = h_parse_method();if (ret != OK)return ret;ret = h_build_requrl();if (ret != OK)return ret;ret = h_parse_url();if (ret != OK)return ret;ret = h_build_reqsuffix();if (ret != OK)return ret;ret = h_build_reqversion();if (ret != OK)return ret;ret = h_build_reqvheader();if (ret != OK)return ret;ret = h_build_requomapheader();if (ret != OK)return ret;ret = h_build_reqcontent();if (ret != OK)return ret;if (_cgi){ret = h_cgi();if (ret != OK)return ret;}return OK;
}void http_conn::HandlerERROR(const std::string &page, int ret)
{std::string pagePath = WEB_ROOT;pagePath += '/';pagePath += page;int fd = open(pagePath.c_str(), O_RDONLY);if (fd >= 0){/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(ret);_response_line += ' ';_response_line += statusDescMap[ret];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";struct stat st;stat(pagePath.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += "text/html; charset=UTF-8";tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;/* 发送状态行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 发送响应头 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 发送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 发送资源 */sendfile(_sock, fd, nullptr, st.st_size); // 发送响应正文}else{LOG(WARNING, "open false");}
}void http_conn::HandlerOK()
{if (!_cgi){int fd = open(_request_url.c_str(), O_RDONLY);if (fd > 0){/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";struct stat st;stat(_request_url.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;/* 发送状态行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 发送响应头 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 发送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 发送资源 */sendfile(_sock, fd, nullptr, st.st_size); // 发送响应正文}}else{/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";tmp += std::to_string(_response_content.size());tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;/* 发送状态行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 发送响应头 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 发送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 发送响应正文 */send(_sock, _response_content.c_str(), _response_content.size(), 0);}
}void http_conn::h_write(StatusCode ret)
{switch (ret){case OK:HandlerOK();break;case NOT_FOUND:HandlerERROR(NOT_FOUND_PAGE,ret);break;case BAD_REQUEST:HandlerERROR(BAD_REQUEST_PAGE,ret); break;case SERVER_ERROR:HandlerERROR(SERVER_ERROR_PAGE,ret); break;case CLOSE:close(_sock);_sock = -1;break;default:HandlerERROR(NOT_FOUND_PAGE,ret);break;}
}

        LOG.hpp

#pragma once#include<iostream>
#include <string>using std::cout;
using std::cerr;
using std::endl;enum LogLevel
{INFO,WARNING,ERROR,FATAL
};// 宏替换能保证每次调用的地方都是对应的文件和行
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__) // 打印日志// 只声明 Log 函数
void Log(const std::string& level, const std::string& message, const std::string& fileName, int line);

        LOG.cpp

#include "LOG.hpp"
#include <ctime>void Log(const std::string& level, const std::string& message, const std::string& fileName, int line)
{// 日志格式:// [level]<file:line>{日志内容}==>timetime_t tm = time(nullptr);cerr << "[" << level << "](" << fileName << ":" << line << ")" << "{" << message << "}==> " << ctime(&tm);
}

        main.cpp

#include <arpa/inet.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );if(listenfd < 0){LOG(FATAL,"socket() failed");}int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );if(ret < 0){LOG(FATAL,"bind() failed");}ret = listen( listenfd, 5 );if(ret < 0){LOG(FATAL,"listen() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );signal(SIGPIPE, SIG_IGN);while(1){cerr << "******************************************开始********************************************" << endl;int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );if( connfd < 0){LOG(INFO,"accept success");}http_conn* p_http = new http_conn(connfd);p_http->h_run();delete p_http;cerr << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

        wwwroot/calculator.hpp

#pragma once#include<iostream>
using std::cout;
using std::cerr;
using std::endl;#include<unistd.h>const static size_t INFO = 1;
const static size_t ERROR = 2;#define CGILOG(level, str) CgiLog(#level, str)void CgiLog(const std::string& level, const std::string& message)
{cerr << "CGI " << level << "==>" << message << endl;
}// 获取请求方法
char* GetMethod()
{char* method = getenv("METHOD"); // 获取请求方法,用来判断如何获取参数if(method == nullptr){CGILOG(ERROR, "get method env failed");exit(2);}CGILOG(INFO, "get method:" + std::string(method));return method;
}// 获取参数
void GetQueryString(const std::string &method, std::string& queryString)
{CGILOG(INFO, "GetQueryString function start");// 分两种,一种POST的,一种带参GET的if(method == "GET"){ // 从环境变量中获得参数CGILOG(INFO, "method is GET");queryString = getenv("QUERY_STRING");}else if(method == "POST"){ // 从管道(0)中获取参数,可以直接用cin,但是可能会有回车啥的,所以就用readCGILOG(INFO, "method is POST");size_t contentLength = std::stoul(getenv("CONTENT_LENGTH"));char ch;while(contentLength--){read(0, &ch, 1); // ---------------------------这里未处理read返回值queryString.push_back(ch);}}else{CGILOG(ERROR, "unknow method[" + method + ']');exit(3);}CGILOG(INFO, "get query string:" + queryString);
}// 切分函数
bool CutString(const std::string& str, const std::string& sep, std::string& leftStr, std::string& rightStr)
{size_t pos = str.find(sep);if(pos != std::string::npos){leftStr = str.substr(0, pos);rightStr = str.substr(pos + sep.size());return true;}else{return false;}
}

        wwwroot/calculator.cpp

#include "calculator.hpp"int main()
{std::string query_string;GetQueryString(GetMethod(), query_string); //获取参数//以&为分隔符将两个操作数分开std::string str1;std::string str2;CutString(query_string, "&", str1, str2);//以=为分隔符分别获取两个操作数的值std::string name1;std::string value1;CutString(str1, "=", name1, value1);std::string name2;std::string value2;CutString(str2, "=", name2, value2);//处理数据int x = atoi(value1.c_str());int y = atoi(value2.c_str());std::cout<<"<html>";std::cout<<"<head><meta charset=\"UTF-8\"></head>";std::cout<<"<body>";std::cout<<"<h3>"<<x<<" + "<<y<<" = "<<x+y<<"</h3>";std::cout<<"<h3>"<<x<<" - "<<y<<" = "<<x-y<<"</h3>";std::cout<<"<h3>"<<x<<" * "<<y<<" = "<<x*y<<"</h3>";if(x % y){double dx = x;double dy = y;std::cout<<"<h3>"<<x<<" / "<<y<<" = "<<dx / dy<<"</h3>"; //除0后cgi程序崩溃,属于异常退出}else {std::cout<<"<h3>"<<x<<" / "<<y<<" = "<<x/y<<"</h3>"; //除0后cgi程序崩溃,属于异常退出}std::cout<<"</body>";std::cout<<"</html>";return 0;
}

        wwwroot/index.html

<html><head><meta charset="UTF-8"></head><body><h1>404 NOT_FOUND:对不起,你所查找的资源不存在</h1></body>
</html>

        wwwroot/400.html

<html><head><meta charset="UTF-8"></head><body><h1>400 BAD_REQUEST:错误的请求</h1></body>
</html>

        wwwroot/404.html

<html><head><meta charset="UTF-8"></head><body><h1>404 NOT_FOUND:对不起,你所查找的资源不存在</h1></body>
</html>

        wwwroot/500.html

<html><head><meta charset="UTF-8"></head><body><h1>500 SERVER_ERROR:服务器内部错误</h1></body>
</html>

宏观逻辑如下图:

代码分析

signal(SIGPIPE, SIG_IGN)在干什么

        忽略了SIGPIPE信号,当服务器向一个已经关闭的文件描述或者读端关闭的管道中写入数据时,就会触发这个信号。服务器收到该信号后的默认行为是终止进程,这显然对服务器来讲是很不友好的。当客户端与服务器建立连接之后,服务器处理完请求,正在给客户端写入响应,此时直接退出客户端或者突然客户端断电等就会使服务器触发这个信号,此时服务器应当忽略这个信号,进入下一轮处理逻辑。

        不了解signal的可以看如下文章:

  1. 进程信号之signal系统调用-CSDN博客
  2. 进程信号之sigaction系统调用_sigaction函数功能说明-CSDN博客
  3. 进程信号之进程的信号掩码,信号集,sigprocmask函数-CSDN博客

        我们可以通过recv函数来判断,客户端是否关闭连接。

cgi机制中为什么要建立父子通讯管道

        这是为了将服务器从浏览器接收到的有效参数传递给cgi程序,cgi程序将处理结果返回给服务器。

cgi机制中为什么要使用环境变量

        这还是为了给cgi程序传递信息,比如请求方法,比如GET请求的参数

cgi机制中为什么GET请求通过环境变量传递参数,POST请求通过管道传递参数
  • GET请求的参数一般较少。通过环境变量传递即可
  • POST请求的参数一般比较大。通过管道传递才行
cgi机制中为什么要进行重定向到标准输入输出

        进程替换后,原进程的代码段等全部销毁,内核数据结构如文件描述符保留。可是此时cgi程序无法得知与父进程通信的管道文件描述符是几号。而标准输入输出规定就是0和1,将标准输入输出定向到父子通讯管道后,cgi程序只需总是从标准输入中取数据,将处理结果写入标准输出即可,实现服务器主逻辑与cgi处理逻辑解耦。

问题分析

发送问题

       数据一次写入不完,就会造成信息发送丢失

接收问题

        这里阻塞式读取,如果客户端一直保持连接,就是不发送数据,服务器岂不是会一直卡着。当然了按照http协议,根本不会出现这种情况,因为浏览器会立即发送数据。但是我们想要编写更可靠的服务器,所以后续我们会解决这个问题。

效率问题

        现在的服务器程序每次只能处理一个连接,完全处理完这个连接才能继续处理下一个连接,效率较低。

多线程版本http服务器

        我们只需要改一下main.cpp即可

        main.cpp

#include <arpa/inet.h>
#include <pthread.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;void *handler(void *args)
{http_conn *p_http = (http_conn *)args;p_http->h_run();delete p_http;
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);while (1){cerr << "******************************************开始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}http_conn *p_http = new http_conn(connfd);pthread_t tid;pthread_create(&tid, nullptr, handler, (void *)p_http);pthread_detach(tid);    //设置线程分离cerr << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

问题分析

        每到来一个连接就立即创建一个线程,如果短时间内突然到来非常多的连接,服务器内部就会含有大量的线程,服务器压力太大。

多进程版本的http服务器

#include <arpa/inet.h>
#include <pthread.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;// void *handler(void *args)
// {
//     http_conn *p_http = (http_conn *)args;
//     p_http->h_run();
//     delete p_http;
// }int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);while (1){cerr << "******************************************开始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}pid_t pid1 = fork();if(pid1 == 0)//子进程{close(listenfd); //子进程关闭监听套接字pid_t pid2 = fork();if(pid2 == 0) // 孙子进程进行任务处理{http_conn* p_http = new http_conn(connfd);p_http->h_run();delete p_http;exit(0);// 孙子进程被操作系统领养}exit(0);}waitpid(-1,NULL,0);// 子进程创建孙子进程后立即退出close(connfd); // 父进程关闭连接套接字// 父进程cerr << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

代码分析

  • 之所以创建孙子进程,是因为以下原因
  • 父进程创建子进程,倘若不创建孙子进程,由子进程处理任务,那么父进程必须等待子进程退出后,方能处理下个连接,这是因为父进程要waitpid子进程,清理子进程的内核数据结构。这样就又变成了,只有这个连接处理完后才能处理下一个连接,效率很低。
  • 创建孙子进程后,子进程直接退出,孙子进程进行耗时的任务处理,孙子进程退出后发现自己的父进程已经退出,由而被操作系统领养,由操作系统进行回收

问题分析

        如果短时间内涌来大量连接,此时操作系统内部会有大量的进程,服务器压力很大

错误检查

        本来准备给大家继续写线程池的,然后突然想看看我们的服务器打开的文件描述符。发现文件描述符泄露!而且有两处!

ls -l /proc/pid/fd

        用于查看进程打开的文件描述符,我们现在运行服务器程序,多次请求我们的服务器,然后每次运行后都查看该服务器进程打开的文件描述符,发现文件描述符不断增加。

        按道理来讲,我们无论处理多少次请求服务器进程,都应该在查看时只打开一个文件描述符(以及操作系统自己打开的文件描述符)

        错误一:下图已更正

        忘记关闭文件描述符

        错误二:下图已更正

        cgi请求父进程读取完cgi程序处理后的数据后,应当直接关闭文件描述符。

        后续代码中已更正这个错误。

线程池版本的http服务器

        首先在上述几个地方我们做了更正,其次为了方便编译,我们设置了公共头文件common.hpp,除了头文件包含部分http_conn.hpp、LOG.cpp、LOG.hpp这些文件无其他变化

        目录结构

        http_conn.cpp

#include "http_conn.hpp"
// 从tcp接收缓冲区拿走一行数据
// 这里我们阻塞式读取,直到遇到 "\r\n"结束
// 所以要么我们取走一行完整数据,要么对方关闭连接,要么读取出错;h_recv_line才会返回
int http_conn::h_recv_line(int _sock, std::string &str)
{char ch = 'X';while (ch != '\n'){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){if (ch == '\r'){recv(_sock, &ch, 1, MSG_PEEK);if (ch == '\n'){recv(_sock, &ch, 1, 0);}else{ch = '\n';}}str.push_back(ch);}else if (s == 0){return 0;}else{return -1;}}return str.size();
}// 按照sep切分字符串
bool http_conn::cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep)
{size_t pos = str.find(sep);if (pos != std::string::npos){leftOut = str.substr(0, pos);rightOut = str.substr(pos + sep.size());return true;}return false;
}// 构建请求行
// 失败则返回:CLOSE
// 成功则返回:OK
StatusCode http_conn::h_build_reqline()
{if (h_recv_line(_sock, _request_line) > 0){_request_line.pop_back();LOG(INFO, "h_build_reqline success--->_request_line: " + _request_line);return OK;}else{LOG(ERROR, "h_build_reqline failed");return CLOSE;}
}// 构建请求方法
// 失败则返回:BAD_REQUEST
// 成功则返回:OK
StatusCode http_conn::h_build_reqmethod()
{int n = _request_line.find(SPACE);if (n != std::string::npos){_request_method = _request_line.substr(0, n);LOG(INFO, "h_build_reqmethod success--->_request_method: " + _request_method);return OK;}else{LOG(ERROR, "_request_method failed");return BAD_REQUEST;}
}// 判断请求方法是否正确
// 我们只处理"GET""POST"请求
// 其余请求返回:BAD_REQUEST
// 这两个请求返回:OK
StatusCode http_conn::h_parse_method()
{if (_request_method == "GET" || _request_method == "POST"){LOG(INFO, "h_parse_method success");return OK;}else{LOG(ERROR, "h_parse_method false");return BAD_REQUEST;}
}// 构建url,顺带着构建参数_request_url_arg
// 失败则返回:BAD_REQUEST
// 成功则返回:OK
StatusCode http_conn::h_build_requrl()
{int n1 = _request_line.find(SPACE);n1 = _request_line.find(SPACE, n1 + 1);if (n1 != std::string::npos){int n2 = _request_line.find('?');if (n2 != std::string::npos) // 这意味着带参数{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n2 - _request_line.find(SPACE) - 1);_request_url_arg = _request_line.substr(n2 + 1, n1 - n2 - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: " + _request_url_arg);return OK;}else // 这意味着不带参数{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n1 - _request_line.find(SPACE) - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: NO");return OK;}}else{LOG(ERROR, "h_build_requrl failed");return BAD_REQUEST;}
}// 判断url是否合法
// 非法情况:资源不存在,资源无权读,资源无权执行
// 设置_cgi
// 失败则返回:NOT_FOUND
// 成功则返回:OK
StatusCode http_conn::h_parse_url()
{struct stat fileStat;if (stat(_request_url.c_str(), &fileStat)) // 文件不存在{LOG(ERROR, "path " + _request_url + " NOT_FOUND, err:" + std::string(strerror(errno)));return NOT_FOUND;}else{if (S_ISDIR(fileStat.st_mode)) // 文件存在但是是一个目录,应该访问其中的默认页面{_request_url += '/';_request_url += HOME_PAGE;stat(_request_url.c_str(), &fileStat);}if (_request_method == "GET" && _request_url_arg.size() == 0) // 这代表想要获取资源,应当对目标文件具有读权限{_cgi = false;if (fileStat.st_mode & S_IRUSR || fileStat.st_mode & S_IRGRP || fileStat.st_mode & S_IROTH) // 是否具有读权限{LOG(INFO, "h_parse_url success");return OK;}else{LOG(ERROR, "h_parse_url false NO read");return NOT_FOUND;}}else // 这代表想要运行cgi程序,应当具有执行权限{_cgi = true;if ((fileStat.st_mode & S_IXUSR) || (fileStat.st_mode & S_IXGRP) || (fileStat.st_mode & S_IXOTH)) // 是否具有执行权限{LOG(INFO, "_request_url: true");return OK;}else{LOG(ERROR, "_request_url: false NO executable");return NOT_FOUND;}}}
}// 构建资源类型
// 只返回OK
StatusCode http_conn::h_build_reqsuffix()
{int n = _request_url.find('.');if (n == std::string::npos){LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}else{_request_suffix = _request_url.substr(n + 1);LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}
}
// 构建_request_line
// 任务不存在出错情况
// 只返回:OK
StatusCode http_conn::h_build_reqversion()
{_requst_version = _request_line.substr(_request_line.rfind(SPACE) + 1);LOG(INFO, "h_build_reqversion success--->_requst_version: " + _requst_version);return OK;
}// 构建请求头数组_request_v_header
// 读取错误返回:CLOSE
// 正确则返回:OK
StatusCode http_conn::h_build_reqvheader()
{std::vector<std::string> &header = _request_v_header;std::string tmp;while (true){int length = h_recv_line(_sock, tmp);if (length <= 0){LOG(ERROR, "h_build_reqheader failed");return CLOSE;}if (length == 1){break;}tmp.pop_back();header.push_back(tmp);tmp.clear();}LOG(INFO, "h_build_reqheader success");return OK;
}// 构建请求头哈希表_request_uomap_header
// 请求头格式错误则返回:BAD_REQUEST
// 正确则返回:OK
StatusCode http_conn::h_build_requomapheader()
{std::string key;std::string value;for (auto &str : _request_v_header) // 多行请求报头接收到了vector中{// 请求报头中的KV是以 冒号+空格 分隔的if (cut_string(str, key, value, ": ")){_request_uomap_header.insert({key, value});}else{LOG(ERROR, "h_build_requomapheader failed");return BAD_REQUEST;}}LOG(INFO, "h_build_requomapheader success");return OK;
}// 构建请求正文_request_content
// 读取错误返回:CLOSE
// 正确则返回:OK
StatusCode http_conn::h_build_reqcontent()
{if (_request_method == "POST" && _request_uomap_header["Content-Length"] != "0"){cout << 66666 << endl;int content_len = std::stoi(_request_uomap_header["Content-Length"]);char ch = 0;while (content_len--){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){_request_content.push_back(ch);}else // 读取正文对端写关闭或者读取失败-------------------------------------{LOG(ERROR, "h_build_reqcontent failed");return CLOSE;}}LOG(INFO, "h_build_reqcontent success--->_request_content: " + _request_content);return OK;}else{LOG(INFO, "h_build_reqcontent sucess--->_request_content: NULL");return OK;}
}// 执行cgi程序
StatusCode http_conn::h_cgi()
{/* cgi整体流程:创建子进程,让cgi进程替换掉子进程,父进程将request中的参数传给cgi进程,cgi进程处理完后将结果返回给父进程 */// 走这里request一定是POST或者带参的GET方法,那么此时request请求中的path就是所需要的可执行程序的路径,此时就要让这个可执行程序替换掉这里的子进程// auto &path = _request_url; // 可执行程序的路径// 想要让父子进程通信,直接用管道来实现int input[2]; // 站在父进程角度的输入和输出,父进程用input[0]读,用output[1]写int output[2];if (pipe(input) < 0) // 创建input管道{					 // 创建input管道失败LOG(ERROR, "create input pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}if (pipe(output) < 0) // 创建output管道{					  // 创建output管道失败LOG(ERROR, "create output pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}// 走到这里管道就创建好了,此时创建一个子进程就可以进行父子进程通信了// 创建子进程pid_t pid = fork();// 子进程// 设置环境变量:"QUERY_STRING="、"CONTENT_LENGTH="、"METHOD="// 重定向:将标准输入文件描述符定向到管道output读端output[0];将标准输出文件描述符定向到管道input写端input[1]// cgi程序替换子进程if (pid == 0){close(input[0]);close(output[1]);/* **************************************************设置环境变量********************************************** */if (_request_method == "GET"){std::string queryString_env = "QUERY_STRING=" + _request_url_arg;if (putenv((char *)queryString_env.c_str()) != 0){LOG(ERROR, "putenv queryString failed");exit(1);}}else if (_request_method == "POST"){std::string contentLength_env = "CONTENT_LENGTH=" + _request_uomap_header["Content-Length"];if (putenv((char *)contentLength_env.c_str()) != 0){LOG(ERROR, "POST method, putenv CONTENT_LENGTH failed");exit(1);}}std::string method_env = "METHOD=" + _request_method;if (putenv((char *)method_env.c_str()) != 0){LOG(ERROR, "putenv method failed");exit(1);}/* **************************************************设置环境变量********************************************** */dup2(output[0], 0);dup2(input[1], 1);close(output[0]);close(input[0]);execl(_request_url.c_str(), _request_url.c_str(), nullptr);LOG(ERROR, "execl failed");exit(1);}// 父进程else if (pid > 0){close(input[1]);close(output[0]);if (_request_method == "POST"){size_t total = 0, size_singleTime = 0;size_t size = _request_content.size();while (total < size){size_singleTime = write(output[1], (void *)(_request_content.c_str() + total), size - total);if (size_singleTime <= 0) // 给子进程发送数据失败{int k = kill(pid, SIGTERM); // 给子进程发送结束信号if (k == 0){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else if (k == -1 && errno == ESRCH){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else{LOG(FATAL, "kill(pid, SIGTERM) failed");return SERVER_ERROR;}}else{total += size_singleTime;}}}// 发送完数据之后就接收CGI进程返回的结果char ch;while (read(input[0], &ch, 1) > 0){_response_content.push_back(ch);}cout << "-->" << "server get result:\n"<< _response_content << endl;close(input[0]);close(output[1]);int status = 0;pid_t _pid = waitpid(-1, &status, 0);if (pid == _pid){if (WIFEXITED(status)){if (WEXITSTATUS(status) != 0){cout << "exit code ::" << WEXITSTATUS(status) << endl;return BAD_REQUEST;}else{cout << "exit code ::" << WEXITSTATUS(status) << endl;return OK;}}else{LOG(WARNING, "WIFEXITED(status) is false");return SERVER_ERROR;}}else{LOG(WARNING, "waitpid is false");return SERVER_ERROR;}// 子进程替换之后会执行完毕,此时替换进程整个空间都会被回收,所以替换进程中可以不需要手动关input[1]和output[0]}else{LOG(WARNING, "fork is false");close(input[0]);close(output[1]);return SERVER_ERROR;}// return OK;
}/* ****************************************************事务入口************************************************************ *//* 构建请求 */
StatusCode http_conn::h_read()
{StatusCode ret = OK;ret = h_build_reqline();if (ret != OK)return ret;ret = h_build_reqmethod();if (ret != OK)return ret;ret = h_parse_method();if (ret != OK)return ret;ret = h_build_requrl();if (ret != OK)return ret;ret = h_parse_url();if (ret != OK)return ret;ret = h_build_reqsuffix();if (ret != OK)return ret;ret = h_build_reqversion();if (ret != OK)return ret;ret = h_build_reqvheader();if (ret != OK)return ret;ret = h_build_requomapheader();if (ret != OK)return ret;ret = h_build_reqcontent();if (ret != OK)return ret;if (_cgi){ret = h_cgi();if (ret != OK)return ret;}return OK;
}void http_conn::HandlerERROR(const std::string &page, int ret)
{std::string pagePath = WEB_ROOT;pagePath += '/';pagePath += page;int fd = open(pagePath.c_str(), O_RDONLY);if (fd >= 0){/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(ret);_response_line += ' ';_response_line += statusDescMap[ret];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";struct stat st;stat(pagePath.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += "text/html; charset=UTF-8";tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;/* 发送状态行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 发送响应头 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 发送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 发送资源 */sendfile(_sock, fd, nullptr, st.st_size); // 发送响应正文close(fd);}else{LOG(WARNING, "open false");}
}void http_conn::HandlerOK()
{if (!_cgi){int fd = open(_request_url.c_str(), O_RDONLY);if (fd > 0){/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";struct stat st;stat(_request_url.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;// /* 发送状态行 */// send(_sock, _response_line.c_str(), _response_line.size(), 0);// /* 发送响应头 */// for (auto &str : _response_v_header)// {// 	send(_sock, str.c_str(), str.size(), 0);// }// /* 发送空行 */// send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);// /* 发送资源 */// sendfile(_sock, fd, nullptr, st.st_size);// 构建响应首部大字符串 big_strstd::string big_str;big_str += _response_line;for (auto &str : _response_v_header){big_str += str;}big_str += _response_blankline;// 发送响应首部ssize_t first = 0;while (first < big_str.size()){ssize_t bytes = send(_sock, big_str.c_str() + first, big_str.size() - first, 0);if (bytes == -1){LOG(FATAL, "send errno");break;}first += bytes;}// 发送文件first = 0;while (first < st.st_size){ssize_t bytes = sendfile(_sock, fd, &first, st.st_size - first);if (bytes == -1){LOG(FATAL, "sendfile errno");break;}first += bytes;}cerr << "first: " << first << endl;close(fd);}}else{/* 构建状态行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 构建响应报头 */std::string tmp = "Content-Length: ";tmp += std::to_string(_response_content.size());tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 构建响应空行 */_response_blankline = END_OF_LINE;// /* 发送状态行 */// send(_sock, _response_line.c_str(), _response_line.size(), 0);// /* 发送响应头 */// for (auto &str : _response_v_header)// {// 	send(_sock, str.c_str(), str.size(), 0);// }// /* 发送空行 */// send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);// /* 发送响应正文 */// send(_sock, _response_content.c_str(), _response_content.size(), 0);//	构建响应首部大字符串 big_strstd::string big_str;big_str += _response_line;for (auto &str : _response_v_header){big_str += str;}big_str += _response_blankline;// 发送响应首部ssize_t first = 0;while (first <= big_str.size()){ssize_t bytes = send(_sock, big_str.c_str() + first, big_str.size() - first, 0);first += bytes;}// 发送响应正文first = 0;while (first <= _response_content.size()){ssize_t bytes = send(_sock, _response_content.c_str() + first, _response_content.size() - first, 0);first += bytes;}}
}void http_conn::h_write(StatusCode ret)
{switch (ret){case OK:HandlerOK();break;case NOT_FOUND:HandlerERROR(NOT_FOUND_PAGE, ret);break;case BAD_REQUEST:HandlerERROR(BAD_REQUEST_PAGE, ret);break;case SERVER_ERROR:HandlerERROR(SERVER_ERROR_PAGE, ret);break;case CLOSE:close(_sock);_sock = -1;break;default:HandlerERROR(NOT_FOUND_PAGE, ret);break;}
}

        http_conn.hpp

#pragma once
#include "LOG.hpp"
#define WEB_ROOT "wwwroot"			 // Web根目录
#define HOME_PAGE "index.html"		 // 默认资源文件
#define BAD_REQUEST_PAGE "400.html"	 // 请求错误
#define NOT_FOUND_PAGE "404.html"	 // 资源不存在
#define SERVER_ERROR_PAGE "500.html" // 服务器出错
#define SPACE " "					 // 空格
#define END_OF_LINE "\r\n"			 // 行尾回车换行enum StatusCode
{CLOSE = 0,		   // 连接关闭OK = 200,		   // 正常情况BAD_REQUEST = 400, // 请求错误NOT_FOUND = 404,   // 资源不存在SERVER_ERROR = 500 // 服务器出错
};static std::unordered_map<int, std::string> statusDescMap ={{200, "OK"},{400, "BAD_REQUEST"},{404, "NOT_FOUND"},{500, "SERVER_ERROR"},
};static std::unordered_map<std::string, std::string> suffixMap ={{"html","text/html; charset=UTF-8"},{"css", "text/css"},{"js", "application/javascript"},{"jpg", "image/jpeg"},{"xml", "application/xml"},{"cgi", "text/html; charset=UTF-8"},};class http_conn
{
private:int _sock; // 用于获得这个客户连接的socket
public:std::string _request_line;											// 请求行std::vector<std::string> _request_v_header;							// 请求头数组std::unordered_map<std::string, std::string> _request_uomap_header; // 请求头哈希表std::string _request_content;										// 请求正文std::string _request_method;  // 请求方法std::string _request_url;	  // 请求urlstd::string _request_url_arg; // 请求参数std::string _requst_version;  // 请求版本std::string _request_suffix;  // 资源类型
public:std::string _response_line;					 // 响应行std::vector<std::string> _response_v_header; // 响应头数组std::string _response_blankline;			 // 响应空行std::string _response_content;				 // 响应正文
public:bool _cgi;					   // 是否采用cgi机制
public:// 构造函数http_conn(int sock) : _sock(sock), _cgi(false), _request_suffix("html"){}~http_conn(){if(_sock >= 0) close(_sock);}public:
void h_run()
{StatusCode ret = h_read();h_write(ret);
}private:
StatusCode h_read();
void h_write(StatusCode ret);private:// 构建请求行_request_line// 失败则返回:CLOSE// 成功则返回:OKStatusCode h_build_reqline();// 构建请求方法_request_method// 失败则返回:BAD_REQUEST// 成功则返回:OKStatusCode h_build_reqmethod();// 判断请求方法是否正确// 我们只处理"GET""POST"请求// 其余请求返回:BAD_REQUEST// 这两个请求返回:OKStatusCode h_parse_method();// 构建url,顺带着构建参数_request_url_arg// 失败则返回:BAD_REQUEST// 成功则返回:OKStatusCode h_build_requrl();// 判断url是否合法// 非法情况:资源不存在,资源无权读,资源无权执行// 设置_cgi// 失败则返回:NOT_FOUND// 成功则返回:OKStatusCode h_parse_url();StatusCode h_build_reqsuffix();// 构建_request_line// 任务不存在出错情况// 只返回:OKStatusCode h_build_reqversion();// 构建请求头数组_request_v_header// 读取错误返回:CLOSE// 正确则返回:OKStatusCode h_build_reqvheader();// 构建请求头哈希表_request_uomap_header// 请求头格式错误则返回:BAD_REQUEST// 正确则返回:OKStatusCode h_build_requomapheader();// 构建请求正文_request_content// 读取错误返回:CLOSE// 正确则返回:OKStatusCode h_build_reqcontent();// 执行cgi程序StatusCode h_cgi();private:void HandlerERROR(const std::string &page, int ret);void HandlerOK();private:// 从tcp接收缓冲区拿走一行数据// 这里我们阻塞式读取,直到遇到 "\r\n"结束// 所以要么我们取走一行完整数据,要么对方关闭连接;h_recv_line才会返回int h_recv_line(int _sock, std::string &str);// 按照sep切分字符串bool cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep);
};

        LOG.hpp

#pragma once
#include "common.hpp"using std::cout;
using std::cerr;
using std::endl;enum LogLevel
{INFO,WARNING,ERROR,FATAL
};// 宏替换能保证每次调用的地方都是对应的文件和行
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__) // 打印日志// 只声明 Log 函数
void Log(const std::string& level, const std::string& message, const std::string& fileName, int line);

        LOG.cpp

#include "LOG.hpp"void Log(const std::string& level, const std::string& message, const std::string& fileName, int line)
{// 日志格式:// [level]<file:line>{日志内容}==>timetime_t tm = time(nullptr);cerr << "[" << level << "](" << fileName << ":" << line << ")" << "{" << message << "}==> " << ctime(&tm);
}

        common.hpp

#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>#include <arpa/inet.h>
#include <pthread.h>#include<iostream>#include <exception>
#include <semaphore.h>#include <list>
#include <ctime>

        locker.hpp

#pragma once
#include "common.hpp"// 封装信号量
class sem
{
public:sem(){if( sem_init( &m_sem, 0, 0 ) != 0 ){throw std::exception();}}~sem(){sem_destroy( &m_sem );}bool wait(){return sem_wait( &m_sem ) == 0;}bool post(){return sem_post( &m_sem ) == 0;}private:sem_t m_sem;
};// 封装后斥锁
class locker
{
public:locker(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}}~locker(){pthread_mutex_destroy( &m_mutex );}bool lock(){return pthread_mutex_lock( &m_mutex ) == 0;}bool unlock(){return pthread_mutex_unlock( &m_mutex ) == 0;}private:pthread_mutex_t m_mutex;
};// 封装条件变量
class cond
{
public:cond(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}if ( pthread_cond_init( &m_cond, NULL ) != 0 ){pthread_mutex_destroy( &m_mutex );throw std::exception();}}~cond(){pthread_mutex_destroy( &m_mutex );pthread_cond_destroy( &m_cond );}bool wait(){int ret = 0;pthread_mutex_lock( &m_mutex );ret = pthread_cond_wait( &m_cond, &m_mutex );pthread_mutex_unlock( &m_mutex );return ret == 0;}bool signal(){return pthread_cond_signal( &m_cond ) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};

        threadpool.hpp

#pragma once
#include "locker.hpp"
#include "LOG.hpp"template <typename T>
class threadpool
{
public:threadpool(int thread_number = 8, int max_requests = 10000);~threadpool();bool append(T request);private:static void *worker(void *arg);void run();private:int m_thread_number;        /* 线程池中线程的数量 */int m_max_requests;         /* 任务队列的大小 */pthread_t *m_threads;       /* 描述线程池的数组,其大小为m_thread_number */// 线程池内部维护请求队列,意味着线程池要提供append接口// 请求队列或者说任务队列,本身属于临界资源,需要加锁访问// 请求队列中存储的是指向http_conn在堆区位置的指针std::list<T> m_workqueue; /* 任务队列 */locker m_queuelocker;       /* 保护请求队列的互斥锁 */sem m_queuestat;            /* 是否有任务需要处理 */bool m_stop;                /* 是否结束线程 */
};// 构造函数
template <typename T>
threadpool<T>::threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL)
{if ((thread_number <= 0) || (max_requests <= 0)){LOG(FATAL,"线程池创建时参数有误");}m_threads = new pthread_t[m_thread_number];if (!m_threads){LOG(ERROR,"线程标识符数组创建失败");}for (int i = 0; i < thread_number; ++i){if (pthread_create(m_threads + i, NULL, worker, this) != 0){delete[] m_threads;LOG(FATAL,"线程创建失败");}if (pthread_detach(m_threads[i])){delete[] m_threads;LOG(FATAL,"线程分离失败");}}std::cerr << "***********************************全部线程创建成功***********************************" << std::endl;
}template <typename T>
threadpool<T>::~threadpool()
{delete[] m_threads;m_stop = true;LOG(INFO,"线程池已析构");
}template <typename T>
bool threadpool<T>::append(T request)
{m_queuelocker.lock();if (m_workqueue.size() > m_max_requests){m_queuelocker.unlock();return false;}m_workqueue.push_back(request);m_queuelocker.unlock();m_queuestat.post();//增加信号量return true;
}template <typename T>
void *threadpool<T>::worker(void *arg)
{threadpool *pool = (threadpool *)arg;pool->run();return pool;
}template <typename T>
void threadpool<T>::run()
{while (!m_stop){// 申请信号量LOG(INFO,"有个线程在申请信号量");m_queuestat.wait();LOG(INFO,"有个线程申请信号量成功");m_queuelocker.lock();// 请求队列为空if (m_workqueue.empty()){m_queuelocker.unlock();continue;}// 从任务队列取走任务T request = m_workqueue.front();m_workqueue.pop_front();LOG(INFO,"有个线程取走任务");m_queuelocker.unlock();if (!request){continue;}// 执行任务LOG(INFO,"有个线程开始执行任务");request->h_run();delete request;LOG(INFO,"有个线程执行任务结束");}
}

        main.cpp

#include "http_conn.hpp"
#include "threadpool.hpp"
#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);threadpool<http_conn*>* pool = new threadpool<http_conn*>(10,10000);// 此时已经有10个线程在嗷嗷待哺while (1){cerr << "******************************************开始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}http_conn *p_http = new http_conn(connfd);pool->append(p_http);cerr << "******************************************结束********************************************" << endl;}close(listenfd);return 0;
}

        后续无论再请求多少次,文件描述都只能打开一个(以及操作系统自己打开的和保持网络连接的),大概长这样

  1. 这样每到来一个连接,我们就把它放在连接队列里
  2. 然后线程一个个取走这些连接,并处理即可。

接下来我们写一个进程池版本的建议cgi服务器,目的是为了分析进程池的各种特点。

进程池版本的cgi服务器

        服务器目录结构

         processpool.h

#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>// process是一个子进程类
class process
{
public:process() : m_pid( -1 ){}public:pid_t m_pid;    // 子进程pidint m_pipefd[2];// 父子管道
};/* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ */
// 模板参数T--->CGI类
template< typename T >
class processpool
{
private:// 将构造函数定义为私有的,因此我们只能通过后面的create静态函数来创建processpool实例processpool( int listenfd, int process_number = 8 );
public:// 单例模式,以保证程序最多创建一个processpoo1实例,这是程序正确处理信号的必要条件(这句话最为关键)// 在这个地方细心的同学可能已经发现了,下面这种获取单例的模式不是线程安全的,可是这里依然是正确的// 什么跟线程安全联系在一起,是线程竞争资源,这里不存在竞争这种情况/* 在一个多进程或多线程的程序中,信号处理需要保持全局一致性。信号是异步事件,操作系统可以在任何时刻向进程发送信号,如 SIGTERM(终止信号)、SIGINT(中断信号)等。如果程序中有多个 processpool 实例,每个实例可能会独立地设置信号处理逻辑,这就会导致信号处理的混乱。例如,当收到 SIGTERM 信号时,多个 processpool 实例可能会同时尝试关闭不同的资源或者执行不同的清理操作,这样会造成资源管理的混乱,甚至可能导致程序崩溃。而单例模式确保整个程序中只有一个 processpool 实例,所有的信号处理逻辑都由这个唯一的实例来管理,保证了信号处理的一致性和正确性 */static processpool< T >* create( int listenfd, int process_number = 8 ){if( !m_instance ){m_instance = new processpool< T >( listenfd, process_number );}return m_instance;}~processpool(){delete [] m_sub_process;}// 启动进程池,自此程序完全交给进程池void run();private:void setup_sig_pipe();void run_parent();// 父进程在创建子进程之后的运行逻辑void run_child();// 子进程在被父进程创建后的运行逻辑private:static const int MAX_PROCESS_NUMBER = 16;   //进程池允许的最大子进程数量static const int USER_PER_PROCESS = 65536;  //子进程最多能处理的客户数量static const int MAX_EVENT_NUMBER = 10000;  //epoll最多能处理的事件数,该常量交给epoll_waitint m_process_number;                       //进程池中的进程总数int m_idx;                                  //子进程在进程池中的序号(大家对此或许会存在疑问,不着急往下看)int m_epollfd;                              //每个进程(注意不是每个子进程)的epoll事件表int m_listenfd;                             //监听socketint m_stop;                                 //子进程通过m_stop决定是否停止运行process* m_sub_process;                     //保存所有子进程的描述信息static processpool< T >* m_instance;        //进程池静态实例
};
template< typename T >
processpool< T >* processpool< T >::m_instance = NULL;
/* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ */// 信号管道--->同一事件源    每个进程都会有这个信号管道
static int sig_pipefd[2];/**********************************************************************************************/
// 静态方法:设置文件描述符为非阻塞
static int setnonblocking( int fd )
{int old_option = fcntl( fd, F_GETFL );int new_option = old_option | O_NONBLOCK;fcntl( fd, F_SETFL, new_option );return old_option;
}// 静态方法:向m_epollfd中注册该文件fd读事件,et模式
static void addfd( int epollfd, int fd )
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );setnonblocking( fd );
}// 静态方法:从m_epollfd中移除该文件fd,并且关闭该文件描述符
static void removefd( int epollfd, int fd )
{epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );close( fd );
}// 静态方法:信号捕捉函数,处理逻辑为--->将sig信号写进sig_pipefd[1]
static void sig_handler( int sig )
{int save_errno = errno;int msg = sig;send( sig_pipefd[1], ( char* )&msg, 1, 0 );errno = save_errno;
}// 静态方法:为信号sig自定义信号捕捉,执行信号捕捉函数期间屏蔽所有信号
static void addsig( int sig, void( handler )(int), bool restart = true )
{struct sigaction sa;memset( &sa, '\0', sizeof( sa ) );sa.sa_handler = handler;if( restart ){sa.sa_flags |= SA_RESTART;}sigfillset( &sa.sa_mask );assert( sigaction( sig, &sa, NULL ) != -1 );
}
/**********************************************************************************************//*¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥*/
// 进程池构造函数
// listenfd用于初始化m_listenfd
// process_number用于初始设置进程池中的进程总数m_process_number
// m_idx是:子进程在进程池中的序号,全部初始化为-1// 父进程是main函数进程
// 父进程先获得进程池静态实例,随后父进程创建堆区m_sub_process,父进程创建全双工父子信道
// 子进程继承了父进程静态进程池实例,继承了父进程堆区m_sub_process,继承了全双工父子信道
//此后父进程具有文件描述符:listenfd(物理位置在栈区),process_number个m_sub_process[i].m_pipefd[0](物理位置在堆区)
//此后子进程i具有文件描述符:listenfd(物理位置在栈区),m_sub_process[i].m_pipefd[1](物理位置在堆区),还有吗?有!
// 父进程不会修改自己实例中的m_idx(-1),每个子进程都会设置自己的m_idx,子进程序号从0开始。
// 父进程堆区m_sub_process[i].m_pid存放了子进程pid,而子进程堆区m_sub_process[i].m_pid == -1;
template< typename T >
processpool< T >::processpool( int listenfd, int process_number ) : m_listenfd( listenfd ), m_process_number( process_number ), m_idx( -1 ), m_stop( false )
{assert( ( process_number > 0 ) && ( process_number <= MAX_PROCESS_NUMBER ) );m_sub_process = new process[ process_number ];//堆区保存所有子进程的描述信息,初始值为8assert( m_sub_process );for( int i = 0; i < process_number; ++i ){/*  int m_pipefd[2];// 全双工父子管道 */int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd );assert( ret == 0 );m_sub_process[i].m_pid = fork();assert( m_sub_process[i].m_pid >= 0 );if( m_sub_process[i].m_pid > 0 )// 父进程关闭m_sub_process[i].m_pipefd[1]{close( m_sub_process[i].m_pipefd[1] );continue;}// 子进程关闭m_sub_process[i].m_pipefd[0]--->此处是否会造成文件描述的泄露呢?// 父进程要与多个子进程通信,自然就有多个 m_sub_process[i].m_pipefd[0]// 父进程在创建第I个子进程时,已经有 m_sub_process[0].m_pipefd[0]、 m_sub_process[1].m_pipefd[0]...... m_sub_process[i-1].m_pipefd[0]    // 那么第i个子进程只关闭 m_sub_process[i].m_pipefd[0]后,还有之前的很多个无用的文件描述符没有被关闭呀   else{close( m_sub_process[i].m_pipefd[0] );m_idx = i;break;}}
}// 统一事件源
// 创建m_epollfd,创建并注册全双工信号管道sig_pipefd,设置非阻塞fd,统一SIGCHLD、SIGTERM、SIGINT信号处理,忽略SIGPIPE
template< typename T >
void processpool< T >::setup_sig_pipe()
{m_epollfd = epoll_create( 5 );assert( m_epollfd != -1 );int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, sig_pipefd );assert( ret != -1 );setnonblocking( sig_pipefd[1] );addfd( m_epollfd, sig_pipefd[0] );addsig( SIGCHLD, sig_handler );addsig( SIGTERM, sig_handler );addsig( SIGINT, sig_handler );addsig( SIGPIPE, SIG_IGN );
}// 事件按m_idx分发给父进程和子进程
template< typename T >
void processpool< T >::run()
{if( m_idx != -1 ){run_child();return;}run_parent();
}
/*¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥*/// 子进程处理逻辑
// 子进程i具有文件描述符:listenfd(物理位置在栈区),m_sub_process[i].m_pipefd[1](物理位置在堆区),还有吗?有!
template< typename T >
void processpool< T >::run_child()
{// 子进程i具有文件描述符:listenfd,m_sub_process[i].m_pipefd[1],sig_pipefd,还有吗?有!// 子进程关心的文件描述符:sig_pipefd[0],m_sub_process[m_idx].m_pipefd[ 1 ]setup_sig_pipe();int pipefd = m_sub_process[m_idx].m_pipefd[ 1 ];// 此处仅仅只是值的简单复制而已addfd( m_epollfd, pipefd );epoll_event events[ MAX_EVENT_NUMBER ];// T--->cgi_conn// users是一个数组,数组元素是cgi_connT* users = new T [ USER_PER_PROCESS ];assert( users );int number = 0;int ret = -1;while( ! m_stop ){number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );//阻塞式/* 在等待过程中,函数被信号中断。应用程序通常可以再次调用 epoll_wait 继续等待。例如,当程序注册了某个信号处理函数,在 epoll_wait 等待期间该信号被触发,epoll_wait 就可能因被信号中断而返回 -1 且 errno 为 EINTR。 */if ( ( number < 0 ) && ( errno != EINTR ) ){printf( "epoll failure\n" );break;}// 父进程向子进程发来了数据for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;// if( ( sockfd == pipefd ) && ( events[i].events & EPOLLIN ) ){// 注意此处只读取一个数据,仅仅起到个提醒作用:有新连接了,你来取吧。int client = 0;ret = recv( sockfd, ( char* )&client, sizeof( client ), 0 );// 读取失败或者对端关闭,进行下一轮(健壮性)if( ( ( ret < 0 ) && ( errno != EAGAIN ) ) || ret == 0 ) {continue;}// 读取成功,开始接管一个新连接else{struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( m_listenfd, ( struct sockaddr* )&client_address, &client_addrlength );if ( connfd < 0 ){printf( "errno is: %d\n", errno );continue;}// 子进程关心的文件描述符:sig_pipefd[0],m_sub_process[m_idx].m_pipefd[ 1 ],connfdaddfd( m_epollfd, connfd );users[connfd].init( m_epollfd, connfd, client_address );}}else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) ){int sig;char signals[1024];ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );if( ret <= 0 ){continue;}else{for( int i = 0; i < ret; ++i ){switch( signals[i] ){case SIGCHLD:{pid_t pid;int stat;while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 ){continue;}break;}case SIGTERM:case SIGINT:{m_stop = true;break;}default:{break;}}}}}// 客户数据到来else if( events[i].events & EPOLLIN ){users[sockfd].process();//此处会阻塞}else{continue;}}}delete [] users;users = NULL;close( pipefd );//close( m_listenfd );close( m_epollfd );
}// 父进程处理逻辑
// 开始前父进程打开的文件描述符:listenfd(物理位置在栈区),process_number个m_sub_process[i].m_pipefd[0](物理位置在堆区)
template< typename T >
void processpool< T >::run_parent()
{// 父进程打开的文件描述符:listenfd,process_number个m_sub_process[i].m_pipefd[0],sig_pipefd// 父进程关心的文件描述符:listened,sig_pipefd[0]setup_sig_pipe();addfd( m_epollfd, m_listenfd );epoll_event events[ MAX_EVENT_NUMBER ];int sub_process_counter = 0;int new_conn = 1;int number = 0;int ret = -1;while( ! m_stop ){number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );//阻塞式/* 在等待过程中,函数被信号中断。应用程序通常可以再次调用 epoll_wait 继续等待。例如,当程序注册了某个信号处理函数,在 epoll_wait 等待期间该信号被触发,epoll_wait 就可能因被信号中断而返回 -1 且 errno 为 EINTR。 */if ( ( number < 0 ) && ( errno != EINTR ) ){printf( "epoll failure\n" );break;}for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;// 收到一个新连接:if( sockfd == m_listenfd ){int i =  sub_process_counter;do{if( m_sub_process[i].m_pid != -1 ){break;}i = (i+1)%m_process_number;}while( i != sub_process_counter );if( m_sub_process[i].m_pid == -1 ){m_stop = true;break;}sub_process_counter = (i+1)%m_process_number;send( m_sub_process[i].m_pipefd[0], ( char* )&new_conn, sizeof( new_conn ), 0 );printf( "send request to child %d\n", i );}else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) ){int sig;char signals[1024];ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );if( ret <= 0 ){continue;}else{for( int i = 0; i < ret; ++i ){switch( signals[i] ){case SIGCHLD:{pid_t pid;int stat;while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 ){for( int i = 0; i < m_process_number; ++i ){if( m_sub_process[i].m_pid == pid ){printf( "child %d join\n", i );close( m_sub_process[i].m_pipefd[0] );m_sub_process[i].m_pid = -1;}}}m_stop = true;for( int i = 0; i < m_process_number; ++i ){if( m_sub_process[i].m_pid != -1 ){m_stop = false;}}break;}case SIGTERM:case SIGINT:{printf( "kill all the clild now\n" );for( int i = 0; i < m_process_number; ++i ){int pid = m_sub_process[i].m_pid;if( pid != -1 ){kill( pid, SIGTERM );}}break;}default:{break;}}}}}else{continue;}}}//close( m_listenfd );close( m_epollfd );
}#endif

       server.cpp

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include "processpool.h"
#include "cgi.h"int main( int argc, char* argv[] )
{if( argc <= 1 ){printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );return 1;}// const char* ip = argv[1];int port = atoi( argv[1] );int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;// inet_pton( AF_INET, ip, &address.sin_addr );address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret != -1 );ret = listen( listenfd, 5 );assert( ret != -1 );// 此时主进程打开的文件描述符只有 0 1 2 和 listenfdprocesspool<cgi_conn >* pool = processpool<cgi_conn >::create(listenfd);if(pool){pool->run();delete pool;}close(listenfd);return 0;
}

         cgi.h

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include "processpool.h"
#include <iostream>
class cgi_conn
{
private:static const int BUFFER_SIZE = 1024; // 读缓冲区大小static int m_epollfd;                // 全局epollfd句柄int m_sockfd;                        // 客户连接sockaddr_in m_address;               // 客户信息char m_buf[BUFFER_SIZE];             // 读取缓冲区int m_read_idx;                      // 标记读缓冲中已经读入的客户数据的最后一个字节的下一个位置
public:cgi_conn() {}~cgi_conn() {}/*初始化客户连接,清空读缓冲区*/void init(int epollfd, int sockfd, const sockaddr_in &client_addr){m_epollfd = epollfd;m_sockfd = sockfd;m_address = client_addr;memset(m_buf, '\0', BUFFER_SIZE);m_read_idx = 0;}void process(){int idx = 0;int ret = -1;/*循环读取和分析客户数据*/while (true){idx = m_read_idx;ret = recv(m_sockfd, m_buf + idx, BUFFER_SIZE - 1 - idx, 0);if (ret < 0){if(errno != EAGAIN){removefd(m_epollfd,m_sockfd);}break;}else if( ret == 0){removefd(m_epollfd,m_sockfd);break;}else{m_read_idx += ret;for(;idx<m_read_idx;++idx){if((idx >= 1) && (m_buf[idx-1] =='\r') &&(m_buf[idx] == '\n')){break;}}if(idx == m_read_idx){continue;}m_buf[idx-1] == '\0';ret = fork();if(ret == -1){removefd(m_epollfd,m_sockfd);std::cout << 1 << std::endl;break;}else if(ret > 0){removefd(m_epollfd,m_sockfd);std::cout << 2 << std::endl;break;}else{close(STDOUT_FILENO);dup(m_sockfd);std::cout << 3 << std::endl;execl("test","test",0);exit(0);}}}}
};
int cgi_conn::m_epollfd = -1;

         test.cpp

#include <iostream>
int main()
{std::cout << "我是cgi程序,我已经处理还数据" << std::endl;return 0;
}

        对于该服务器我们重点关注进程池本身

        进程池代码分析

        此处的进程池单例真的是个单例吗?

        当父进程fork()子进程后,父子进程会修改进程池部分成员变量的值,采用的是写时复制的原则。对于进程池内父子进程执行逻辑的代码不会改变,这也就决定父子进程依旧遵循我们程序设计之初为其设计的蓝图。也就是说这部分代码依旧唯一存在,发挥单例作用。

        子进程继承的文件描述符

  1. 子进程关闭m_sub_process[i].m_pipefd[0]--->此处是否会造成文件描述的泄露呢
  2. 父进程要与多个子进程通信,自然就有多个 m_sub_process[i].m_pipefd[0
  3. 父进程在创建第I个子进程时,已经有 m_sub_process[0].m_pipefd[0]、 m_sub_process[1].m_pipefd[0]...... m_sub_process[i-1].m_pipefd[0]    
  4. 那么第i个子进程只关闭 m_sub_process[i].m_pipefd[0]后,还有之前的很多个无用的文件描述符没有被关闭,这确实会占用系统资源,是需要优化的地方
  5. 但是这不属于文件描述符泄露,只要进程池启动完毕,无论后续提供多少次服务,系统内部的文件描述符总量不变。而文件描述符泄露的特点是,随服务次数的增多,系统内部文件描述符越来越多。
        统一事件源

        该部分代码用于统一在主循环中进行信号处理,做到统一事件源

        SIGCHLD信号

  

        子进程进行进程替换之后,原先的代码段已经全部销毁,通过SIGCHLD信号进行子进程回收,是较为高效的一种做法

        父子通信管道

        父子进程通过管道来进行交流通信,是较为经典的一种做法。在这里父进程主要用于提醒子进程有连接到来。

        其余方面不再赘述,在代码中我写了不少的注释,用于辅助大家进行理解。这里只是简易演示进程池机制,大家体会一下即可。

总结

  1. 此时我们构建的http服务器相对来讲已经比较完善
  2. 根据http协议,http请求是基于短连接的,所以说浏览器与服务器建立连接后,将立即发送数据,而且会将数据一次性发送完。所以可以认为根本不存在我们上述说的:线程由于迟迟接受不到数据而一直卡住的情况。这是http协议的特性让我们被动规避了接收问题。
  3. 接下来我们将编写一个聊天室程序,脱离http协议,只关注服务器本身。这样可以让我们更加全面的去分析问题,以及摆脱http各种请求头、响应头的相关协议规定让我们更加专注于服务器本身的逻辑。

逐步构建高性能聊天室服务器

服务器逻辑:

        服务器逻辑很简单:

  1. 会有多个客户端chatclient与服务器chatserver相连接
  2. 每个客户上线,服务器就给其他客户发送该客户上线了
  3. 每个客户下线,服务器就给其他客户发送该客户下线了
  4. 每个客户发送的消息,服务器就转发给其他客户

基于epoll的多线程版本聊天室服务器

        服务器目录结构

        客户端目录结构

        chatserver.cpp

#include "LOG.hpp"
#include <sys/epoll.h>
#define MAX_EVENT_NUMBER 10000
#define BUFFER_SIZE 1024using std::cerr;
using std::cin;
using std::cout;
using std::endl;std::unordered_map<int, std::string> users; // 存储在线客户// 设置文件描述符为非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 将文件描述符上的读事件注册进内核事件表,ET + EPOLLONESHOT模式;并设置文件描述符为非阻塞
// ET + EPOLLONESHOT模式:ET模式下,只有数据被全部读完后,下次到来数据才会提醒;而该模式下,设置ET + EPOLLONESHOT后,用于只提醒一次,除非重新设置
// 原因:这样在同一时刻,一个连接永远只有一个线程在为其服务
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;if (one_shot){event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}
// 重新添加文件描述符上的读事件
void reset_oneshot(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 将文件描述符从内核事件表中移除
void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}class user_info
{
public:int user_sock;std::string user_name;int user_epollfd;public:user_info(int sock, std::string name, int epollfd = 0) : user_sock(sock), user_name(name), user_epollfd(epollfd) {}
};// 由于用户p_user_info上下线或者发送数据,而转发位置在p_str的num字节数据给其他用户
void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}// 该函数基于:线程有独立的栈结构
void *handler_online(void *args)
{user_info *p_user_info = (user_info *)args;std::string str;str += p_user_info->user_name + ": online";send_turn(p_user_info, str.c_str(), str.size());LOG(INFO, "handler_online success " + str);delete p_user_info;
}void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收数据if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 对方关闭连接if (bytes == 0){// 该用户已离开std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除该文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users该成员users.erase(p_user_info->user_sock);// 关闭该连接close(p_user_info->user_sock);break;}if (bytes < 0){// 已经转发完所有数据且对方没有关闭连接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加该文件描述符上的读事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);if (epollfd == -1){LOG(FATAL, "epoll_create failed");}// 监听套接字不能设置为只提醒一次addfd(epollfd, listenfd, false);while (1){int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if ((number < 0) && (errno != EINTR)){LOG(FATAL, "epoll_wait failed");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 新连接到来if (sockfd == listenfd){int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);// 注册客户连接文件描述符,设置为只提醒一次addfd(epollfd, connfd, true);std::string client_ip = inet_ntoa(client_address.sin_addr);int client_port = ntohs(client_address.sin_port);std::string user = client_ip + std::to_string(client_port);if (users[connfd] == "") // 这代表客户首次到来,我们需要告诉其他人,这个客户上线了{// 先将该用户存储在在线用户表中users[connfd] = user;// 新启一个线程去发送上线通知user_info *p_user_info = new user_info(connfd, user);pthread_t tid;pthread_create(&tid, NULL, handler_online, (void *)p_user_info);// 分离该线程pthread_detach(tid);}}// 新数据到来或者对端关闭连接else if (events[i].events & EPOLLIN){user_info *p_user_info = new user_info(sockfd, users[sockfd], epollfd);pthread_t tid;pthread_create(&tid, NULL, handler_turn, (void *)p_user_info);// 分离该线程pthread_detach(tid);}else{}}}close(listenfd);return 0;
}

        chatclient.cpp

#include "LOG.hpp"
#include <poll.h>
#define BUFFER_SIZE 64int main( int argc, char* argv[] )
{if( argc <= 2 ){printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );return 1;}const char* ip = argv[1];int port = atoi( argv[2] );struct sockaddr_in server_address;bzero( &server_address, sizeof( server_address ) );server_address.sin_family = AF_INET;inet_pton( AF_INET, ip, &server_address.sin_addr );server_address.sin_port = htons( port );int sockfd = socket( PF_INET, SOCK_STREAM, 0 );if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 ){printf( "connection failed\n" );close( sockfd );return 1;}pollfd fds[2];fds[0].fd = 0;fds[0].events = POLLIN;fds[0].revents = 0;fds[1].fd = sockfd;fds[1].events = POLLIN | POLLRDHUP;fds[1].revents = 0;char read_buf[BUFFER_SIZE];int pipefd[2];int ret = pipe( pipefd );while( 1 ){ret = poll( fds, 2, -1 );if( ret < 0 ){printf( "poll failure\n" );break;}if( fds[1].revents & POLLRDHUP ){printf( "server close the connection\n" );break;}else if( fds[1].revents & POLLIN ){memset( read_buf, '\0', BUFFER_SIZE );recv( fds[1].fd, read_buf, BUFFER_SIZE-1, 0 );printf( "%s\n", read_buf );}if( fds[0].revents & POLLIN ){ret = splice( 0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );ret = splice( pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );}}close( sockfd );return 0;
}

        common.hpp

#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>#include <arpa/inet.h>
#include <pthread.h>#include<iostream>#include <exception>
#include <semaphore.h>#include <list>
#include <ctime>#include <iomanip>
#include <sstream>

  代码分析

        ET + EPOLLONESHOT模式下
void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收数据if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 对方关闭连接if (bytes == 0){// 该用户已离开std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除该文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users该成员users.erase(p_user_info->user_sock);// 关闭该连接close(p_user_info->user_sock);break;}if (bytes < 0){// 已经转发完所有数据且对方没有关闭连接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加该文件描述符上的读事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}

        这段代码很值得大家去分析。在ET + EPOLLONESHOT模式下,进程先读取数据,由于每次通信都是聊天式的,收发数据量都不大,所以说大概率一次就把数据取完,每次取完数据直接发送数据。IO是耗时操作,如果在发送数据时,该客户又到来新一批数据,则在下一轮循环中,该线程可以继续转发该用户数据。大家可以去体会一下,如果我把客户数据全部拿到后一起转发行不行呢?大家可以去修改一下上述代码,

if (bytes < 0){// 已经转发完所有数据且对方没有关闭连接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加该文件描述符上的读事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}

        如果大家把转发放在if(errno == ...)这个函数体中,发送结束后线程退出,就会引发新问题。发送数据是个耗时操作,如果该线程在这个过程中关闭连接,则无法在下一轮循环中得到这个信息,因为线程已经退出。

问题分析

        在该代码中资源竞争问题比较大

void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}

        如果此时users中被其他线程插入了新用户,则完全有可能新用户,无法收到本该在他上线后应该收到的消息。假使我们对users容器进行加锁保护,这会使效率大幅度下降,因为在同一时刻,永远只有一个线程能访问user容器,无法实现多线程转发的高效率。我们自然可以通过一些手段改进一下这个问题,但是我们转过头来想一想。一个新用户上线后的1秒中之内,没有收到技术上来讲应该收到的消息,这从宏观上讲不算什么问题。

        如果多线程send时往同一个文件描述符中写入数据,就有可能造成数据乱序问题。如果我们对send上锁,这也属于因噎废食。假使有1000个用户同时在线,那么多线程正好往一个文件描述符中写入数据的概率也不是很大。

        作为练习来讲,我们我不再花大功夫去解决诸如上述这些问题。但是在实际项目中,我们绝对要力求将其写到最佳。

        可是有个问题我们应当解决。如果短时间内突然需要大量转发操作,那么操作系统内部就会有大量线程,服务器压力太大,我们采用线程池去解决这个问题。

线程池版本的聊天室服务器

        服务器目录结构

        chatserver.cpp

#include "LOG.hpp"
#include <sys/epoll.h>
#include "threadpool.hpp"
#define MAX_EVENT_NUMBER 10000
#define BUFFER_SIZE 1024using std::cerr;
using std::cin;
using std::cout;
using std::endl;std::unordered_map<int, std::string> users; // 存储在线客户// 设置文件描述符为非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 将文件描述符上的读事件注册进内核事件表,ET + EPOLLONESHOT模式;并设置文件描述符为非阻塞
// ET + EPOLLONESHOT模式:ET模式下,只有数据被全部读完后,下次到来数据才会提醒;而该模式下,设置ET + EPOLLONESHOT后,用于只提醒一次,除非重新设置
// 原因:这样在同一时刻,一个连接永远只有一个线程在为其服务
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;if (one_shot){event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}
// 重新添加文件描述符上的读事件
void reset_oneshot(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 将文件描述符从内核事件表中移除
void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}class user_info
{
public:int user_sock;std::string user_name;int user_epollfd;int flag; // 0表示上线关联handler_online  1表示在线发送数据关联handler_turnpublic:user_info(int sock, std::string name, int epollfd = 0,int f =0) : user_sock(sock), user_name(name), user_epollfd(epollfd),flag(f) {}};// 由于用户p_user_info上下线或者发送数据,而转发位置在p_str的num字节数据给其他用户
void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}// 该函数基于:线程有独立的栈结构
void *handler_online(void *args)
{user_info *p_user_info = (user_info *)args;std::string str;str += p_user_info->user_name + ": online";send_turn(p_user_info, str.c_str(), str.size());LOG(INFO, "handler_online success " + str);delete p_user_info;
}void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收数据if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 对方关闭连接if (bytes == 0){// 该用户已离开std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除该文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users该成员users.erase(p_user_info->user_sock);// 关闭该连接close(p_user_info->user_sock);break;}if (bytes < 0){// 已经转发完所有数据且对方没有关闭连接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加该文件描述符上的读事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}void* handler (void* args)
{user_info *p_user_info = (user_info *)args;if(p_user_info->flag == 0){handler_online(p_user_info);}else{handler_turn(p_user_info);}
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver  port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int opt = 1;// 设置地址复用,防止服务器崩掉后进入TIME_WAIT,短时间连不上当前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);if (epollfd == -1){LOG(FATAL, "epoll_create failed");}// 监听套接字不能设置为只提醒一次addfd(epollfd, listenfd, false);threadpool<user_info*>* pool = new threadpool<user_info*>(10,10000);// 此时已经有10个线程在嗷嗷待哺while (1){int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if ((number < 0) && (errno != EINTR)){LOG(FATAL, "epoll_wait failed");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 新连接到来if (sockfd == listenfd){int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);// 注册客户连接文件描述符,设置为只提醒一次addfd(epollfd, connfd, true);std::string client_ip = inet_ntoa(client_address.sin_addr);int client_port = ntohs(client_address.sin_port);std::string user = client_ip + std::to_string(client_port);if (users[connfd] == "") // 这代表客户首次到来,我们需要告诉其他人,这个客户上线了{// 先将该用户存储在在线用户表中users[connfd] = user;// 新启一个线程去发送上线通知user_info *p_user_info = new user_info(connfd, user);pthread_t tid;pthread_create(&tid, NULL, handler_online, (void *)p_user_info);// 分离该线程pthread_detach(tid);}}// 新数据到来或者对端关闭连接else if (events[i].events & EPOLLIN){user_info *p_user_info = new user_info(sockfd, users[sockfd], epollfd,1);pthread_t tid;pthread_create(&tid, NULL, handler, (void *)p_user_info);// 分离该线程pthread_detach(tid);}else{}}}close(listenfd);return 0;
}

        threadpool.hpp

#pragma once
#include "locker.hpp"
#include "LOG.hpp"template <typename T>
class threadpool
{
public:threadpool(int thread_number = 8, int max_requests = 10000);~threadpool();bool append(T request);private:static void *worker(void *arg);void run();private:int m_thread_number;        /* 线程池中线程的数量 */int m_max_requests;         /* 任务队列的大小 */pthread_t *m_threads;       /* 描述线程池的数组,其大小为m_thread_number */// 线程池内部维护请求队列,意味着线程池要提供append接口// 请求队列或者说任务队列,本身属于临界资源,需要加锁访问// 请求队列中存储的是指向user_info在堆区位置的指针std::list<T> m_workqueue; /* 任务队列 */locker m_queuelocker;       /* 保护请求队列的互斥锁 */sem m_queuestat;            /* 是否有任务需要处理 */bool m_stop;                /* 是否结束线程 */
};// 构造函数
template <typename T>
threadpool<T>::threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL)
{if ((thread_number <= 0) || (max_requests <= 0)){LOG(FATAL,"线程池创建时参数有误");}m_threads = new pthread_t[m_thread_number];if (!m_threads){LOG(ERROR,"线程标识符数组创建失败");}for (int i = 0; i < thread_number; ++i){if (pthread_create(m_threads + i, NULL, worker, this) != 0){delete[] m_threads;LOG(FATAL,"线程创建失败");}if (pthread_detach(m_threads[i])){delete[] m_threads;LOG(FATAL,"线程分离失败");}}std::cerr << "***********************************全部线程创建成功***********************************" << std::endl;
}template <typename T>
threadpool<T>::~threadpool()
{delete[] m_threads;m_stop = true;LOG(INFO,"线程池已析构");
}template <typename T>
bool threadpool<T>::append(T request)
{m_queuelocker.lock();if (m_workqueue.size() > m_max_requests){m_queuelocker.unlock();return false;}m_workqueue.push_back(request);m_queuelocker.unlock();m_queuestat.post();//增加信号量return true;
}template <typename T>
void *threadpool<T>::worker(void *arg)
{threadpool *pool = (threadpool *)arg;pool->run();return pool;
}template <typename T>
void threadpool<T>::run()
{while (!m_stop){// 申请信号量LOG(INFO,"有个线程在申请信号量");m_queuestat.wait();LOG(INFO,"有个线程申请信号量成功");m_queuelocker.lock();// 请求队列为空if (m_workqueue.empty()){m_queuelocker.unlock();continue;}// 从任务队列取走任务T request = m_workqueue.front();m_workqueue.pop_front();LOG(INFO,"有个线程取走任务");m_queuelocker.unlock();if (!request){continue;}// 执行任务LOG(INFO,"有个线程开始执行任务");handler(request);LOG(INFO,"有个线程执行任务结束");}
}

        我没发出来的代码都是跟之前发生时没有区别,大家直接复制上文代码即可。

总结

  1. 对于聊天室服务器重在关注epoll机制,ET + EPOLLONESHOT模式以及如何配合线程池
  2. 大家要想把上面写的代码全都搞明白,最好先测试运行,然后自己逐行分析,然后自己重写一遍。
  3. 对于聊天室服务器我没有参考其他人的写法,所以说基于我没有相关从业经验,肯定写的不全面。正如上面说的大家关注重点部分,当做学习使用即可。
  4. 对于http服务器,我参考了linux高性能服务器编程本书以及网上的某些项目,但是我发现了这些项目存在的很多小问题或者错误或者就是逻辑相对混乱或者就是效率较低。我从零重写了上述http各种版本的服务器,在我们写的代码中,主逻辑几乎是线性的也更加清晰。但是大家想要看懂依旧并不容易,如果大家逐行分析,并且再次从零构建,大家会遇到很多值得思考的问题。
http://www.dtcms.com/a/263813.html

相关文章:

  • 青否数字人直播再创新纪录!“人工智能+消费”开新篇?zhibo175
  • ABB CH-3185 3 bhl 000986 p 1006 ab ability 800 xa自动化系统
  • 【V6.0 - 听觉篇】当AI学会“听”:用声音特征捕捉视频的“情绪爽点”
  • 【开源项目】一款真正可修改视频MD5工具视频质量不损失
  • 【第二章:机器学习与神经网络概述】04.回归算法理论与实践 -(3)决策树回归模型(Decision Tree Regression)
  • UE5.6 官方文档笔记 [1]——虚幻编辑器界面
  • Python 单例模式与魔法方法:深度解析与实践应用
  • MySQL允许root用户远程连接
  • PDFBox + Tess4J 从PDF中提取图片OCR识别文字
  • 探秘阿里云Alibaba Cloud Linux:云时代的操作系统新宠
  • C语言学习笔记:深入解析结构体数组(附代码实践)
  • Qt QTableWidget多行多列复制粘贴
  • Android 网络全栈攻略(四)—— TCPIP 协议族与 HTTPS 协议
  • 安全左移(Shift Left Security):软件安全的演进之路
  • Spring Boot 2 多模块项目中配置文件的加载顺序
  • 智能交通信号灯
  • Django打造智能Web机器人控制平台
  • HarmonyOS应用开发高级认证知识点梳理 (三)状态管理V2装饰器核心规则
  • android车载开发之HVAC
  • 笔记本电脑怎样投屏到客厅的大电视?怎样避免将电脑全部画面都投出去?
  • 【蓝牙】Linux Qt4查看已经配对的蓝牙信息
  • 05【C++ 入门基础】内联、auto、指针空值
  • 算法-每日一题(DAY12)最长和谐子序列
  • 为Mkdocs网站添加Google广告
  • CRMEB开源商城系统Windows+IIS环境安装配置详解
  • word中一行未满但是后面有空白行
  • 每日一练:找到初始输入字符串 I
  • AbMole| H₂DCFDA(M9096;活性氧(ROS)探针)
  • MySQL索引深度解析:B+树、B树、哈希索引怎么选?
  • 凸包进阶旋转卡壳(模板题目集)