一.简介
以下的内容以及代码,是基于c++实现的一个简单的部署在Linux的web服务器,以及一个简单的前端加上一个负责保存留言的guestbook.txt,这个web服务器是一个简单的单线程程序,并不涉及线程池,进程池,互斥锁等意在实现Linux高性能服务器的编程。则相当这是我自己的一个学习记录。想看进阶一点的可以尝试这位朋友的博客。简单web服务器的实现(C++)https://blog.youkuaiyun.com/qq_36573828/article/details/82784425
本人也很少参与与他人合作的代码项目,而关于网络编程也大概只是停留在大学的计算机网络这门课程上,因此代码可读性不强,臃肿甚至错误多多也许希望大家尽情谅解,本人也是小白一只。
二.项目实现
一旦涉及网络通信,就不能避开TCP/IP协议以及三次握手和四次挥手。而在c++中也提供了socket.h等头文件来直接调用实现网络通信。
这是一个简单的c++web服务器
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
const int port = 8888;
int main(int argc,char *argv[])
{
// 检查命令行参数数量
if(argc<0)
{
printf("need two canshu\n");
return 1;
}
int sock;
int connfd;
struct sockaddr_in sever_address;
// 清空并初始化服务器地址结构
bzero(&sever_address,sizeof(sever_address));
sever_address.sin_family = PF_INET;
sever_address.sin_addr.s_addr = htons(INADDR_ANY);
sever_address.sin_port = htons(8888);
// 创建TCP套接字
sock = socket(AF_INET,SOCK_STREAM,0);
assert(sock>=0);
// 将套接字绑定到服务器地址上
int ret = bind(sock, (struct sockaddr*)&sever_address,sizeof(sever_address));
assert(ret != -1);
// 监听套接字,等待连接
ret = listen(sock,1);
assert(ret != -1);
while(1)
{
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
// 接受客户端连接
connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd<0)
{
printf("errno\n");
}
else
{
// 读取客户端请求
char request[1024];
recv(connfd,request,1024,0);
request[strlen(request)+1]='\0';
printf("%s\n",request);
printf("successeful!\n");
// 构造HTTP响应头部
char buf[520]="HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";
// 发送HTTP响应头部
int s = send(connfd,buf,strlen(buf),0);
// 打开并发送HTML文件内容
int fd = open("hello.html",O_RDONLY);
sendfile(connfd,fd,NULL,2500); // 使用零拷贝发送文件内容
close(fd);
// 关闭连接套接字
close(connfd);
}
}
return 0;
}
以及一个简单的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>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
}
.container {
text-align: center;
}
h1 {
font-size: 3em;
color: #333;
}
p {
font-size: 1.2em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello, World!</h1>
<p>Welcome to my beautiful page.</p>
</div>
</body>
</html>
在这段代码中,并没有直接处理TCP的三次握手和四次挥手过程,因为这些过程是由操作系统的网络栈自动管理的。应用程序只需要通过标准的套接字API(如socket
, bind
, listen
, accept
, send
, recv
, close
)来请求建立、使用和关闭TCP连接。
在上述代码中,三次握手的实现是隐含的:
- 当服务器调用
listen
函数进入监听状态后,它会等待客户端的连接请求。 - 当客户端尝试连接服务器时,操作系统会为服务器自动处理第一个SYN和随后的SYN-ACK的接收和发送。
- 当服务器调用
accept
函数时,它实际上是在等待一个已经完成了三次握手的客户端连接。accept
函数返回一个全新的套接字(称为已接受套接字),用于与客户端通信。
四次挥手的实现也是隐含的:
- 当客户端或服务器决定关闭连接时,它会调用
close
函数。对于服务器端,这是在处理完每个客户端请求后发生的。 close
函数会导致套接字发送一个FIN段,开始关闭连接的过程。- 对方接收到FIN后,会发送一个ACK,并可能在适当的时候发送自己的FIN。
- 最后,当FIN被确认后,连接被完全关闭。
在浏览器上面成功访问到本地web服务器后,我们可以在服务器得到以下的响应
这是一个 HTTP 请求报文,用于向服务器发起请求。
GET / HTTP/1.1 // 请求行:请求方法为GET,请求路径为根路径'/',使用HTTP/1.1协议版本
Host: localhost:8888 // 主机头部:指定了请求的目标主机和端口号
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0 // 用户代理头部:发起请求的用户代理(浏览器)的信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 // 接受头部:指定客户端可以接受的媒体类型及其优先级
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 // 接受语言头部:指定客户端接受的自然语言及其优先级
Accept-Encoding: gzip, deflate, br // 接受编码头部:指定客户端接受的内容编码方式
Connection: keep-alive // 连接头部:指定客户端请求完成后是否保持与服务器的连接
Upgrade-Insecure-Requests: 1 // 升级头部:表示客户端是否愿意升级到HTTPS请求以提高安全性
Sec-Fetch-Dest: document // 安全头部:帮助服务器判断请求的目的地
Sec-Fetch-Mode: navigate // 安全头部:帮助服务器判断请求的模式
Sec-Fetch-Site: none // 安全头部:帮助服务器判断请求的来源
Sec-Fetch-User: ?1 // 安全头部:帮助服务器判断请求的用户
// 空行,用于分隔请求头部和请求体
successeful! // 表示服务器成功处理请求的标志,由客户端发出
虽然http1.1协议问世已经有一段年头了,但是它仍然是绝大多数浏览器可执行的协议,对于涉及web服务器,我们也避不开对http的方法调用与状态码分析,这就要及时翻阅相关的技术手册。大家也可以看这篇博客。这里,http的https://blog.youkuaiyun.com/weixin_60358891/article/details/130231494
这里也补一个http响应格式,
其实关于http响应格式的,感兴趣的也可以做抓包实验,学校一般做的是wireshark的抓包实验,做过一次也会对这个东西很清楚的。我也放了一个实验指导(由于目前互联网上的许多网站已经更新协议至https,导致使用wireshark通过http协议来抓取明文密码的方法已不再可行,现在的网站一般都不可能直接抓包获取你提交的账号密码了)。
https://pan.baidu.com/s/1Q3ms10SU-Sk9tOuHXVCjgw?pwd=4566
提取码:4566
以下就是单单为原本的web服务器加上从前端读取留言,并将时间戳和留言以及响应记录在guestbook.txt,而如果前端向服务器发出请求获取留言,则将guestbook.txt打印出来。为了实现好这一目的,我们需要在前端网页提前设计向web服务器发送和刷新留言的功能,虽然下述代码是通过使用了JavaScript和AJAX技术实现的,但是AJAX在其中的使用是偏向简单的,或者说无论是我的前端和web服务器都没有实现完整的错误处理逻辑。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>留言板</title>
<script>
// refreshMessages 函数用于通过 AJAX 请求刷新留言记录
function refreshMessages() {
var xhr = new XMLHttpRequest(); // 创建 AJAX 请求对象
xhr.onreadystatechange = function() { // 设置请求状态变化的回调函数
if (xhr.readyState === XMLHttpRequest.DONE) { // 检查请求是否完成
if (xhr.status === 200) { // 检查请求是否成功
// 将响应内容设置为留言记录表的内联HTML
document.getElementById("messagesTable").innerHTML = xhr.responseText;
} else {
alert("无法获取留言记录"); // 请求失败时提示用户
}
}
};
xhr.open("GET", "guestbook.txt", true); // 初始化 GET 请求
xhr.send(); // 发送请求
}
// sendMessage 函数用于发送用户留言到服务器
function sendMessage() {
var message = document.getElementById("messageInput").value; // 获取用户输入的留言
if (message.trim() === "") { // 检查留言是否为空
alert("留言不能为空!"); // 留言为空时提示用户
return;
}
var xhr = new XMLHttpRequest(); // 创建 AJAX 请求对象
xhr.onreadystatechange = function() { // 设置请求状态变化的回调函数
if (xhr.readyState === XMLHttpRequest.DONE) { // 检查请求是否完成
if (xhr.status === 200) {
alert("留言成功!");
// 留言成功后清空留言框,并刷新留言记录
document.getElementById("messageInput").value = "";
refreshMessages();
} else {
alert("留言失败!");
}
}
};
xhr.open("POST", "", true); // 初始化 POST 请求
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 设置请求头
// 发送留言内容
xhr.send("message=" + message);
}
</script>
</head>
<body>
<h1>留言板</h1>
<!-- 留言记录表,留言记录将通过 AJAX 请求动态加载 -->
<table id="messagesTable" border="1">
<!-- 表内内容将被 AJAX 请求的响应文本替换 -->
</table>
<!-- 留言输入框和发送按钮 -->
<!-- 表单的onsubmit事件被设置为阻止默认提交行为,并调用 sendMessage 函数 -->
<form id="messageForm" onsubmit="event.preventDefault(); sendMessage();">
<input type="text" id="messageInput" placeholder="输入您的留言">
<input type="submit" value="发送留言">
</form>
<!-- 刷新留言记录按钮,点击时调用 refreshMessages 函数 -->
<button onclick="refreshMessages()">刷新留言记录</button>
</body>
</html>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <cstring>
#include <errno.h>
#include <dirent.h>
#include <map>
#include <ctime>
// 定义服务器监听的端口号和文件路径
const int port = 8888;
const char* guestbook_path = "guestbook.txt"; // 留言板文件的路径
const char* html_file_path = "h.html"; // HTML表单文件的路径
// 函数:发送文件内容给客户端
void send_file(int client_socket, const char* file_path) {
// 打开文件用于读取
FILE *file = fopen(file_path, "rb");
if (file) {
// 获取文件大小
fseek(file, 0, SEEK_END);
size_t file_size = ftell(file);
fseek(file, 0, SEEK_SET);
// 分配内存用于存储文件内容
char *file_content = (char *)malloc(file_size);
// 读取文件内容到内存
if (fread(file_content, 1, file_size, file) != file_size) {
perror("fread failed"); // 读取失败,打印错误
free(file_content); // 释放内存
fclose(file); // 关闭文件
return;
}
// 发送HTTP响应头
const char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
if (send(client_socket, response, strlen(response), 0) < 0) {
perror("send failed"); // 发送失败,打印错误
}
// 发送文件内容
if (send(client_socket, file_content, file_size, 0) < 0) {
perror("send failed"); // 发送失败,打印错误
}
// 释放内存和关闭文件
free(file_content);
fclose(file);
} else {
perror("fopen failed"); // 打开文件失败,打印错误
// 发送404 Not Found响应
const char *response = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
if (send(client_socket, response, strlen(response), 0) < 0) {
perror("send failed"); // 发送失败,打印错误
}
}
}
// 函数:将留言追加到留言板文件中
void append_message(const char* message) {
// 打开留言板文件用于追加
FILE *guestbook = fopen(guestbook_path, "a");
if (guestbook) {
// 获取当前时间
time_t now = time(NULL);
// 将留言和时间追加到文件中
fprintf(guestbook, "%s - %s\n", ctime(&now), message);
fclose(guestbook); // 关闭文件
} else {
perror("Error opening guestbook file"); // 打开文件失败,打印错误
}
}
// 函数:处理GET请求,显示留言板内容
void handle_get_guestbook(int client_socket) {
send_file(client_socket, guestbook_path); // 发送留言板文件内容
}
int main(int argc, char *argv[]) {
// 检查命令行参数数量
if (argc != 1) {
fprintf(stderr, "Usage: %s\n", argv[0]);
return 1;
}
// 声明套接字和地址变量
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
ssize_t bytes_received;
// 创建服务器套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("socket creation failed"); // 创建失败,打印错误
return 1;
}
// 初始化服务器地址结构
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听任意IP地址
server_addr.sin_port = htons(port); // 端口号
// 将服务器套接字绑定到地址
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed"); // 绑定失败,打印错误
return 1;
}
// 开始监听服务器套接字
if (listen(server_socket, 1) < 0) {
perror("listen failed"); // 监听失败,打印错误
return 1;
}
// 打印服务器监听信息
printf("Server is listening on port %d...\n", port);
while (1) {
// 接受客户端连接
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
if (client_socket < 0) {
perror("accept failed"); // 接受失败,打印错误
continue;
}
// 打印客户端连接信息
printf("Connected to client at %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 接收客户端请求
bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received <= 0) {
perror("recv failed"); // 接收失败,打印错误
close(client_socket); // 关闭客户端套接字
continue;
}
buffer[bytes_received] = '\0'; // 确保以空字符结尾
// 打印接收到的请求
printf("Received request: %s\n", buffer);
// 检查请求类型
if (strncmp(buffer, "GET /", 5) == 0) {
// 处理GET请求
const char *get_target = buffer + 5;
if (strncmp(get_target, "guestbook", 8) == 0) {
handle_get_guestbook(client_socket); // 显示留言板内容
} else {
send_file(client_socket, html_file_path); // 发送HTML表单页面
}
} else if (strncmp(buffer, "POST /", 6) == 0) {
// 处理POST请求
const char *message_start = buffer + 6; // 跳过"POST /"
append_message(message_start); // 追加留言到留言板
// 发送200 OK响应
const char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
if (send(client_socket, response, strlen(response), 0) < 0) {
perror("send failed"); // 发送失败,打印错误
}
} else {
// 处理其他请求类型
// 发送400 Bad Request响应
const char *response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n";
if (send(client_socket, response, strlen(response), 0) < 0) {
perror("send failed"); // 发送失败,打印错误
}
}
// 关闭客户端套接字
close(client_socket);
}
// 关闭服务器套接字
close(server_socket);
return 0;
}
当然前端的代码实现也有改进的地方,可以通过JavaScript过滤发来的无效信息以及重新排版达到美观的效果,到此,上述代码以及一个 guestbook.txt文本文件,就可以实现通过本地接口8888发送留言保存在web服务器,并且通过这个接口获取留言,以上代码仅仅是简单的单线程服务器,并不涉及构建高性能服务器的知识。
三.后记
这几天,从我舍友捞了一本书回来,我大略看了一下,如果大家想做Linux的高性能的服务器的,这本书也不错,是机械工业出版社的《Linux高性能服务器编程》,刚坑蒙拐骗拿过来,还没细看,但是讲的挺深的,不过我也基本是把这个项目打完了,而且这本书也确实有点难啃,抽屉里还有一本3年也没看下去的数据科学的,从线性回归到深度学习的书,大概率这本书也是这样,不过能看一点是一点。说实话,这种技术类的书籍,比课本难也不如博客来的简便,但它的专业性够强,还是希望未来的学弟学妹别像我一样,东看看西跑跑的,静不下来,这类型的书还是很有帮助的,上述的web服务器代码也可能在我看这本书到某个程度的时候改进一下,说不定也搞一个高性能的服务器出来。