大家好,我是 同学小张,+v: jasper_8017 一起交流,持续学习C++进阶、OpenGL、WebGL知识和AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。
如果C++程序中需要接收HTTP消息,但需求不大,不需要复杂的网络模块的处理,不需要考虑高并发等场景。这时候如果用第三方库就有点大材小用了。直接用C++原生方式手搓一个HTTP服务就比较方便且轻便了,不用安装第三方库,不用链接动态链接库,不用解决其它一些依赖问题。
本文我们使用c++的socket接口,从0到1实现一个可用的简单的http server。
前置知识(socket接口的使用)可以参考前面的文章:C++中的socket网络编程,兼看TCP的三次握手与四次挥手过程(附常见面试题整理)
文章目录
- 1. 主要代码
- 2. 代码中的一些细节处理
- 2.1 地址端口重用
- 2.1.1 端口重用的意义和作用
- 2.1.2 使用 `SO_REUSEADDR` 的注意事项
- 2.2 在一些错误的地方可以加一些日志来发现连接失败的原因
- 3. 处理 HTTP 请求
- 4. 头文件与主函数
- 5. 总结
1. 主要代码
直接先上主要代码:
void server(unsigned short port) {
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
return;
}
sockaddr_in server_addr;
std::memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port);
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
close(server_socket);
return;
}
if (listen(server_socket, 5) == -1) {
close(server_socket);
return;
}
while (true) {
sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_socket == -1) {
continue;
}
std::thread(handle_client, client_socket).detach();
}
close(server_socket);
}
这是 socket 服务建立的主要流程:
(1)首先socket函数创建一个新的socket:socket(AF_INET, SOCK_STREAM, 0)
(2)bind指定端口:if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
(3)listen监听起来:if (listen(server_socket, 5) == -1)
(4)accept 接收连接请求:accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
(5)接收到新的连接之后,通过 std::thread(handle_client, client_socket).detach();
来新开一个线程处理这个链接
(6)关闭服务socket:close(server_socket);
2. 代码中的一些细节处理
以上主要流程中,实际应用时可能还需要加一点细节:
2.1 地址端口重用
在bind之前,加入以下代码:
int reuse_option = 1;
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &reuse_option, sizeof(reuse_option)) == -1) {
// 处理错误
}
使用 SO_REUSEADDR 这个选项时,它允许一个已绑定到特定端口的 socket 可以被快速重用,即使该端口上的 TCP 连接在完成正常的四次挥手后不久,端口仍然处于 TIME_WAIT 状态。SO_REUSEADDR 主要适用于服务器端的 socket,因为它需要频繁地创建和关闭连接。对于客户端 socket,通常不需要设置这个选项。
setsockopt()
函数在 C++ 网络编程中用来设置 socket 的选项。当使用 SO_REUSEADDR
这个选项时,它允许一个已绑定到特定端口的 socket 可以被快速重用,即使该端口上的 TCP 连接在完成正常的四次挥手后不久,端口仍然处于 TIME_WAIT 状态。
2.1.1 端口重用的意义和作用
(1)加快端口重用速度:
- 在 TCP 连接关闭后,端口通常会进入 TIME_WAIT 状态,这段时间内端口不能被立即重用。
SO_REUSEADDR
允许端口在 TIME_WAIT 状态下被重用,从而加快了端口的重用速度。
(2)避免端口耗尽:
- 如果应用程序频繁地创建和关闭连接,端口可能会很快耗尽。通过允许端口重用,可以减少端口耗尽的风险。
(3)提高服务器性能:
- 对于需要处理大量并发连接的服务器来说,端口重用可以减少等待端口释放的时间,从而提高服务器的响应速度和性能。
(4)简化编程模型:
- 开发者不需要编写额外的代码来处理端口耗尽的情况,简化了编程模型。
(5)避免端口冲突:
- 在多进程或多线程环境中,不同的进程或线程可能会尝试绑定到同一个端口。使用
SO_REUSEADDR
可以避免这种情况导致的端口冲突。
2.1.2 使用 SO_REUSEADDR
的注意事项
- 安全性问题:
- 允许端口重用可能会带来一些安全风险,因为端口可以被快速重用,恶意用户可能会利用这一点来发起 SYN Flood 攻击。因此,在使用
SO_REUSEADDR
时,需要权衡性能和安全性。
- 允许端口重用可能会带来一些安全风险,因为端口可以被快速重用,恶意用户可能会利用这一点来发起 SYN Flood 攻击。因此,在使用
2.2 在一些错误的地方可以加一些日志来发现连接失败的原因
例如:
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
if (errno == EADDRINUSE) {
// 可以在这里加入重试逻辑,或者选择另一个端口等
} else {
// 其他类型的bind错误处理
}
close(server_socket);
return;
}
通过errorno 可以知道bind错误时具体的原因,从而作出相应的处理。
3. 处理 HTTP 请求
上面提到,在建立一个新的连接之后,会通过 std::thread(handle_client, client_socket).detach();
来新开一个线程处理这个连接。处理这个连接的函数,我们这里叫 handle_client
:
void handle_client(int client_socket) {
char buffer[1024];
std::string response;
// Read the request
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
std::string request(buffer);
// Parse the request to extract parameters
std::string request_line = request.substr(0, request.find("\r\n"));
std::istringstream request_stream(request_line);
std::string method, uri, version;
request_stream >> method >> uri >> version;
// Check if the requested URI is "/set_value"
if (uri.find("/set_value") == 0) {
// TODO 函数的具体执行逻辑
response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: " + std::to_string(success_message.size()) + "\r\n"
"\r\n" + success_message;
}
} else {
// Handle not found
std::string error_message = "Error: API Not Found";
response =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: " + std::to_string(error_message.size()) + "\r\n"
"\r\n" + error_message;
}
} else {
std::string error_message = "Error: Bad Request";
response =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: " + std::to_string(error_message.size()) + "\r\n"
"\r\n" + error_message;
}
// Send the response
send(client_socket, response.c_str(), response.size(), 0);
// Close the client socket
close(client_socket);
}
这个函数里的内容:
(1)通过 recv 函数接收客户端发来的消息内容
(2)处理消息,组织成 HTTP 的回复消息格式
(3)send 函数将回复消息发送给客户端
(4)close(client_socket);
关闭这一次的连接
其中,处理消息时,通过 uri.find("/set_value")
来区分 HTTP 调用的接口,这样就可以实现接口的分发。
4. 头文件与主函数
#include <iostream>
#include <thread>
#include <sstream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
unsigned int port = 8080;
std::thread server_thread([port]() {
server(port);
});
server_thread.detach();
// 主线程,其它处理逻辑
}
这里,新开一个线程来启动socket服务,不阻塞主线程其它逻辑。
编译和运行:
g++ -std=c++11 -o simple_server simple_server.cpp -pthread
./simple_server
运行之后,可以使用浏览器或curl命令访问和测试服务器。
5. 总结
本文从0开始利用C++ socket相关函数用原生的方式实现了一个简单的HTTP服务,并提供了一些细节说明。
以上程序大家可以直接粘贴使用。有问题欢迎 +v jasper_8017 来交流讨论。
如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~
- 大家好,我是 同学小张,持续学习C++进阶、OpenGL、WebGL知识和AI大模型应用实战案例
- 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
- +v: jasper_8017 一起交流💬,一起进步💪。
- 微信公众号搜【同学小张】 🙏
本站文章一览: