在上一篇博客中,我们了解到TCP是面向字节流式的进行网络通信的,所以不具备消息边界的功能,所以我们要实现一个完整的网络通信,就必须设计应用层协议,那么要是我们每次都要像上一篇博客那样定义如此麻烦的协议,确实很棘手,因此为了方便,其实已经有大佬定义了一些现成的,非常好用的应用层协议,可以让我们直接使用,不再需要我们自己去定义了,比如HTTP(超文本传输协议就是其中之一)。
| 协议名称 | 协议全称 | 默认端口 | 传输层协议 | 说明 |
|---|---|---|---|---|
| HTTP | 超文本传输协议 | 80 | TCP | 网页访问(明文) |
| HTTPS | 安全超文本传输协议 | 443 | TCP | 加密网页 |
| FTP | 文件传输协议 | 21 / 20 | TCP | 文件上传下载 |
| TFTP | 简单文件传输协议 | 69 | UDP | 简单传输 |
| SMTP | 邮件发送协议 | 25 | TCP | 发送邮件 |
| POP3 | 邮件接收协议 | 110 | TCP | 下载邮件 |
| IMAP | 邮件访问协议 | 143 | TCP | 管理邮件 |
| DNS | 域名系统 | 53 | UDP/TCP | 域名解析 |
| DHCP | 动态主机配置协议 | 67 / 68 | UDP | 自动分配IP |
| Telnet | 远程登录协议 | 23 | TCP | 不安全远程登录 |
| SSH | 安全远程登录 | 22 | TCP | 加密远程连接 |
| SNMP | 网络管理协议 | 161 | UDP | 网络设备管理 |
| NTP | 网络时间协议 | 123 | UDP | 时间同步 |
这些就是常见的一些应用层协议。
认识URL
现在,大家有问题一般都是问豆包等AI软件,以前没有AI的时候,大家有问题都是问度娘,这个URL就是我们访问百度的域名


当我们在浏览器中通过域名访问网站时,本质上其实并不是直接访问这个字符串,而是需要先将这个域名解析为对应的 IP 地址,然后进行访问
具体流程是:
- 首先,客户端(浏览器或操作系统)会向本地 DNS 服务器发送解析请求,这个服务器提供者就是三大运营商。
- 如果本地 DNS 服务器没有缓存结果,它会向更高层级的 DNS 服务器发起查询:
- 根域名服务器
- 顶级域名服务器
- 权威域名服务器
- 最终找到该域名对应的 IP 地址,并将结果返回给客户端
拿到 IP 地址之后,客户端就可以通过:
👉 IP地址 + 端口号
但是又仔细的人就会看到上图中我们使用IP地址进行访问的时候,并没有看到端口号这个东西,难道只需要通过IP地址就可以访问到具体的网站了吗?其实并不是,其实是因为我们使用的是HTTPS协议,这个协议就代表着它的端口号就是443(这就像我们在生活中110就是报警电话,119就是火警电话一样),所以我们就不需要在显现的写出来,浏览器替我们隐式补全了端口号,而不是端口号不存在。
IP 地址负责定位主机,端口号负责定位主机上的具体服务,二者缺一不可
HTTP协议

这张图其实展示的是一次完整的 HTTP 通信过程中,请求报文和响应报文的整体结构。从客户端角度来看,当浏览器发起请求时,会先构造一段符合 HTTP 协议规范的文本数据发送给服务器,这段数据最开始是请求行 ,包含请求方法(如 GET、POST)、资源路径以及 HTTP 版本,用来明确"我要做什么、访问哪里、用什么协议版本"。紧接着是多行请求报头 ,每一行都是 key:value 的形式,用于携带额外信息,比如目标主机、数据类型、客户端信息等,这些内容全部以 \r\n 作为分隔。
在请求报头结束后,会有一个非常关键的空行,它的作用是明确告诉服务器:报头部分已经结束,接下来如果还有内容,就是请求的有效载荷。对于 GET 请求来说通常没有正文,而 POST 请求则会在这里携带数据,比如表单或 JSON,这部分就是图中标注的"请求正文"。
服务器接收到请求并处理后,会按照同样的协议格式返回响应。响应报文的开头是状态行 ,包含 HTTP 版本、状态码以及对应的描述信息,例如 200 OK 表示请求成功。随后是多行响应报头 ,同样以 key:value 的形式存在,用来说明返回数据的类型、长度以及服务器相关信息等。报头之后同样会有一个空行,用于分隔元数据和真正的数据内容,最后的响应正文才是客户端真正关心的部分,可能是 HTML 页面、JSON 数据,或者 JS、CSS 等资源。
从整体来看,这张图的核心就是在说明:HTTP 通信本质上是在 TCP 之上,按照固定格式传输的一段"结构化字符串",通过请求行/状态行、报头、空行和正文这几个部分进行组织,而 \r\n 则充当了天然的分隔符,使客户端和服务器能够准确地解析彼此发送的数据。

HTTP请求


- 首行:方法+url+版本
- Header:请求的属性,冒号分割的键值对;每一组属性之间用\r\n进行分割,遇到空行则表示Header部分结束
- Body:空行后面的内容就是Body,Body的内容可以是空字符串。
HTTP响应

- 首行:版本号+状态码+状态码解释
- Header:请求的属性,冒号分割的键值对;每一组属性之间用\r\n进行分割,遇到空行则表示Header部分结束
- Body:
- 空行后面的内容就是Body,Body的内容可以是空字符串。如果服务器返回了一个html页面, 那么html页面内容就是在 body中。
现在我们就来简单实现一个能被浏览器访问的 HTTP 服务器
HTTP服务器
一、实现效果
启动服务器后,在浏览器输入:
http://ip:port/
即可访问我们自己写的网页:
- 支持返回
index.html - 支持简单页面跳转
二、基本原理
HTTP 服务器本质还是一个 TCP 服务器,只不过多了一步:
👉 解析请求 + 返回符合 HTTP 协议的数据
整体流程:
socket → bind → listen → accept
↓
创建线程
↓
read → 解析 → write
三、核心代码思路
1️⃣ 读取浏览器请求
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
浏览器发来的请求大致是:
GET /index.html HTTP/1.1
Host: xxx

2️⃣ 解析请求行
std::stringstream ss(req_header[0]);
std::string method, url, version;
ss >> method >> url >> version;
我们只关心:
- 请求方法(GET)
- 访问路径(url)
3️⃣ 拼接文件路径
std::string path;
if (url == "/" || url == "/index.html")
path = "./wwwroot/index.html";
else
path = "./wwwroot" + url;
4️⃣ 读取网页内容
std::ifstream in(path);
while (std::getline(in, line))
{
content += line + "\n";
}
5️⃣ 构造 HTTP 响应
std::string response;
response = "HTTP/1.0 200 OK\r\n";
response += "Content-Length: " + std::to_string(content.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += content;
然后发送给浏览器:
write(sockfd, response.c_str(), response.size());
6️⃣ 简单处理 404
if (!in.is_open())
{
std::string body = "<h1>404 Not Found</h1>";
response = "HTTP/1.0 404 Not Found\r\n";
response += "Content-Length: " + std::to_string(body.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += body;
}
HTTP服务器完整代码

Socket.hpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <cstring>
enum Err
{
SocketErr = 1,
BindErr,
ListenErr
};
class Socket
{
public:
Socket()
{
}
void Init()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
std::cout << "socket fail!!!" << std::endl;
exit(SocketErr);
}
}
void Bind(uint16_t port, std::string ip)
{
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &server.sin_addr);
if (bind(sockfd_, (struct sockaddr *)&server, sizeof server) < 0)
{
std::cout << "bind fail" << std::endl;
exit(BindErr);
}
}
void Listen()
{
if (listen(sockfd_, 10) < 0)
{
std::cout << "listen fail" << std::endl;
exit(ListenErr);
}
}
int Accept(uint16_t *client_port, std::string *client_ip)
{
struct sockaddr_in client;
bzero(&client, sizeof client);
socklen_t len = sizeof(client);
int newfd = accept(sockfd_, (struct sockaddr *)&client, &len);
if (newfd < 0)
{
return -1;
}
char ip[64];
inet_ntop(AF_INET, &client.sin_addr, ip, sizeof ip);
*client_port = ntohs(client.sin_port);
*client_ip = ip;
return newfd;
}
bool Connect(uint16_t server_port, std::string server_ip)
{
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) < 0)
{
return false;
}
return true;
}
~Socket()
{
close(sockfd_);
}
private:
int sockfd_;
};
HttpServer.cc
#include <iostream>
#include <unistd.h>
#include "Socket.hpp"
#include <fstream>
#include <vector>
#include <sstream>
const std::string wwwroot = "./wwwroot";
class HttpServer;
class ThreadData
{
public:
ThreadData(int sockfd, HttpServer *ts)
: sockfd_(sockfd), ts_(ts)
{
}
public:
int sockfd_;
HttpServer *ts_;
};
class HttpServer
{
public:
HttpServer(uint16_t port, std::string ip)
: port_(port), ip_(ip)
{
}
void Init()
{
listenfd_.Init();
listenfd_.Bind(port_, ip_);
listenfd_.Listen();
}
void start()
{
while (1)
{
uint16_t client_port;
std::string client_ip;
int sockfd = listenfd_.Accept(&client_port, &client_ip);
if (sockfd < 0)
{
continue;
}
ThreadData *td = new ThreadData(sockfd, this);
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
static void *ThreadRun(void *arg)
{
pthread_detach(pthread_self());
ThreadData *td = (ThreadData *)arg;
td->ts_->HttpHandler(td->sockfd_);
delete td;
return nullptr;
}
std::string ReadHtmlContent(std::string &htmlpath)
{
std::string content;
std::ifstream in(htmlpath.c_str());
if (!in.is_open())
{
return "404";
}
std::string line;
while (std::getline(in, line))
{
content += line;
}
in.close();
return content;
}
void HttpHandler(int sockfd)
{
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof buffer - 1);
std::string request;
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
request = buffer;
std::vector<std::string> req_header;
while (!request.empty())
{
ssize_t pos = request.find("\r\n", 0);
if (pos == std::string::npos)
{
break;
}
std::string line = request.substr(0, pos);
if (line.empty())
{
break;
}
req_header.push_back(line);
request.erase(0, pos + 2);
}
std::stringstream ss(req_header[0]);
std::string method;
std::string url;
std::string http_version;
ss >> method >> url >> http_version;
std::string path = url;
if (path == "/" || path == "/index.html")
{
path = wwwroot + "/index.html";
}
else
{
path = wwwroot + url;
}
std::string text = ReadHtmlContent(path);
std::string response;
if (text == "404")
{
std::string body = "<h1>404 Not Found</h1>";
response = "HTTP/1.0 404 Not Found\r\n";
response += "Content-Length: " + std::to_string(body.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += body;
}
else
{
response = "HTTP/1.0 200 OK\r\n";
response += "Content-Length: " + std::to_string(text.size()) + "\r\n";
response += "Content-Type: text/html\r\n";
response += "\r\n";
response += text;
}
write(sockfd, response.c_str(), response.size());
}
close(sockfd);
}
~HttpServer()
{
}
private:
Socket listenfd_;
uint16_t port_;
std::string ip_;
};
int main(int argc, char *argv[])
{
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
HttpServer *svr = new HttpServer(port, ip);
svr->Init();
svr->start();
return 0;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index</title>
</head>
<body>
<h1>你是一个好人</h1>
<a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a>
</body>
</html>
hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<h1>祝你幸福</h1>
<a href="http://49.232.193.163:8080/index.html">回到首页</a>
</body>
</html>



HTTP的方法

其中最常用的就是GET和POST方法
现在我们就来通过现象来看看GET和POST方法有什么不同。
GET方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index</title>
</head>
<body>
<h1>你是一个好人</h1>
<form action="http://49.232.193.163:8080/a/b/hello.html" method="GET">
银行卡账号:<input type="text" name="user"><br>
银行卡密码:<input type="password" name="pass"><br>
<input type="submit" value="登录">
</form>
<a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a>
</body>
</html>


通过现象我们可以看到,我们在输入银行卡账号和密码之后,点击登录之后,并没有跳转到我们想要的界面,而是返回给我们404响应码,这是因为我们在使用GET方法之后,会将我们提交的参数拼接到我们的url之后,这就使得我们的url中就有了我们提交的参数信息。
POST方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>index</title>
</head>
<body>
<h1>你是一个好人</h1>
<form action="http://49.232.193.163:8080/a/b/hello.html" method="POST">
银行卡账号:<input type="text" name="user"><br>
银行卡密码:<input type="password" name="pass"><br>
<input type="submit" value="登录">
</form>
<a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a>
</body>
</html>



从结果来看,我们可以看到使用POST的方法的时候,POST的方法会将我们提交的参数显示到http请求信息中的正文部分,我们可以从正文部分看到我们提交的参数信息。
从之前博客中的 TCP 通信,到现在我们亲手实现一个 HTTP 服务器,其实我们已经了解了
- 一个请求是怎么被服务器解析的?
- 浏览器和服务器到底在"说什么"?
- GET 和 POST 的区别
总而言之就是:
👉 HTTP 不再是黑盒,而只是一个"有规则的字符串协议"