目录
3.2.5. HttpServer.cc (看一看 Http 请求格式)
3.2.6. HttpServer.cc (添加 HTTP 响应数据)
3.3.2. HttpServer.cc (获得HTTP请求的资源路径)
3.3.3. HttpServer.cc (处理资源路径,包含默认首页)
1. HTTP协议
2. 认识URL
2.1. URL中的四个主要字段
2.2. URL Encode && URL Decode
3. HTTP 协议格式
3.1. 快速构建 HTTP 请求和响应的报文格式
3.1.1. HTTP 请求格式
3.1.2. HTTP 响应格式
3.1.3. 关于 HTTP 请求 && 响应的宏观理解
3.2. 实现一个 HTTP demo
3.2.1. Sock.hpp
#ifndef __SOCK_HPP_
#define __SOCK_HPP_
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
namespace Xq
{
class Sock
{
public:
Sock() :_sock(-1) {}
// 创建套接字
void Socket(void)
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock == -1)
{
LogMessage(FATAL, "%s,%d\n", strerror(errno));
exit(1);
}
LogMessage(DEBUG, "listen sock: %d create success\n", _sock);
}
// 将该套接字与传入的地址信息绑定到一起
void Bind(const std::string& ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
int ret = bind(_sock, reinterpret_cast<const struct sockaddr*>(&addr), sizeof(addr));
if(ret == -1)
{
LogMessage(FATAL, "bind error\n");
exit(2);
}
}
// 封装listen 将该套接字设置为监听状态
void Listen(void)
{
int ret = listen(_sock, 10);
if(ret == -1)
{
LogMessage(FATAL, "listen error\n");
exit(3);
}
}
//封装accept
// 如果想获得客户端地址信息
// 那么我们可以用输出型参数
// 同时我们需要将服务套接字返回给上层
int Accept(std::string& client_ip, uint16_t* port)
{
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof client_addr;
bzero(&client_addr, addrlen);
int server_sock = accept(_sock, \
reinterpret_cast<struct sockaddr*>(&client_addr), &addrlen);
if(server_sock == -1)
{
LogMessage(FATAL, "%s,%d\n", strerror(errno));
return -1;
}
// 将网络字节序的整数转为主机序列的字符串
client_ip = inet_ntoa(client_addr.sin_addr);
// 网络字节序 -> 主机字节序
*port = ntohs(client_addr.sin_port);
// 返回服务套接字
return server_sock;
}
// 向特定服务端发起连接
void Connect(struct sockaddr_in* addr, const socklen_t* addrlen)
{
int ret = connect(_sock,\
reinterpret_cast<struct sockaddr*>(addr), \
*addrlen);
if(ret == -1)
{
LogMessage(FATAL, "%s%d\n", strerror(errno));
}
}
// 服务端结束时, 释放监听套接字
~Sock(void)
{
if(_sock != -1)
{
close(_sock);
LogMessage(DEBUG, "close listen sock: %d\n", _sock);
}
}
public:
int _sock; // 套接字
};
}
#endif
3.2.2. Log.hpp
#pragma once
#include "Date.hpp"
#include <iostream>
#include <map>
#include <string>
#include <cstdarg>
#define LOG_SIZE 1024
// 日志等级
enum Level
{
DEBUG, // DEBUG信息
NORMAL, // 正常
WARNING, // 警告
ERROR, // 错误
FATAL // 致命
};
const char* pathname = "./log.txt";
void LogMessage(int level, const char* format, ...)
{
// 如果想打印DUBUG信息, 那么需要定义DUBUG_SHOW (命令行定义, -D)
#ifndef DEBUG_SHOW
if(level == DEBUG)
return ;
#endif
std::map<int, std::string> level_map;
level_map[0] = "DEBUG";
level_map[1] = "NORAML";
level_map[2] = "WARNING";
level_map[3] = "ERROR";
level_map[4] = "FATAL";
std::string info;
va_list ap;
va_start(ap, format);
char stdbuffer[LOG_SIZE] = {0}; // 标准部分 (日志等级、日期、时间)
snprintf(stdbuffer, LOG_SIZE, "[%s],[%s],[%s] ", level_map[level].c_str(), Xq::Date().get_date().c_str(), Xq::Time().get_time().c_str());
info += stdbuffer;
char logbuffer[LOG_SIZE] = {0}; // 用户自定义部分
vsnprintf(logbuffer, LOG_SIZE, format, ap);
info += logbuffer;
FILE* fp = fopen(pathname, "a");
fprintf(fp, "%s", info.c_str());
fclose(fp);
va_end(ap);
}
3.2.3. Usage.hpp
#pragma once
#include <iostream>
void Usage(const std::string &name)
{
std::cout << "Usage:\n " << name << " port" << std::endl;
}
3.2.4. HttpServer.hpp
#ifndef _HTTPSERVER_HPP_
#define _HTTPSERVER_HPP_
#include <functional>
#include "Sock.hpp"
#include "Log.hpp"
#include "Date.hpp"
#include "Usage.hpp"
using func_t = std::function<void(int)>;
namespace Xq
{
class HttpServer
{
public:
// 初始化服务器
HttpServer(uint16_t port)
{
// 创建套接字
_sock.Socket();
// 绑定套接字
_sock.Bind("", port);
// 将该套接字设置为监听状态
_sock.Listen();
}
// 服务端需要绑定的服务
void BindHandle(func_t func)
{
_func = func;
}
// 子进程执行的服务 (该服务就是外部绑定的服务)
void ExecuteHandle(int server_sock)
{
_func(server_sock);
}
// 启动服务器
void start(void)
{
while(true)
{
std::string client_ip;
uint16_t client_port = 0;
int server_sock = _sock.Accept(client_ip, &client_port);
// 让子进程执行该客户端请求
if(fork() == 0)
{
// 子进程关闭监听套接字
close(_sock._sock);
// 处理该客户端请求
ExecuteHandle(server_sock);
// 执行完, 关闭该服务套接字
close(server_sock);
// 子进程退出
exit(0);
}
// 父进程不需要该服务套接字, 关闭掉
close(server_sock);
// 同时, 我们显式忽略了SIGCHLD信号
// 避免僵尸问题的产生
// 父进程继续去获取客户端连接
}
}
private:
Sock _sock;
func_t _func;
};
}
#endif
3.2.5. HttpServer.cc (看一看 Http 请求格式)
#include <memory>
#include <signal.h>
#include <fstream>
#include "HttpServer.hpp"
#include "Tool.hpp"
#define BUFFER_SIZE 10240
void HttpServerHandle(int sock)
{
// 因为http底层是tcp协议
// 而tcp协议是面向字节流的
// 因此可以使用 recv 接口
char buffer[BUFFER_SIZE] = {0};
// 读取客户端请求
ssize_t real_size = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if(real_size > 0)
{
buffer[real_size] = 0;
// 打印一下 HTTP 请求
std::cout << buffer << "------------------" << std::endl;
}
}
int main(int arg, char** argv)
{
if(arg != 2)
{
Usage(argv[0]);
exit(1);
}
// 显示忽略 SIGCHLD 信号, 避免僵尸问题
signal(SIGCHLD, SIG_IGN);
std::unique_ptr<Xq::HttpServer> server(new Xq::HttpServer(atoi(argv[1])));
// Bind 处理客户端请求的方法
server->BindHandle(HttpServerHandle);
// 启动服务器
server->start();
return 0;
}
3.2.6. HttpServer.cc (添加 HTTP 响应数据)
void HttpServerHandle(int sock)
{
// 因为http底层是tcp协议
// 而tcp协议是面向字节流的
// 因此可以使用 recv 接口
char buffer[BUFFER_SIZE] = {0};
ssize_t real_size = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if(real_size > 0)
{
buffer[real_size] = 0;
// 打印一下 HTTP 请求
std::cout << buffer << "------------------" << std::endl;
}
// 状态行(服务器端HTTP版本 状态码 状态码描述符\r\n)
std::string HttpResponse = "HTTP/1.1 200 OK\r\n";
// 暂时不添加响应报头
// 添加空行
HttpResponse += "\r\n";
// 响应正文 (简单的构建一个html)
HttpResponse += "<html><h3> hello world! </h3></html>";
// 发送给客户端
send(sock, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int arg, char** argv)
{
// 省略
}
3.3. 通过该 demo 逐步分析过程,理解细节
3.3.1. Tool.hpp
#ifndef _UTIL_HPP_
#define _UTIL_HPP_
#include <iostream>
#include <string>
#include <vector>
namespace Xq
{
class Tool
{
public:
static void CutString(const std::string& src, const std::string& sep, std::vector<std::string>* out)
{
size_t start = 0;
size_t pos = 0;
while(pos != std::string::npos && start < src.size() - 1)
{
pos = src.find(sep, start);
if(pos != std::string::npos)
{
out->push_back(src.substr(start, pos - start));
start = pos;
start += sep.size();
}
}
if (start < src.size())
{
out->push_back(src.substr(start));
}
}
};
}
#endif
3.3.2. HttpServer.cc (获得HTTP请求的资源路径)
#include <memory>
#include <signal.h>
#include <fstream>
#include "HttpServer.hpp"
#include "Tool.hpp"
#define BUFFER_SIZE 10240
// 服务器资源的起始目录
#define START_PATH "./wwwroot"
void HttpServerHandle(int sock)
{
// 因为http底层是tcp协议
// 而tcp协议是面向字节流的
// 因此可以使用 recv 接口
char buffer[BUFFER_SIZE] = {0};
ssize_t real_size = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if(real_size > 0)
{
buffer[real_size] = 0;
// 打印一下 HTTP 请求
std::cout << buffer << "------------------" << std::endl;
}
// 这个vector用于存储HTTP请求中的每一行信息
std::vector<std::string> v_line;
Xq::Tool::CutString(buffer, "\r\n", &v_line);
// 因为我们需要 HTTP 请求中的 资源路径
// 故我们实际上只需要 HTTP 请求的第一行信息 (v_line[0])
// 即请求行, 而请求行中的第二个字段就是资源路径
// 这个vector用于存储请求行中的每一个字段
std::vector<std::string> v_block;
Xq::Tool::CutString(v_line[0], " ", &v_block);
for(const auto& vit : v_block)
{
std::cout << vit << std::endl;
}
// 而此时的v_block[1]就是HTTP请求中的资源路径
std::string resource_path = v_block[1];
std::cout << "resource_path: " << resource_path << std::endl;
// 状态行(服务器端HTTP版本 状态码 状态码描述符\r\n)
std::string HttpResponse = "HTTP/1.1 200 OK\r\n";
// 暂时不添加响应报头
// 添加空行
HttpResponse += "\r\n";
// 响应正文 (简单的构建一个html)
HttpResponse += "<html><h3> hello world! </h3></html>";
// 发送给客户端
send(sock, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int arg, char** argv)
{
if(arg != 2)
{
Usage(argv[0]);
exit(1);
}
// 显示忽略 SIGCHLD 信号, 避免僵尸问题
signal(SIGCHLD, SIG_IGN);
std::unique_ptr<Xq::HttpServer> server(new Xq::HttpServer(atoi(argv[1])));
// Bind 处理客户端请求的方法
server->BindHandle(HttpServerHandle);
// 启动服务器
server->start();
return 0;
}
3.3.3. HttpServer.cc (处理资源路径,包含默认首页)
#include <memory>
#include <signal.h>
#include <fstream>
#include "HttpServer.hpp"
#include "Tool.hpp"
#define BUFFER_SIZE 10240
// 服务器资源的起始目录
#define START_PATH "./wwwroot"
void HttpServerHandle(int sock)
{
// 因为http底层是tcp协议
// 而tcp协议是面向字节流的
// 因此可以使用 recv 接口
char buffer[BUFFER_SIZE] = {0};
ssize_t real_size = recv(sock, buffer, BUFFER_SIZE - 1, 0);
if(real_size > 0)
{
buffer[real_size] = 0;
// 打印一下 HTTP 请求
std::cout << buffer << "------------------" << std::endl;
}
// 这个vector用于存储HTTP请求中的每一行信息
std::vector<std::string> v_line;
Xq::Tool::CutString(buffer, "\r\n", &v_line);
// 因为我们需要 HTTP 请求中的 资源路径
// 故我们实际上只需要HTTP 请求的第一行信息
// 即请求行, 而请求行中的第二个字段就是资源路径
// 这个vector用于存储请求行中的每一个字段
std::vector<std::string> v_block;
Xq::Tool::CutString(v_line[0], " ", &v_block);
for(const auto& vit : v_block)
{
std::cout << vit << std::endl;
}
// 而此时的v_block[1]就是HTTP请求中的资源路径
std::string resource_path = v_block[1];
std::cout << "resource_path: " << resource_path << std::endl;
// 服务器要访问的目标路径
std::string Target_path;
Target_path = START_PATH;
Target_path += resource_path;
// 如果该路径是Web根目录 ("/"), 那么返回默认首页
if(resource_path == "/")
{
Target_path += "index.html";
}
// std::cout << "Target_path: " << Target_path << std::endl;
// 目标路径已经准备就绪, 现在就需要打开该文件
std::ifstream in(Target_path.c_str());
// HTTP 响应
std::string HttpResponse;
// 如果该文件不存在, 那么HTTP响应中的状态码就是404
if(!in.is_open())
{
// 状态行(服务器端HTTP版本 状态码 状态码描述符\r\n)
HttpResponse = "HTTP/1.1 404 Not Found\r\n";
// 空行
HttpResponse += "\r\n";
// 错误信息文件的路径
std::string error_path = START_PATH;
std::string error_info;
error_path += "/error.html";
// 将错误信息添加到HTTP响应正文中
std::ifstream error(error_path);
if(error.is_open())
{
while(getline(error, error_info))
{
HttpResponse += error_info;
}
}
error.close();
// done
}
else
{
// 状态行
HttpResponse = "HTTP/1.1 200 OK\r\n";
// 空行
HttpResponse += "\r\n";
// 有效载荷
std::string content;
while(std::getline(in, content))
{
HttpResponse += content;
}
// done
}
// 读取文件完毕后, 关闭文件
in.close();
// 发送给客户端
send(sock, HttpResponse.c_str(), HttpResponse.size(), 0);
}
int main(int arg, char** argv)
{
// 忽略...
}
3.3.4. index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title> 咸蛋超人的网站 </title>
</head>
<body>
<html><p> ------------------ 北国风光, 千里冰封, 万里雪飘。 --------------------- </p><html>
<html><p> ------------------ 望长城内外, 惟余莽莽; 大河上下, 顿失滔滔。 --------------------- </p><html>
<html><p> ------------------ 山舞银蛇, 原驰蜡象, 欲与天公试比高。 --------------------- </p><html>
<html><p> ------------------ 须晴日, 看红装素裹, 分外妖娆。 --------------------- </p><html>
<html><p> ------------------ 江山如此多娇, 引无数英雄竞折腰。 --------------------- </p><html>
<html><p> ------------------ 惜秦皇汉武, 略输文采; 唐宗宋祖, 稍逊风骚。 --------------------- </p><html>
<html><p> ------------------ 一代天骄, 成吉思汗, 只识弯弓射大雕。 --------------------- </p><html>
<html><p> ------------------ 俱往矣, 数风流人物, 还看今朝。 --------------------- </p><html>
</body>
</html>
3.3.5. error.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title> 咸蛋超人的网站 </title>
</head>
<body>
<h1> 404 --- Not Found </h1>
</body>
</html>