本文档介绍了基本计算机网络中有关前后端交互的工程实践,考虑到cpp并非好的后端语言,这里后端所用语言是Go,前端为html。附录中会给出使用cpp作为后端语言进行前后端交互的例子,本文会有概念与实操,按文档进行以获得最佳效果。
本文档前后端最终交互内容:
前端显示一个页面,提示输入一个整数。等待输入后将整数返回后端,根据映射表,后端返回内容给前端,前端再将其输出。
映射表:
| 输入 | 输出 |
|---|---|
| 非整数 | Num必须是整数 |
| 整数 | 2*整数+1 |
模块一:前后端交互简要介绍
简介
首先需要明确的是,前后端交互的三个流程是请求,处理与响应。即前端(也就是浏览器网页),向后端服务器(也就是使用代码写的一个程序)发送一个请求,后端服务器接受后进行处理,处理完后会返回内容给前端(这一过程就是响应)。
打一个比喻: 我们一直写的程序的输入依靠的是文件或者控制台输入,输出是文件或者控制台输出,前后端交互就是输入为前端发送来的请求,输出为返回给前端的内容,相当于后端为一个函数。
HTTP协议
在前后端交互中,HTTP(Hypertext Transfer Protocol)协议是最常用的协议之一。HTTP是一种应用层协议,用于在Web应用程序之间传输数据。它定义了客户端和服务器之间的通信规则和约定。
以下是HTTP协议的一些关键概念和要点:
- 请求和响应: HTTP通信是通过请求和响应进行的。客户端发送HTTP请求给服务器,服务器处理请求并返回HTTP响应给客户端。
- 方法: HTTP定义了几种请求方法,常见的有GET、POST、PUT、DELETE等。不同的方法用于执行不同的操作,如获取资源、提交数据、更新资源和删除资源。
- URL: URL(Uniform Resource Locator)用于标识要访问的资源的位置。它由协议类型、服务器地址、路径和可选的查询参数组成。
- 请求头: HTTP请求包括一个请求头,用于传递关于请求的元数据,如请求的方法、请求的资源、请求的内容类型等。
- 请求体: 某些请求,如POST请求,可以包含请求体。请求体用于传递数据给服务器,如表单数据、JSON数据等。
实例演示:用户登录场景
完整的 HTTP 请求报文 (Request)
POST /api/login HTTP/1.1
Host: 47.101.141.52:8080
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
{"username": "zhangsan", "password": "123"}
- 请求解析:
- 方法与 URL:使用了
POST方法,目标路径是/api/login。 - 请求头:从第一行
POST /api/login开始,到User-Agent: ...结束的所有行。它们描述了“我是谁”以及“我要发什么”。其中Content-Type: application/json告诉后端,接下来的请求体里装的是 JSON 格式的数据。 - 请求体:在空行之后,包含了真正的业务数据
{"username": "zhangsan", ...}。
- 方法与 URL:使用了
- 响应状态码: HTTP响应包括一个状态码,用于指示请求的处理结果。常见的状态码有200表示成功,404表示资源未找到,500表示服务器内部错误等。
- 响应头: HTTP响应还包括响应头,用于传递关于响应的元数据,如响应的内容类型、响应的长度等。
- 响应体: 响应体是服务器返回给客户端的实际数据内容,可以是HTML、JSON、图片等不同类型的数据。
实例演示:用户登录场景
完整的 HTTP 响应报文 (Response)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 46
Set-Cookie: session_id=xyz987; Path=/
{"message": "登录成功", "token": "abcde123"}
- 响应解析:
- 响应状态码:
200 OK明确告诉前端,这次登录逻辑在后端跑通了。 - 响应头:从第一行
HTTP/1.1 200 OK开始,到Set-Cookie: ...结束的所有行。响应头是可以由后端开发者在代码中自定义的。 - 功能说明:
Set-Cookie是后端发给前端的“通行证”。由于 HTTP 是“无状态”的,服务器通过在响应头里自定义这个字段,要求浏览器存下信息。当浏览器下次发起请求时会自动带上它,从而让服务器识别出用户。 - 响应体:返回了 JSON 格式的成功消息和 Token,前端 JavaScript 拿到后会据此执行页面跳转。
- 响应状态码:
小贴士:你会发现请求头/响应头与 Body 之间都有一个空行,那是协议规定的“分界线”,没有它,程序就不知道哪里是信封,哪里是信件内容。
- Cookie和Session: HTTP协议支持使用Cookie和Session来维持状态。服务器可以通过在响应中设置Cookie来存储一些客户端状态信息,客户端将Cookie保存并在后续请求中发送给服务器。
值得注意的是: HTTP协议是一种无状态协议,也就是每一个请求与响应之间是独立的,服务器并不会保留客户端的状态信息。举个例子,对于一个登录界面,前端发送登录请求,后端在验证凭据合法后虽然允许了访问,但由于 HTTP 协议的无状态特性,服务器并不会在内存中记住这位用户,但开发者可能会通过服务端手动实现记忆。这意味着,当用户发起第二次请求(比如查看个人订单)时,服务器依然会把用户当成陌生人,要求你重新证明身份。解决方案就是使用Cookie、token等。
WebSocket协议
WebSocket协议是一种双向的通信协议,他允许在客户端和服务器之间建立持久的连接并进行实时双向通信,相对于HTTP请求,WebSocket提供了更低的延迟与更好的性能,所以我的机器人Nina就也采用了WebSocket协议,用于实时接受消息并处理。
HTTP协议与WebSocket协议的主要区别是,HTTP协议必须每次由前端请求,后端才能产生响应;而WebSocket协议是前后端一次握手(成功连接,这一过程通过HTTP协议)后,前后端可以任意向对方发送消息,直到主动断开连接,后端不再依赖前端的请求。
核心术语
- API: 接口,也就是前端调用接口来实现交互,可以理解为一个函数,调用函数返回内容。
- JSON: 前后端交互的主要语言,一种数据格式。
- 端口: 一台服务器(一个 IP)可以同时运行很多程序(比如网站、数据库)。端口用来区分这些不同的服务。一个端口只能运行一个程序。
- 路由: 可以理解为函数名。路由的组成:URL 中域名/端口之后的部分。
https://api.shop.com:8080/users-->/users就是路由。https://api.shop.com:8080/products-->/products是另一个路由。
- 后端路由: 服务器根据不同的路径,调用不同的函数。看到
/login,去跑登录的代码。看到/get_order,去跑查询订单的代码。
CORS与跨域
浏览器有一个基本安全原则:同源策略。它规定,默认情况下,只有当“协议、域名、端口”三者完全一致时,浏览器才允许前端脚本去读取另一个地址的数据。
举几个例子:
| 场景 | 是否跨域 | 原因 |
|---|---|---|
http://a.com -> http://a.com/api |
否 | 协议、域名、端口都一样 |
http://a.com -> https://a.com |
是 | 协议不同 (http vs https) |
http://a.com -> http://b.com |
是 | 域名不同 (a vs b) |
http://a.com:8080 -> http://a.com:3000 |
是 | 端口不同 (8080 vs 3000) |
为什么要有这个限制? 举个例子:如果登录了网银(bank.com),同时也打开了一个恶意网站(evil.com)。如果没有同源策略,evil.com 里的脚本就能轻而易举地读取你网银的登录信息或余额。
值得注意的是产生了跨域并不意味着后端没有收到请求,事实是,对于简单请求,后端收到请求也返回给了前端,但前端的载体浏览器发现,后端并没有批准跨域,所以拒绝了数据并报错。但对于自定义头与POST(JSON)等的复杂请求,浏览器会发送OPTIONS预检请求,若预检不通过则不会发送业务请求。
目前的主流解决方案有CORS与Nginx反向代理,本文只介绍CORS,Nginx反向代理后续介绍。
CORS其实非常简单,CORS (Cross-Origin Resource Sharing),即跨域资源共享。它做的就是要求后端在响应头里加一张“通行证”:
Access-Control-Allow-Origin: * (允许所有人) 或 http://your-frontend.com (只允许你的前端)。
完整的交互流程
- 前端与用户产生交互,需要发送请求
- 定位IP与端口(API服务的地方)
- 向后端传递请求,请求中会包含路由
- 后端根据不同的路由调用不同的函数,返回响应
以上只是比较基础的前后端交互的概念与流程,更深层的TCP等协议都未做介绍,以下进入实操。
模块二:连接云服务器
参见连接服务器文档
模块三:前后端交互实例1:
Step1: 建议目录结构
cpp-demo/
├── main.cpp
├── httplib.h
└── index.html
Step2: 编写后端代码
#include "httplib.h"
#include <iostream>
using namespace httplib;
int main() {
Server svr;
// 1. 静态文件路由:访问 http://localhost:8080 时返回当前的 index.html
svr.Get("/", [](const Request& /*req*/, Response& res) {
res.set_content("<html><body><h1>Loading...</h1><script>location.href='/index.html'</script></body></html>", "text/html");
});
// 允许服务器读取当前文件夹下的静态文件 (如 index.html)
svr.set_mount_point("/", "./");
// 2. API 路由:处理前端发来的数据
svr.Get("/api/greet", [](const Request& req, Response& res) {
// 获取前端传来的参数 "name"
std::string name = req.get_param_value("name");
if (name.empty()) name = "陌生人";
std::string json_data = "{\"message\": \"你好 " + name + ",这是来自 C++ 后端的回复!\"}";
// 设置跨域头(如果本地直接双击打开 HTML 需要这个)
res.set_header("Access-Control-Allow-Origin", "*");
res.set_content(json_data, "application/json");
std::cout << "收到请求,名字是: " << name << std::endl;
});
std::cout << "C++ 后端已启动:http://localhost:8080" << std::endl;
svr.listen("0.0.0.0", 8080);
return 0;
}
Step3: 编写前端代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>C++ 前后端交互</title>
</head>
<body style="text-align: center; padding-top: 50px; font-family: sans-serif;">
<h2>C++ 本地交互演示</h2>
<input type="text" id="nameInput" placeholder="输入你的名字">
<button onclick="sendToCpp()">发送给 C++</button>
<p id="response" style="color: blue; margin-top: 20px;"></p>
<script>
async function sendToCpp() {
const name = document.getElementById('nameInput').value;
// 请求 C++ 开启的 8080 端口
const res = await fetch(`http://localhost:8080/api/greet?name=${name}`);
const data = await res.json();
document.getElementById('response').innerText = data.message;
}
</script>
</body>
</html>
Step4:
访问https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h ,下载httplib.h,并参考Step1放置在文件夹内。
Step5:
点击开始菜单,搜索“Developer Command Prompt for VS 2022”(注:建立在大家高程使用的vs2022上) 打开命令窗口
where cl
若输出类似
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\<version>\bin\Hostx64\x64\cl.exe
就可以执行编译命令(并非使用g++编译)
cl /EHsc /utf-8 main.cpp ws2_32.lib /Fe:myserver.exe
myserver.exe
(此时任务还在进行,不要关闭终端。当测试结束后,输入ctrl+c,结束进程)
编译可能会遇到很多问题,编码语言请使用UTF-8,否则前端会出现乱码,若变更前端字符代码,则后端编译删去/utf-8,这是我找到的比较好的解决方案,这再次印证了c++并不是一门好的后端语言。
Step6: 打开浏览器,进入 http://localhost:8080
至此,已经完成了一个使用c++编写的前后端交互的程序。
模块四:DevTools使用指南:
Step1:一个挑战题!
密文使用Base64加密,网络上有很多解码器!
访问http://47.101.141.52/ ;来挑战一下吧!
模块五:前后端交互实例2:
Step1: 建议目录结构
go-demo1/
├── main.go
└── index.html
Step2: 编写后端代码
// 1. 告诉系统:这是一个可以独立运行的程序
package main
// 2. 导入工具箱:我们需要处理网络(http)、打印文字(fmt)和处理JSON数据(encoding/json)
import (
"encoding/json"
"fmt"
"net/http"
)
// 3. 定义我们要接收和发送的数据格式
type Message struct {
Name string `json:"name"` // 学生输入的名字
Reply string `json:"reply"` // 服务器回的内容
Details string `json:"details"` //额外信息
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
fmt.Printf("收到来自 %s 的请求!\n", r.RemoteAddr)
// 设置返回的内容格式为 JSON
w.Header().Set("Content-Type", "application/json")
// 如果是“预检请求”(OPTIONS),直接返回成功
if r.Method == "OPTIONS" {
return
}
// 解析学生从前端传来的名字
var received Message
json.NewDecoder(r.Body).Decode(&received)
// 准备要寄回给学生的数据
sendBack := Message{
Name: received.Name,
Reply: "服务器已收到!你好 " + received.Name,
Details: "这是从后端传来的额外详情信息。",
}
// 把数据变成 JSON 字符串发回给前端
json.NewEncoder(w).Encode(sendBack)
}
func main() {
// 设置“路标”:当访问 /api/hello 时,交给 handleRequest 函数处理
http.HandleFunc("/api/hello", handleRequest)
fmt.Println(">>> 后端服务已启动!监听端口: 8080")
// 启动服务
http.ListenAndServe(":8080", nil)
}
Step3: 编写前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>极简前后端交互实验</title>
<style>
body { font-family: 'PingFang SC', sans-serif; padding: 40px; background: #f5f5f5; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
input { padding: 10px; width: 70%; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
.box { margin-top: 20px; padding: 15px; border-radius: 6px; font-size: 13px; }
.sent { background: #e8f0fe; border-left: 5px solid #1a73e8; }
.response { background: #f8f9fa; border-left: 5px solid #343a40; color: #333; }
.error { background: #fff1f0; border-left: 5px solid #ff4d4f; color: #cf1322; }
h4 { margin-bottom: 5px; color: #666; }
pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h3>第一步:输入你的名字</h3>
<input type="text" id="userName" placeholder="比如:张三">
<button onclick="startMission()">发起请求</button>
<div id="requestPart" class="box sent" style="display:none">
<h4>1. 发送的“数据包”实体:</h4>
<pre id="sentJson"></pre>
</div>
<div id="responsePart" class="box response" style="display:none">
<h4>2. 服务器返回的“全貌”(Raw Response):</h4>
<pre id="fullResponse"></pre>
<hr>
<h4>3. 最终提取的实际内容:</h4>
<p id="finalMsg" style="color: green; font-weight: bold;"></p>
</div>
<div id="errorPart" class="box error" style="display:none">
<h4>出错了!原因分析:</h4>
<p id="errorMsg"></p>
</div>
</div>
<script>
const SERVER_IP = "47.101.141.52";
async function startMission() {
const name = document.getElementById('userName').value;
const url = `http://${SERVER_IP}:8080/api/hello`;
// 重置显示状态
document.getElementById('requestPart').style.display = 'block';
document.getElementById('responsePart').style.display = 'none';
document.getElementById('errorPart').style.display = 'none';
const payload = { name: name || "无名氏" };
document.getElementById('sentJson').innerText = JSON.stringify(payload, null, 2);
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// 如果代码运行到这里,说明请求成功了
document.getElementById('responsePart').style.display = 'block';
// 显示返回的完整状态
const rawInfo = `状态码: ${response.status} ${response.statusText}\n内容类型: ${response.headers.get('Content-Type')}`;
document.getElementById('fullResponse').innerText = rawInfo;
// 显示实际内容
const result = await response.json();
document.getElementById('finalMsg').innerText = result.reply + "\n(" + result.details + ")";
} catch (err) {
// 如果出错了,显示错误
document.getElementById('errorPart').style.display = 'block';
document.getElementById('errorMsg').innerText =
"无法连接到服务器。请打开devtools查看console与network两个板块:\n" +
"1. 在console中查看是否存在红色报错,记录下报错\n" +
"2. 打开network面板,查看hello的那条请求的状态码\n";
console.error(err);
}
}
</script>
</body>
</html>
Step4: 根据上述连接服务器上传文件的方法,将main.go上传至服务器。
Step5: 打开服务器终端(bash) 启动后端服务
go run main.go
(此时任务还在进行,不要关闭终端。当测试结束后,输入ctrl+c,结束进程)
Step6:
双击index.html,查看是否能够完成交互,还是会报错。若报错可至devtools查看console与network栏查看报错消息与状态码。
思考为什么会报错。
点击这里查看指导
由于并没有处理跨域,所以浏览器拒绝了后端返回的数据!所以我们需要修改代码,使其接受跨域。 ```go func handleRequest(w http.ResponseWriter, r *http.Request) { fmt.Printf("收到来自 %s 的请求!\n", r.RemoteAddr) // 加入这三行代码,表示允许所有访问 w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") // 后续代码不变 ``` 再跳转至Step4尝试,至此,已经完成现在业界使用主流后端语言go编写的第一个前后端交互!模块六:前后端交互实例3:
前端显示一个页面,提示输入一个整数。等待输入后将整数返回后端,根据映射表,后端返回内容给前端,前端再将其输出。
映射表:
| 输入 | 输出 |
|---|---|
| 非整数 | Num必须是整数 |
| 整数 | 2*整数+1 |
Step1: 建议目录结构
go-demo2/
├── main.go
└── index.html
Step2: 编写后端代码
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
// Response 定义返回数据结构
type Response struct {
Input string `json:"input"` // 保持原始输入为字符串,方便展示
Result interface{} `json:"result"` // 使用接口类型,使其能同时支持整数和字符串
}
func main() {
http.HandleFunc("/api/calc", func(w http.ResponseWriter, r *http.Request) {
// --- 【核心:增加跨域配置】 ---
// 允许所有来源访问
w.Header().Set("Access-Control-Allow-Origin", "*")
// 允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
// 允许的请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// 如果是预检请求 (OPTIONS),直接返回
if r.Method == "OPTIONS" {
return
}
w.Header().Set("Content-Type", "application/json")
// 获取参数
numStr := r.URL.Query().Get("num")
var resp Response
resp.Input = numStr
// 尝试转为整数
num, err := strconv.Atoi(numStr)
if err != nil {
// 【映射表逻辑 1】:非整数
resp.Result = "Num 必须是整数"
} else {
// 【映射表逻辑 2】:整数 -> 2 * n + 1
resp.Result = 2*num + 1
}
// 返回 JSON
json.NewEncoder(w).Encode(resp)
})
fmt.Println("服务启动在 :8080 端口...")
http.ListenAndServe(":8080", nil)
}
Step3: 编写前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"> <!-- 设置字符编码为 UTF-8 -->
<title>2*num+1 计算</title> <!-- 页面标题 -->
</head>
<body>
<h3>整数映射计算器</h3>
<input type="text" id="numInput" placeholder="请输入内容">
<button onclick="calculate()">提交</button>
<div style="margin-top: 20px;">
<p>输入内容:<span id="showInput"></span></p>
<p>返回结果:<b id="showResult" style="color: blue;"></b></p>
</div>
<script>
const SERVER_IP = "47.101.141.52";
async function calculate() {
const val = document.getElementById('numInput').value;
// 调用后端接口
const response = await fetch(`http://${SERVER_IP}:8080/api/calc?num=${val}`);
const data = await response.json();
document.getElementById('showInput').innerText = data.input;
document.getElementById('showResult').innerText = data.result;
}
</script>
</body>
</html>
Step4: 根据上述连接服务器上传文件的方法,将main.go上传至服务器。
Step5: 打开服务器终端(bash) 启动后端服务
go run main.go
(此时任务还在进行,不要关闭终端。当测试结束后,按ctrl+c,结束进程)
至此,已经完成本教程的全部内容,对前后端交互流程已有大致理解。以上go程序均为go原生http,并没有涉及到gin框架,框架后续介绍。工程上使用框架可以大幅降低代码量,常见的为go的gin框架与java的spring。
附录:
如果出现端口被占用怎么办,两种解决方案
A1:将后端服务端口换一个
A2:
sudo lsof -i :8080 #8080更改为被占用的那个端口
kill -9 <pid>
END