【网络】3.HTTP(讲解HTTP协议和写HTTP服务)
目录
- 1 认识URL
- 1.1 URI的格式
- 2 HTTP协议
- 2.1 请求报文
- 2.2 响应报文
- 3 模拟HTTP
- 3.1 Socket.hpp
- 3.2 HttpServer.hpp
- 3.2.1 start()
- 3.2.2 ThreadRun()
- 3.2.3 HandlerHttp()
- 总结
1 认识URL
什么是URI?
URI 是 Uniform Resource Identifier的缩写,URI就是由某个协议方案表示的资源的定位标识符。采用HTTP协议时,协议方案就是http。除此之外,还有ftp、mailto、telnet等。
什么是URL?
URI用字符串标识某一互联网资源,而URL表示资源的地点(互联网上所处的位置)。可见URL是URI的子集。
1.1 URI的格式
登录信息认证
指定用户名和密码作为从五毒气短获取资源时必要的登录信息(身份认证)。此项是可选项。
服务器地址
使用绝对URI必须指定带访问的服务器地址。地址可以是DNS,或者是IPV4,IPV6格式。
服务器端口号
指定服务器连接的网络端口号,如果用户省略则自动使用默认端口号。
带层次的文件路径
指定服务器上的文件路径来定位特指的资源。这与UNIX系统的文件目录结构类似。如果不写,默认是首页,一般是index.html
查询字符串
针对已指定的文件路径内的资源,可以使用查询字符串兑换如任意参数。此项可选。
片段标识符
使用片段标识符通常可标记处已获取资源的子资源(文档内的某个位置)。但是在RFC中没有明确规定其使用方法。该项为可选项
2 HTTP协议
HTTP协议用于客户端和服务器之间的通信。
客户端
请求访问文本或图像等资源的一端称为客户端。
服务端
提供资源响应的一端称为服务端。
2.1 请求报文
下面是HTTP请求的格式:
真实的请求如下:
对比一下格式与真实情况:
2.2 响应报文
下面是HTTP响应的格式:
真实响应如下:
下面是对比:
3 模拟HTTP
下面的代码是socket套接字实现,主要为了http通信提供网络接口。
3.1 Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
const int backlog = 10;
class Sock
{
public:
Sock()
{}
~Sock()
{}
public:
void Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
std::cout << "sock error " << strerror(errno) << errno << std::endl;
exit(SocketErr);
}
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr =INADDR_ANY;
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cout << "bind error " << strerror(errno) << errno << std::endl;
exit(BindErr);
}
}
void Listen()
{
if (listen(_sockfd, backlog) < 0)
{
std::cout << "listen error " << strerror(errno) << errno << std::endl;
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(_sockfd, (struct sockaddr*)& peer, &len);
if (newfd < 0)
{
std::cout << "accept error " << strerror(errno) << errno << std::endl;
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connnect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family =AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(_sockfd, (struct sockaddr*)&peer, sizeof(peer));
if (n == -1)
{
std::cout << "connect to" << ip << " : " << port << " error " << std::endl;
return false;
}
return true;
}
void Close()
{
close(_sockfd);
}
int Fd()
{
return _sockfd;
}
private:
//文件描述符
int _sockfd;
};
3.2 HttpServer.hpp
class HttpServer
{
public:
HttpServer(int port = defaultport)
:_port(defaultport)
{}
private:
Sock _listensock;
uint16_t _port;
};
模拟的是使用HTTP协议的过程,客户端需要向浏览器访问,
输入的内容为ip:port。 例如:http://124.223.90.51:8085
因此,需要创建一个函数来启动HTTP服务器:
3.2.1 start()
bool Start()
{
//创建套接字
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
for (;;)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this);
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
1.创建监听套接字
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
_listensock.Socket()
:创建 TCP 套接字(socket())。_listensock.Bind(_port)
:绑定端口 _port(bind())。_listensock.Listen()
:监听端口,等待客户端连接(listen())。
作用:服务器启动,监听 _port 端口,准备接受 HTTP 请求。.
2.接受客户端连接
int sockfd = _listensock.Accept(&clientip, &clientport);
- Accept():阻塞等待客户端连接,成功返回新连接的套接字 sockfd,并获取客户端的 IP 和端口。
- 客户端访问
http://服务器IP:端口
,就会触发 Accept()。
作用:获取客户端连接,并准备创建线程处理。.
3.创建新线程处理客户端请求
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this);
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td);
- 创建 ThreadData 结构体,存储 sockfd 和 this(服务器指针)。
pthread_create()
创建新线程ThreadRun(td)
,让线程处理 sockfd 连接。
作用:服务器为每个客户端请求创建一个新线程并行处理,提高并发能力。.
3.2.2 ThreadRun()
static void* ThreadRun(void* args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
// char buffer[10240];
// //ssize_t n = read(?; buffer, sizeof(buffer - 1)); 可以使用read
// //也可以使用recv
// ssize_t n = recv(td->sockfd, buffer, sizeof(buffer - 1), 0);
// if (n > 0)
// {
// buffer[n] = 0;
// std::cout << buffer;
// }
HandlerHttp(td->sockfd, td->httpsvr);
delete td;
return nullptr;
}
-
这段代码是 HttpServer 服务器每个新线程的执行函数 ThreadRun(),用于处理客户端的 HTTP 请求。
-
当客户端连接服务器时,服务器会创建一个新线程执行 ThreadRun(),读取 HTTP 请求并进行处理。
pthread_detach(pthread_self()) 作用:
- 让线程在完成后自动释放资源,不需要 pthread_join() 手动回收。
- 避免僵尸线程(已结束但未回收的线程)。
- 适用于短生命周期的线程,如 HTTP 服务器的请求处理线程。
为什么要线程分离 ?
- HTTP 请求通常是短暂的,处理完成后线程就不需要存在了。
- 如果不分离,主线程需要 pthread_join() 逐个回收,会浪费资源。
- 让线程自动销毁,提高服务器并发能力。
为什么ThreadRun被设置为static?
- pthread_create() 需要一个 C 语言风格的函数指针
pthread_create
的函数签名如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
第三个参数
start_routine
是一个函数指针,必须符合 void* ()(void) 这种标准格式。普通成员函数有一个隐藏的
this
指针,无法直接传递给 pthread_create()。
所以,必须用:
普通的 C 函数(static 成员函数), 或全局函数。
3.2.3 HandlerHttp()
static void HandlerHttp(int sockfd, HttpServer *httpsvr)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); //在这里,不能保证读到了完整的http请求
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl; //假设我们读到的是一个完整的http请求
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
std::string text;
bool ok = true;
text = ReadHtmlContent(req.file_path); //读取客户端请求的文件 --> "./index.html"
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = ReadHtmlContent(err_html);
}
//返回响应的过程
std::string response_lines;
if (ok)
{
response_lines = "HTTP/1.0 200 OK\r\n";
}
else
{
response_lines = "HTTP/1.0 404 Not Found\r\n";
}
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
response_header += "\r\n";
response_header += "Content-Type: text/html\r\n";
response_header += "\r\n";
response_header += "Set-Cookie : name=haha&&passds=12345";
response_header += "\r\n";
std::string blank_lines = "\r\n";
std::string response = response_lines;
response += response_header;
response += blank_lines;
response += text;
//将内容发送给客户端 -- 响应
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
HandlerHttp函数的作用是处理HTTP请求并返回响应
HandlerHttp() 的作用是 处理 HTTP 请求并返回响应,它是 HTTP 服务器的核心逻辑,负责:
- 读取客户端请求数据
- 解析 HTTP 请求
- 获取请求的资源(如 HTML 页面)
- 构造 HTTP 响应
- 将 HTTP 响应返回给客户端
- 关闭连接
1. 如何解析HTTP请求?
根据前文所说,HTTP请求格式如下:
可以看到,HTTP请求是一个已经序列化的报文,因此,我们如果想要知道具体的Method, URI,Http_Version就必须进行反序列化。
void Deserialize(std::string req)
{
while(true)
{
ssize_t pos = req.find(seq); // 找到 "\r\n" 的位置
if (pos == std::string::npos) // 没有找到 "\r\n",说明 HTTP 头部已经解析完毕
break;
std::string temp = req.substr(0, pos); // 提取一行 HTTP 头部信息
if (temp.empty()) // 如果这一行是空的,说明遇到了 HTTP 头部和正文的分隔行
break;
req_header.push_back(temp); // 将解析出的头部信息存入 `req_header`
req.erase(0, pos + seq.size()); // 删除已经解析的部分,继续处理剩下的内容
}
// req 现在去掉了所有头部,剩下的就是 HTTP 请求的正文
text = req;
}
假设客户端发送了如下 HTTP 请求:
那么req_header
里的内容:
经过反序列化之后,req_header的内容如下:
req_header[0] = “GET /index.html HTTP/1.1”
req_header[1] = “Host: 124.223.90.51:8085”
…
text = “name=hello&password=1234”;
2. 如何获取Method/URI/Http_Version?
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
① std::stringstream ss(req_header[0])
std::stringstream
是 C++ 的 字符串流(string stream),类似std::cin
,可以像读取标准输入一样 按空格分割字符串。- 这里
req_header[0]
是"GET /index.html HTTP/1.1"
,将其存>入 ss 后,ss 变成了一个可以逐个提取单词的输入流。
② ss >> method >> url >> http_version;
ss >> method
→ 提取 “GET”,存入 method。ss >> url
→ 提取 “/index.html”,存入 url。ss >> http_version
→ 提取 “HTTP/1.1”,存入 http_version。
最终:
3. 如何根据客户端的输入确定要访问哪个文件?
首先定义:
const std::string wwwroot = "./wwwroot";
./wwwroot路径是HTTP的根目录,所有的文件都放在根目录下,如果客户端不指定访问具体的哪个文件,那么默认访问根目录。
file_path = wwwroot;
if (url == "/" || url == "/index.html") //根目录
{
file_path += "/";
file_path += homepage;
}
else
{
file_path += url;
}
通过上面的代码确定file_path的值,也就可以精准访问到具体的文件。
4. 如何读取html文件的内容?
之前确定了file_path的值,也就是确定了读取的具体的文件,接下来就是进入到文件内部读取文件的内容。
std::ifstream in(htmlpath, std::ios::binary);
- std::ifstream 打开文件,std::ios::binary 以 二进制模式 读取(防止换行符转换)。
- 如果 htmlpath 指定的文件 不存在或打不开,流对象 in 不会打开。
in.seekg(0, std::ios_base::end);
auto len = in.tellg();
in.seekg(0, std::ios_base::beg);
seekg(0, std::ios_base::end);
→ 将文件指针移动到文件末尾,这样 tellg() 可以获取 文件大小。len = in.tellg();
→ 获取文件的长度(字节数)。seekg(0, std::ios_base::beg);
→ 将文件指针移动回文件开头,准备读取内容。
std::string content;
content.resize(len);
- 创建字符串 content,并分配 len 个字符的空间,以存放 HTML 文件的内容。
in.read((char*)content.c_str(), content.size());
- 读取文件内容到 content:
in.read()
读取 content.size() 个字符,并存入 content 中。content.c_str()
获取 std::string 的底层 C 风格字符数组的指针,保证数据存储正确
5. 为什么要采用二进制的读法?
因为读取的不一定是html文件,有可能是图片,视频。如果是普通的read方法,可能无法读取图片资源。
6. 如何返回HTTP响应?
1. 根据读到的内容判断状态码:
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = ReadHtmlContent(err_html);
}
//返回响应的过程
std::string response_lines;
if (ok)
{
response_lines = "HTTP/1.0 200 OK\r\n";
}
else
{
response_lines = "HTTP/1.0 404 Not Found\r\n";
}
2. 添加Key:Value部分:
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
response_header += "\r\n";
response_header += "Content-Type: text/html\r\n";
response_header += "\r\n";
response_header += "Set-Cookie : name=haha&&passds=12345";
response_header += "\r\n";
3.添加空行部分
std::string blank_lines = "\r\n";
4.整理所有的内容返回给客户端
std::string response = response_lines;
response += response_header;
response += blank_lines;
response += text;
//将内容发送给客户端 -- 响应
send(sockfd, response.c_str(), response.size(), 0);
总结
HttpServer.hpp的内容如下:
#pragma once
#include <iostream>
#include <pthread.h>
#include "Socket.hpp"
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
static const int defaultport = 8085;
const std::string seq = "\r\n";
const std::string wwwroot = "./wwwroot";
const std::string homepage = "index.html";
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd, HttpServer *ts)
:sockfd(fd)
,httpsvr(ts)
{}
public:
int sockfd;
HttpServer *httpsvr;
};
class HttpRequest
{
public:
//反序列化 -- 将从客户端读到的http请求push_back进req_header中
void Deserialize(std::string req)
{
while(true)
{
ssize_t pos = req.find(seq);
if (pos == std::string::npos)
break;
std::string temp = req.substr(0, pos);
if (temp.empty())
break;
req_header.push_back(temp);
req.erase(0, pos + seq.size());
}
//req去掉前面的内容之后,剩下的全是文本
text = req;
}
void DebugPrint()
{
for (auto &line : req_header)
{
std::cout << "-------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method : " << method << std::endl;
std::cout << "url : " << url << std::endl;
std::cout << "http version : " << http_version << std::endl;
std::cout << "file path : " << file_path << std::endl;
std::cout << "text : " << text << std::endl;
}
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version; //用stringstream将method等直接分割了
file_path = wwwroot;
if (url == "/" || url == "/index.html") //根目录
{
file_path += "/";
file_path += homepage;
}
else
{
file_path += url;
}
// auto pos = file_path.rfind(".");
// if (pos == std::string::npos)
}
public:
std::vector<std::string> req_header; //请求
std::string text;
//解析之后的结果 --> 这是http的请求报文的格式
std::string method;
std::string url;
std::string http_version;
std::string file_path;
};
class HttpServer
{
public:
HttpServer(int port = defaultport)
:_port(defaultport)
{}
bool Start()
{
//创建套接字
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
for (;;)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this);
td->sockfd = sockfd;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
static void* ThreadRun(void* args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
// char buffer[10240];
// //ssize_t n = read(?; buffer, sizeof(buffer - 1)); 可以使用read
// //也可以使用recv
// ssize_t n = recv(td->sockfd, buffer, sizeof(buffer - 1), 0);
// if (n > 0)
// {
// buffer[n] = 0;
// std::cout << buffer;
// }
HandlerHttp(td->sockfd, td->httpsvr);
delete td;
return nullptr;
}
//固定版本
// static void HanderHttp(int sockfd, HttpServer *httpsvr)
// {
// char buffer[10240];
// ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); //在这里,不能保证读到了完整的http请求
// if (n > 0)
// {
// buffer[n] = 0;
// std::cout << buffer;
// //返回相应的过程
// std::string text = "mayue is a pig! xixi~";
// std::string response_lines = "HTTP/1.0 200 OK\r\n";
// std::string response_header = "Content-Length: ";
// response_header += std::to_string(text.size());
// response_header += "\r\n";
// std::string blank_lines = "\r\n";
// std::string response = response_lines;
// response += response_header;
// response += blank_lines;
// response += text;
// //将内容发送给发送方 -- 响应
// send(sockfd, response.c_str(), response.size(), 0);
// }
// close(sockfd);
// }
//读取文件内容
static std::string ReadHtmlContent(const std::string &htmlpath)
{
std::ifstream in(htmlpath, std::ios::binary);
if (!in.is_open())
return "";
in.seekg(0, std::ios_base::end);
auto len = in.tellg();
in.seekg(0, std::ios_base::beg);
std::string content;
content.resize(len);
in.read((char*)content.c_str(), content.size());
in.close();
return content;
}
//显示不同的html,进行处理
static void HandlerHttp(int sockfd, HttpServer *httpsvr)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); //在这里,不能保证读到了完整的http请求
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl; //假设我们读到的是一个完整的http请求
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
std::string text;
bool ok = true;
text = ReadHtmlContent(req.file_path); //读取客户端请求的文件 --> "./index.html"
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = ReadHtmlContent(err_html);
}
//返回响应的过程
std::string response_lines;
if (ok)
{
response_lines = "HTTP/1.0 200 OK\r\n";
}
else
{
response_lines = "HTTP/1.0 404 Not Found\r\n";
}
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
response_header += "\r\n";
response_header += "Content-Type: text/html\r\n";
response_header += "\r\n";
response_header += "Set-Cookie : name=haha&&passds=12345";
response_header += "\r\n";
std::string blank_lines = "\r\n";
std::string response = response_lines;
response += response_header;
response += blank_lines;
response += text;
//将内容发送给客户端 -- 响应
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
~HttpServer()
{}
private:
Sock _listensock;
uint16_t _port;
};
HttpServer.cc
#include "Httpserver.hpp"
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//std::unique<HttpServer> svr(new HttpServer());
HttpServer *svr = new HttpServer();
svr->Start();
return 0;
}