C++实现轻量级极简httpserver和httpclient(提供http和websocket接口)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012234115/article/details/79596826

一般来说,C++的项目多是偏底层,不怎么需要跟http打交道,但有时候又需要在C++后端项目中加入一些简单 http以及websocket接口,比如游戏运营服务器,金融交易监控服务等。

但是传统的实现方法比如采用libcurl,asio等较为重型的框架来做有没有必要,因此,这里采用mongoose这个库来实现基本的httpserver和httpclient功能,非常简单,包含一个h文件,一个cpp文件到工程中就行了,无需编译,无需链接库。

本文实现了一个project,将mongoose中提供的http相关api封装成了httpserver类和httpclient类,方便调用,目录结构如下:

├─common
    ├─mongoose.h
    └─mongoose.cpp
├─httpclient
    ├─http_client.h
    ├─http_client.cpp
    └─main.cpp
└─httpserver
    └─web
       └─index.html
    ├─http_server.h
    ├─http_server.cpp
    └─main.cpp

编译环境:win10,vs2015, C++11 (其实是跨平台的)

http服务器

http_server.h

#pragma once

#include <string>
#include <string.h>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include "../common/mongoose.h"

// 定义http返回callback
typedef void OnRspCallback(mg_connection *c, std::string);
// 定义http请求handler
using ReqHandler = std::function<bool (std::string, std::string, mg_connection *c, OnRspCallback)>;

class HttpServer
{
public:
	HttpServer() {}
	~HttpServer() {}
	void Init(const std::string &port); // 初始化设置
	bool Start(); // 启动httpserver
	bool Close(); // 关闭
	void AddHandler(const std::string &url, ReqHandler req_handler); // 注册事件处理函数
	void RemoveHandler(const std::string &url); // 移除时间处理函数
	static std::string s_web_dir; // 网页根目录 
	static mg_serve_http_opts s_server_option; // web服务器选项
	static std::unordered_map<std::string, ReqHandler> s_handler_map; // 回调函数映射表

private:
	// 静态事件响应函数
	static void OnHttpWebsocketEvent(mg_connection *connection, int event_type, void *event_data);

	static void HandleHttpEvent(mg_connection *connection, http_message *http_req);
	static void SendHttpRsp(mg_connection *connection, std::string rsp);

	static int isWebsocket(const mg_connection *connection); // 判断是否是websoket类型连接
	static void HandleWebsocketMessage(mg_connection *connection, int event_type, websocket_message *ws_msg); 
	static void SendWebsocketMsg(mg_connection *connection, std::string msg); // 发送消息到指定连接
	static void BroadcastWebsocketMsg(std::string msg); // 给所有连接广播消息
	static std::unordered_set<mg_connection *> s_websocket_session_set; // 缓存websocket连接

	std::string m_port;    // 端口
	mg_mgr m_mgr;          // 连接管理器
};

http_server.cpp

#include <utility>
#include "http_server.h"

void HttpServer::Init(const std::string &port)
{
	m_port = port;
	s_server_option.enable_directory_listing = "yes";
	s_server_option.document_root = s_web_dir.c_str();

	// 其他http设置

	// 开启 CORS,本项只针对主页加载有效
	// s_server_option.extra_headers = "Access-Control-Allow-Origin: *";
}

bool HttpServer::Start()
{
	mg_mgr_init(&m_mgr, NULL);
	mg_connection *connection = mg_bind(&m_mgr, m_port.c_str(), HttpServer::OnHttpWebsocketEvent);
	if (connection == NULL)
		return false;
	// for both http and websocket
	mg_set_protocol_http_websocket(connection);

	printf("starting http server at port: %s\n", m_port.c_str());
	// loop
	while (true)
		mg_mgr_poll(&m_mgr, 500); // ms

	return true;
}

void HttpServer::OnHttpWebsocketEvent(mg_connection *connection, int event_type, void *event_data)
{
	// 区分http和websocket
	if (event_type == MG_EV_HTTP_REQUEST)
	{
		http_message *http_req = (http_message *)event_data;
		HandleHttpEvent(connection, http_req);
	}
	else if (event_type == MG_EV_WEBSOCKET_HANDSHAKE_DONE ||
		     event_type == MG_EV_WEBSOCKET_FRAME ||
		     event_type == MG_EV_CLOSE)
	{
		websocket_message *ws_message = (struct websocket_message *)event_data;
		HandleWebsocketMessage(connection, event_type, ws_message);
	}
}

// ---- simple http ---- //
static bool route_check(http_message *http_msg, char *route_prefix)
{
	if (mg_vcmp(&http_msg->uri, route_prefix) == 0)
		return true;
	else
		return false;

	// TODO: 还可以判断 GET, POST, PUT, DELTE等方法
	//mg_vcmp(&http_msg->method, "GET");
	//mg_vcmp(&http_msg->method, "POST");
	//mg_vcmp(&http_msg->method, "PUT");
	//mg_vcmp(&http_msg->method, "DELETE");
}

void HttpServer::AddHandler(const std::string &url, ReqHandler req_handler)
{
	if (s_handler_map.find(url) != s_handler_map.end())
		return;

	s_handler_map.insert(std::make_pair(url, req_handler));
}

void HttpServer::RemoveHandler(const std::string &url)
{
	auto it = s_handler_map.find(url);
	if (it != s_handler_map.end())
		s_handler_map.erase(it);
}

void HttpServer::SendHttpRsp(mg_connection *connection, std::string rsp)
{
	// --- 未开启CORS
	// 必须先发送header, 暂时还不能用HTTP/2.0
	mg_printf(connection, "%s", "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
	// 以json形式返回
	mg_printf_http_chunk(connection, "{ \"result\": %s }", rsp.c_str());
	// 发送空白字符快,结束当前响应
	mg_send_http_chunk(connection, "", 0);

	// --- 开启CORS
	/*mg_printf(connection, "HTTP/1.1 200 OK\r\n"
			  "Content-Type: text/plain\n"
			  "Cache-Control: no-cache\n"
			  "Content-Length: %d\n"
			  "Access-Control-Allow-Origin: *\n\n"
			  "%s\n", rsp.length(), rsp.c_str()); */
}

void HttpServer::HandleHttpEvent(mg_connection *connection, http_message *http_req)
{
	std::string req_str = std::string(http_req->message.p, http_req->message.len);
	printf("got request: %s\n", req_str.c_str());

	// 先过滤是否已注册的函数回调
	std::string url = std::string(http_req->uri.p, http_req->uri.len);
	std::string body = std::string(http_req->body.p, http_req->body.len);
	auto it = s_handler_map.find(url);
	if (it != s_handler_map.end())
	{
		ReqHandler handle_func = it->second;
		handle_func(url, body, connection, &HttpServer::SendHttpRsp);
	}

	// 其他请求
	if (route_check(http_req, "/")) // index page
		mg_serve_http(connection, http_req, s_server_option);
	else if (route_check(http_req, "/api/hello")) 
	{
		// 直接回传
		SendHttpRsp(connection, "welcome to httpserver");
	}
	else if (route_check(http_req, "/api/sum"))
	{
		// 简单post请求,加法运算测试
		char n1[100], n2[100];
		double result;

		/* Get form variables */
		mg_get_http_var(&http_req->body, "n1", n1, sizeof(n1));
		mg_get_http_var(&http_req->body, "n2", n2, sizeof(n2));

		/* Compute the result and send it back as a JSON object */
		result = strtod(n1, NULL) + strtod(n2, NULL);
		SendHttpRsp(connection, std::to_string(result));
	}
	else
	{
		mg_printf(
			connection,
			"%s",
			"HTTP/1.1 501 Not Implemented\r\n" 
			"Content-Length: 0\r\n\r\n");
	}
}

// ---- websocket ---- //
int HttpServer::isWebsocket(const mg_connection *connection)
{
	return connection->flags & MG_F_IS_WEBSOCKET;
}

void HttpServer::HandleWebsocketMessage(mg_connection *connection, int event_type, websocket_message *ws_msg)
{
	if (event_type == MG_EV_WEBSOCKET_HANDSHAKE_DONE)
	{
		printf("client websocket connected\n");
		// 获取连接客户端的IP和端口
		char addr[32];
		mg_sock_addr_to_str(&connection->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
		printf("client addr: %s\n", addr);

		// 添加 session
		s_websocket_session_set.insert(connection);

		SendWebsocketMsg(connection, "client websocket connected");
	}
	else if (event_type == MG_EV_WEBSOCKET_FRAME)
	{
		mg_str received_msg = {
			(char *)ws_msg->data, ws_msg->size
		};

		char buff[1024] = {0};
		strncpy(buff, received_msg.p, received_msg.len); // must use strncpy, specifiy memory pointer and length

		// do sth to process request
		printf("received msg: %s\n", buff);
		SendWebsocketMsg(connection, "send your msg back: " + std::string(buff));
		//BroadcastWebsocketMsg("broadcast msg: " + std::string(buff));
	}
	else if (event_type == MG_EV_CLOSE)
	{
		if (isWebsocket(connection))
		{
			printf("client websocket closed\n");
			// 移除session
			if (s_websocket_session_set.find(connection) != s_websocket_session_set.end())
				s_websocket_session_set.erase(connection);
		}
	}
}

void HttpServer::SendWebsocketMsg(mg_connection *connection, std::string msg)
{
	mg_send_websocket_frame(connection, WEBSOCKET_OP_TEXT, msg.c_str(), strlen(msg.c_str()));
}

void HttpServer::BroadcastWebsocketMsg(std::string msg)
{
	for (mg_connection *connection : s_websocket_session_set)
		mg_send_websocket_frame(connection, WEBSOCKET_OP_TEXT, msg.c_str(), strlen(msg.c_str()));
}

bool HttpServer::Close()
{
	mg_mgr_free(&m_mgr);
	return true;
}

main.cpp

#include <iostream>
#include <memory>
#include "http_server.h"

// 初始化HttpServer静态类成员
mg_serve_http_opts HttpServer::s_server_option;
std::string HttpServer::s_web_dir = "./web";
std::unordered_map<std::string, ReqHandler> HttpServer::s_handler_map;
std::unordered_set<mg_connection *> HttpServer::s_websocket_session_set;

bool handle_fun1(std::string url, std::string body, mg_connection *c, OnRspCallback rsp_callback)
{
	// do sth
	std::cout << "handle fun1" << std::endl;
	std::cout << "url: " << url << std::endl;
	std::cout << "body: " << body << std::endl;

	rsp_callback(c, "rsp1");

	return true;
}

bool handle_fun2(std::string url, std::string body, mg_connection *c, OnRspCallback rsp_callback)
{
	// do sth
	std::cout << "handle fun2" << std::endl;
	std::cout << "url: " << url << std::endl;
	std::cout << "body: " << body << std::endl;

	rsp_callback(c, "rsp2");

	return true;
}

int main(int argc, char *argv[]) 
{
	std::string port = "7999";
	auto http_server = std::shared_ptr<HttpServer>(new HttpServer);
	http_server->Init(port);
	// add handler
	http_server->AddHandler("/api/fun1", handle_fun1);
	http_server->AddHandler("/api/fun2", handle_fun2);
	http_server->Start();
	

	return 0;
}

index.html

<!DOCTYPE html>
<html>
<head>
  <title>RESTful API demo</title>

<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript">
    // simple http
    $(document).ready(function(){
     $("button").click(function(){
        $.get("/api/hello",function(data, status){
            console.log("get rsp: ", data);
            $('#result1').html(data);
        });
       });
    });

    $(document).on('keyup', '#n1, #n2', function() {
      $.ajax({
        url: '/api/sum',
        method: 'POST',
        dataType: 'json',
        data: { n1: $('#n1').val(), n2: $('#n2').val() },
        success: function(json) {
          console.log("post rsp: ", json);
          $('#result2').html(json.result);
        }
      });
    });

    // websocket
    var websocket = new WebSocket('ws://' + location.host + '/ws');
    websocket.onopen = function (ev) {
        console.log(ev.data);
    };
    websocket.onerror = function (ev) {
        console.log(ev.data);
    };
    websocket.onclose = function (ev) {
        console.log(ev.data);
    };
    websocket.onmessage = function (ev) {
        console.log(ev.data);

        document.getElementById("ws_text").innerHTML = ev.data;
    };

    window.onload = function () {
        document.getElementById('send_button').onclick = function (ev) {
            var msg = document.getElementById('send_input').value;
            websocket.send(msg);
        };
    };
</script>
</head>
<body>
    <h1>c++ httpserver demo</h1>

    <h2>simple http</h2>

    <h3>GET</h3>
    <div>
        <button id="btn">get request</button>
    </div>
    <div>
     <label>Result1:</label> <span id="result1">&nbsp;</span>
    </div>

    <h3>POST</h3>
    <div>
      <label>Number 1:</label> <input type="text" id="n1" />
    </div>
    <div>
      <label>Number 2:</label> <input type="text" id="n2" />
    </div>
    <div>
     <label>Result2:</label> <span id="result2">&nbsp;</span>
    </div>

    <h2>websocket</h2>

    <div>
        <span id="ws_text">&nbsp;</span>
        <br />
        <input type="text" id="send_input" />
        <button id="send_button">Send</button>
    </div>

</body>
</html>
  • 服务器支持host静态页面资源
  • 服务器支持前端页面的热加载
  • 服务器支持http和websocket两种方式的接口
  • 服务器可以添加头格式来开启跨域(CORS)加载支持
  • 服务器支持websocket单一连接发消息和广播消息
  • 服务器支持管理websocket的session
  • 需要手动设置loop polling的时间间隔
  • 可以自定义静态页面根路径,注册和解注册自定义api函数回调
  • 某些变量必须声明定义成全局或者静态变量
  • 如果需要回传json格式,可以序列化成字符串,在前端解析

http客户端

http_client.h

#pragma once
#include <string>
#include <functional>
#include "../common/mongoose.h"

// 此处必须用function类,typedef再后面函数指针赋值无效
using ReqCallback = std::function<void (std::string)>;

class HttpClient
{
public:
	HttpClient() {}
	~HttpClient() {}

	static void SendReq(const std::string &url, ReqCallback req_callback);
	static void OnHttpEvent(mg_connection *connection, int event_type, void *event_data);
	static int s_exit_flag;
	static ReqCallback s_req_callback;
};

http_client.cpp

#include "http_client.h"

// 初始化client静态变量
int HttpClient::s_exit_flag = 0;
ReqCallback HttpClient::s_req_callback;

// 客户端的网络请求响应
void HttpClient::OnHttpEvent(mg_connection *connection, int event_type, void *event_data)
{
	http_message *hm = (struct http_message *)event_data;
	int connect_status;

	switch (event_type) 
	{
	case MG_EV_CONNECT:
		connect_status = *(int *)event_data;
		if (connect_status != 0) 
		{
			printf("Error connecting to server, error code: %d\n", connect_status);
			s_exit_flag = 1;
		}
		break;
	case MG_EV_HTTP_REPLY:
	{
		printf("Got reply:\n%.*s\n", (int)hm->body.len, hm->body.p);
		std::string rsp = std::string(hm->body.p, hm->body.len);
		connection->flags |= MG_F_SEND_AND_CLOSE;
		s_exit_flag = 1; // 每次收到请求后关闭本次连接,重置标记
        
		// 回调处理
		s_req_callback(rsp);
	}
		break;
	case MG_EV_CLOSE:
		if (s_exit_flag == 0) 
		{
			printf("Server closed connection\n");
			s_exit_flag = 1;
		};
		break;
	default:
		break;
	}
}


// 发送一次请求,并回调处理,然后关闭本次连接
void HttpClient::SendReq(const std::string &url, ReqCallback req_callback)
{
	// 给回调函数赋值
	s_req_callback = req_callback;
	mg_mgr mgr;
	mg_mgr_init(&mgr, NULL);
	auto connection = mg_connect_http(&mgr, OnHttpEvent, url.c_str(), NULL, NULL);
	mg_set_protocol_http_websocket(connection);

	printf("Send http request %s\n", url.c_str());

	// loop
	while (s_exit_flag == 0)
		mg_mgr_poll(&mgr, 500);

	mg_mgr_free(&mgr);
}

main.cpp

#include <iostream>
#include "http_client.h"

void handle_func(std::string rsp)
{
	// do sth according to rsp
	std::cout << "http rsp1: " << rsp << std::endl;
}

int main()
{
	// 拼完整url,带参数,暂时只写了GET请求
	std::string url1 = "http://127.0.0.1:7999/api/hello";
	HttpClient::SendReq(url1, handle_func);
	
	std::string url2 = "http://127.0.0.1:7999/api/fun2";
	HttpClient::SendReq(url2, [](std::string rsp) { 
		std::cout << "http rsp2: " << rsp << std::endl; 
	});

	system("pause");

	return 0;
}
  • client每次请求都是一个独立的请求
  • 请求函数中加入回调用于处理网络返回

测试

可以用浏览器、或者其他工具提交url,查看网络请求返回

GET

请求 

http://localhost:7999/api/hello

结果

{ "result": welcome to httpserver }

POST

请求

http://localhost:7999/api/sum?n1=20&n2=18

结果

{ "result": 38 }

websocket的测试可以用工具也可以用内嵌网页,查看连接状态以及双向发消息

网页截图

源码

csdn:demo

github: demo

 

展开阅读全文

没有更多推荐了,返回首页