1. beast网络库搭建http服务器

1.1 简介

通过asio来实现http服务器也需要严格服从http报文头的格式,其实http报文头的格式就是为了避免我们之前提到的粘包现象,告诉服务器一个数据包的开始和结尾,并在包头里标识请求的类型如get或post等信息。一个标准的HTTP报文头通常由请求头响应头两部分组成。

http请求头格式:

1
2
3
4
5
6
GET /index.html HTTP/1.1     //包含用于描述请求类型、要访问的资源以及所使用的HTTP版本的信息。
Host: www.example.com //指定被请求资源的主机名或IP地址和端口号。
Accept: text/html, application/xhtml+xml //指定客户端能够接收的媒体类型列表,用逗号分隔,例如 text/plain, text/html。
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0 //客户端使用的浏览器类型和版本号,供服务器统计用户代理信息
Cookie: sessionid=abcdefg1234567 //如果请求中包含cookie信息,则通过这个字段将cookie信息发送给Web服务器。
Connection: keep-alive //表示是否需要持久连接(keep-alive)

http响应头格式:

1
2
3
4
5
6
HTTP/1.1 200 OK            //包含协议版本、状态码和状态消息
Content-Type: text/html; charset=UTF-8 //响应体的MIME类型
Content-Length: 1024 //响应体的字节数
Set-Cookie: sessionid=abcdefg1234567; HttpOnly; Path=/ //服务器向客户端发送cookie信息时使用该字段
Server: Apache/2.2.32 (Unix) mod_ssl/2.2.32 OpenSSL/1.0.1e-fips mod_bwlimited/1.4 //服务器类型和版本号
Connection: keep-alive //表示是否需要保持长连接(keep-alive)

1.2 http服务器实现

首先需要重新定义boost库的命名空间,并且创建了两个函数,方便后面对程序进行检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;

//命名一个作用域,程序的声明
namespace my_program_state { //定义了两个全局函数
//统计对端请求的次数
std::size_t request_count() {
static std::size_t count = 0; //这个值初始化一次,之后访问都是之前的数据
return ++count;
}
//得到现在的时间戳
std::time_t now() {
return std::time(0);
}
}

主函数:负责初始化ip和端口的初始化,启动上下文服务轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
try {
auto const address = net::ip::make_address("127.0.0.1");
unsigned short port = static_cast<unsigned short>(8080); //static_cast是静态转换类型
net::io_context ioc{ 1 }; //初始化一个io_context,最多支持一个线程去调度
tcp::acceptor acceptor{ ioc,{address,port} };
tcp::socket socket{ ioc };
http_server(acceptor, socket); //调用http_server函数,进行http连接
ioc.run();
}
catch (std::exception& e) {
std::cerr << "Error:" << e.what() << std::endl;
return EXIT_FAILURE;
}
}

http_server函数实现:http_server中添加了异步接收连接的逻辑,当有新的连接到来时创建http_connection类型的智能指针,并且启动服务,新连接监听对端接收和发送数据。然后http_server继续监听对端的新连接。

1
2
3
4
5
6
7
8
void http_server(tcp::acceptor& acceptor, tcp::socket& socket) {
acceptor.async_accept(socket, [&](boost::system::error_code ec) {
if (!ec) {
std::make_shared<http_connection>(std::move(socket))->start(); //创建一个http_connection的共享指针,创建好就直接启动
}
http_server(acceptor, socket); //继续监听连接请求
});
}

http_connection连接类的实现:负责读取对端发来数据的请求头,并且设置响应头,通过异步方式发送回给对端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
class http_connection :public std::enable_shared_from_this<http_connection> {
public:
//传入一个socket,构造连接,初始化socket_是通过移动构造完成的
http_connection(tcp::socket socket):socket_(std::move(socket)) {
}
void start() {
//读请求
read_request();
//判断超时,保证一个链接不要处理太长时间
check_deadline();
}
private:
tcp::socket socket_;
beast::flat_buffer buffer_{ 8192 }; //接收数据的缓存
http::request<http::dynamic_body>request_; //定义一个请求
http::response<http::dynamic_body>response_; //定义一个回应

//steady_timer是boost提供的一种定时器类型
net::steady_timer deadline_{
//初始化调度器
socket_.get_executor(), std::chrono::seconds(60) //初始化一个时钟,60秒调度一次
};
void read_request() { //读请求函数实现
//创建http_connection的一个智能指针,与外部使用的http_connection智能指针是共享引用计数的
auto self = shared_from_this(); //不能用make_shared创建,因为通过这种方法创建出来的http_connection不与外面使用的共享引用计数
//read_request() 函数中的 http::async_read 调用是为了从套接字 socket_ 中异步读取数据到 buffer_ 中,然后解析这些数据到request_对象中。request_ 通常是一个HTTP请求解析器对象,用于解析HTTP请求。
//参数:当前HTTP连接的套接字;存储读取的数据的缓冲区;回调函数,当异步读取操作完成时会被调用
http::async_read(socket_, buffer_, request_, [self](beast::error_code ec, std::size_t bytes_transferred) { //用作async_read的回调函数,参数:错误码;传输的字节数
boost::ignore_unused(bytes_transferred); //使用Boost库中的ignore_unused宏来告诉编译器忽略未使用的bytes_transferred变量,避免编译警告
if (!ec) {
self->process_request(); //处理请求
}
});
}
//检测定时器,判断该定时器有无超时,超时就关闭socket
void check_deadline() {
auto self = shared_from_this(); //创建http_connection的一个智能指针,与外部使用的http_connection智能指针是共享引用计数的
//异步等待,60秒后,会执行匿名函数
deadline_.async_wait([self](boost::system::error_code ec) {
if (!ec) {
self->socket_.close(ec); //捕获的智能指针self,保证该匿名函数执行期间,http_connection没有被停止掉
}
});
}
//处理请求
void process_request() {
response_.version(request_.version()); //回应的版本,response_是回应头,回应的版本是响应头使用的版本
response_.keep_alive(false); //false表示短连接,true表示长连接
switch (request_.method()) { //检测请求类型
case http::verb::get:
response_.result(http::status::ok); //返回一个状态
response_.set(http::field::server, "Beast"); //回复的数据类型
create_response(); //创建get类型的请求回应
break;
case http::verb::post:
response_.result(http::status::ok); //返回一个状态
response_.set(http::field::server, "Beast"); //回复的数据类型
create_post_response(); //创建post类型的请求回应
break;
default:
response_.result(http::status::bad_request); //返回一个状态
response_.set(http::field::content_type, "text/plain"); //回复的是纯文本类型content_type
beast::ostream(response_.body()) << "Invalid request-method'" << std::string(request_.method_string()) << "'";
break;
}
//无论请求是哪种,最后都需要回应请求
write_response();
}
void create_response() {
if (request_.target() == "/count") { //如果路由是/count,就进行统计
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body()) << "<html>\n"
<< "<head><title>Request count </title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>There have been "
<< my_program_state::request_count()
<< "requests so far.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else if (request_.target() == "/time") { //如果路由是/time,就获取当前的时间戳
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body()) << "<html>\n"
<< "<head><title>Cyrrent time </title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>The current time is "
<< my_program_state::now()
<< "seconds since the epoch..</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else { //没有找到路由(路径地址)
response_.result(http::status::not_found);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body()) << "File not found\r\n";
}
}
//用于异步发送HTTP响应
void write_response() {
auto self = shared_from_this();
response_.content_length(response_.body().size()); //设置HTTP响应的Content-Length 头部字段。这个字段告诉客户端响应体的大小
//启动一个异步写操作,将HTTP响应发送到客户端。参数:当前HTTP连接的套接字;包含HTTP响应的HTTP消息对象;回调函数,当异步写操作完成时会被调用
http::async_write(socket_, response_, [self](beast::error_code ec, std::size_t) {
self->socket_.shutdown(tcp::socket::shutdown_send); //首先调用 shutdown 方法关闭套接字的发送部分。参数表示关闭发送方向,但不关闭接收方向
self->deadline_.cancel(); //取消任何设置的超时
});
}
void create_post_response() {
if (request_.target() == "/email") {
auto& body = this->request_.body(); //获取HTTP请求的主体部分。
auto body_str = boost::beast::buffers_to_string(body.data()); //将请求主体的缓冲区数据转换为字符串
std::cout << "receive body is " << body_str << std::endl;
this->response_.set(http::field::content_type, "text/json"); //设置HTTP响应的 Content-Type 头部字段为 "text/json"
Json::Value root; //创建一个JSON值对象 root,用于构建响应的JSON结构
Json::Reader reader; //创建一个JSON读取器对象 reader
Json::Value src_root; //创建一个源JSON值对象 src_root,用于存储解析后的JSON数据
//使用 reader 解析请求主体字符串 body_str 到 src_root,并将解析成功与否存储在 parse_success 变量中
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) { //如果解析失败
std::cout << "Failed to parse Json data" << std::endl;
root["error"] = 1001; //在响应的JSON对象中设置错误代码
std::string jsonstr = root.toStyledString(); //将响应的JSON对象转换为格式化的字符串(序列化)
beast::ostream(response_.body()) << jsonstr; //将JSON字符串写入响应主体
return;
}
//如果解析成功,从解析后的JSON对象中获取 "email" 字段的值
auto email = src_root["email"].asString();
std::cout << "email is " << email << std::endl;
root["error"] = 0; //在响应的JSON对象中设置错误代码为0,表示成功
root["email"] = src_root["email"]; //将电子邮件地址添加到响应的JSON对象中
root["mag"] = "receive email post success"; //添加一条消息到响应的JSON对象中
std::string jsonstr = root.toStyledString(); //将响应的JSON对象转换为格式化的字符串(序列化)
beast::ostream(response_.body()) << jsonstr; ////将JSON字符串写入响应主体
}
else { //如果请求的目标不是 "/email"
response_.result(http::status::not_found); //设置HTTP响应的状态为 404 Not Found。
response_.set(http::field::content_type, "text/plain"); //设置HTTP响应的 Content-Type 头部字段为 "text/plain
beast::ostream(response_.body()) << "File not found\r\n"; //将 "File not found" 消息写入响应主体
}
}
};

最后启动http的服务器,如果本地浏览器输入127.0.0.1:8080/count,进入界面后不断刷新,就可以看到严格计数的效果;如果在本地浏览器输入127.0.0.1:8080/time,也可以获得当前的时间戳;对于实现的post类型请求,需要结合软件完成检测。

2. beast网络库实现websocket服务器

2.1 简介

对于如何使用Beast库实现一个WebSocket服务器,以及如何处理不同类型的请求。具体来说,这句话包含了以下几个要点:

  • WebSocket协议和HTTP协议的关系:WebSocket是一种长连接协议,允许服务器和客户端之间进行双向通信。虽然它是在HTTP协议之上建立的,但它在建立连接后升级为WebSocket协议。

  • 请求的区分:当在浏览器中输入一个以ws://开头的URL(例如ws://127.0.0.1:9501),浏览器会发起一个WebSocket请求,目标是本地服务器的9501端口。

  • Beast库的作用:Beast库提供了处理WebSocket协议的功能。它允许我们在一个HTTP服务器的基础上,通过协议升级的方式来处理WebSocket请求。

  • 请求处理逻辑:当服务器收到一个请求时,需要判断该请求是普通的HTTP请求还是WebSocket请求。如果是WebSocket请求,服务器将升级协议并处理该请求;如果是普通的HTTP请求,则按HTTP请求处理。

2.2 websocket服务器实现

主函数实现:负责初始化工作任务需要的内容,并启动上下文服务。

1
2
3
4
5
6
7
int main()
{
net::io_context ioc; //初始化一个上下文
WebSocketServer server(ioc, 10086); //通过该ioc构建一个server,端口是10086
server.StartAccept(); //执行WebSocketServer的StartAccept()函数,server接收新的连接
ioc.run(); //ioc跑起来,启动事件服务
}

管理类ConnectionMgr实现:在类中创建了一个无序的map容器_map_cons,key值是转化为字符串的uuid值,value值是对应的Connection类型的智能指针。并且定义了两个函数,对容器_map_cons对连接进行加入和删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//---------------------头文件-------------------------------
class ConnectionMgr
{
public:
static ConnectionMgr& GetInstance(); //实现单例模式
void AddConnection(std::shared_ptr<Connection>conptr); //加入连接
void RmvConnection(std::string); //移出连接
private:
ConnectionMgr(const ConnectionMgr&) = delete; //去除拷贝构造函数
ConnectionMgr& operator=(const ConnectionMgr&) = delete; //去除赋值构造函数
ConnectionMgr();
boost::unordered_map<std::string, std::shared_ptr<Connection>>_map_cons; //无序map,管理连接
};
//-----------------------函数初始化---------------------------
ConnectionMgr& ConnectionMgr::GetInstance() { //C++11以上的版本,通过这种方式实现单例模式
static ConnectionMgr instance; //定义一个局部变量
return instance;
}
void ConnectionMgr::AddConnection(std::shared_ptr<Connection>conptr) {
_map_cons[conptr->GetUid()] = conptr; //将连接的uid和对应的连接通过map来进行管理
}
void ConnectionMgr::RmvConnection(std::string id) {
_map_cons.erase(id); //通过id来删除
}
ConnectionMgr::ConnectionMgr() {
}

WebSocketServer类实现:负责不断监听对端的连接,当与对端建立连接后,就调用Connection类的AsyncAccept函数,将协议升级为了websocket。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//----------------------------头文件----------------------------------
class WebSocketServer
{
public:
WebSocketServer(const WebSocketServer&) = delete; //去除拷贝构造
WebSocketServer& operator = (const WebSocketServer&) = delete; //去除赋值构造
WebSocketServer(net::io_context& ioc, unsigned short port);
void StartAccept(); //接收连接,是TCP上的接收连接
private:
net::ip::tcp::acceptor _acceptor; //接收连接的一个接收器
net::io_context& _ioc;
};
//-------------------------------函数实现-------------------------------------
WebSocketServer::WebSocketServer(net::io_context& ioc, unsigned short port) :_ioc(ioc),
_acceptor(ioc, net::ip::tcp::endpoint(net::ip::tcp::v4(), port)) {
std::cout << "Server start on port: " << port << std::endl; //开始连接
}
void WebSocketServer::StartAccept() {
auto con_ptr = std::make_shared<Connection>(_ioc); //创建一个Connection类型的智能指针
//接收连接
_acceptor.async_accept(con_ptr->GetSocket(), [this, con_ptr](error_code err) { //GetSocket()返回的是connection最低层的socket(tcp的socket)
try {
if (!err) { //没有错误
con_ptr->AsyncAccept(); //相当于升级了,将协议升级为了websocket
}
else {
std::cout << "acceptor async_accept failed, err is " << err.what() << std::endl; //连接失败,打印原因
}
StartAccept(); //server接收新的连接
}
catch (std::exception& exp) {
std::cout << "async_accept error is " << exp.what() << std::endl;
}
});
}

Connection类实现:负责将协议生成websocketm,当与对端建立成功后,异步接收数据和异步发送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//---------------------------------头文件-------------------------------------
class Connection :public std::enable_shared_from_this<Connection> { //连接通过智能指针去管理,允许从内部去构造智能指针,并且与外部使用的智能指针共享引用计数
public:
Connection(net::io_context& ioc);
std::string GetUid(); //返回uid
//connect内部是管理websocket,而websocket底层是通过tcp来实现的,所以它底层有一个socket
net::ip::tcp::socket& GetSocket(); //该函数返回该底层的socket(外面可能会用到)
void AsyncAccept(); //在tcp层面建立好连接后,还要在websocket层面做一个升级,升级就可以调用这个异步的连接函数进行升级
void Start(); //接收对端的数据的,进行收发的
void AsyncSend(std::string msg);
private:
std::unique_ptr<stream<tcp_stream>>_ws_ptr; //用unique_ptr管理外部websocket
std::string _uuid; //有唯一的id
net::io_context& _ioc;
flat_buffer _recv_buffer; //存储接收的数据
std::queue<std::string>_send_que; //发送队列
std::mutex _send_mtx; //发送的一个锁
};
//---------------------------------函数实现--------------------------------------
Connection::Connection(net::io_context& ioc):_ioc(ioc),
_ws_ptr(std::make_unique<stream<tcp_stream>>(make_strand(ioc))) //构造一个stream<tcp_stream>类型的智能指针。通过上下文构造一个strand执行器,则这里的执行器与上下文的执行器就是同一个了
{
//生成唯一的uuid
boost::uuids::random_generator generator; //生成generator
boost::uuids::uuid uuid = generator(); //通过generator来生成唯一的uuid
_uuid = boost::uuids::to_string(uuid); //为了存储该uuid,需要转换为string类型

}
std::string Connection::GetUid(){
return _uuid;
}
net::ip::tcp::socket& Connection::GetSocket() {
//socket()是会返回websocket内部管理的最底层的socket的引用,只要的wensocke不被释放,返回的socket也不会被释放
return boost::beast::get_lowest_layer(*_ws_ptr).socket(); //返回智能指针_ws_ptr所指向的websocket的最底层(是tcp类型的socket)
}

//异步的接收
void Connection::AsyncAccept() {
//生成一个自己的智能指针,通过这种方式生成的智能指针和其它的共享指针共享引用计数
auto self = shared_from_this(); //其它智能指针可能也在管理connection,为了不与它们同步引用计数,所以需要shared_from_this
//websocket异步接收连接,结果相当于是在tcp的基础上,将协议生成websocket
_ws_ptr->async_accept([self](boost::system::error_code err) { //防止回调函数在没有调用之前,connection被智能指针释放掉,所以需要它的引用计数+1
try {
if (!err) { //没有错误
ConnectionMgr::GetInstance().AddConnection(self); //添加连接,将self智能指针加到管理类来管理
self->Start(); //接收读写
}
else {
std::cout << "websocket accept failed,err is" << err.what() << std::endl;
}
}
catch (std::exception& exp) {
std::cout << "websocket async accept exception is " << exp.what();
}
});
}

void Connection::Start() {
auto self = shared_from_this();
//异步读
_ws_ptr->async_read(_recv_buffer, [self](error_code err, std::size_t buffer_bytes) {
try {
if (err) {
std::cout << "websocket async read error is " << err.what() << std::endl; //打印错误信息
ConnectionMgr::GetInstance().RmvConnection(self->GetUid()); //将该连接从管理者移出
return;
}
self->_ws_ptr->text(self->_ws_ptr->got_text()); //设置传输的类型,默认是对方发什么,就回什么类型
std::string recv_data = boost::beast::buffers_to_string(self->_recv_buffer.data()); //存储收到的数据,并将收到的buffer类型数据转成string类型
self->_recv_buffer.consume(self->_recv_buffer.size()); //清空_recv_buffer,为下次接收准备
std::cout << "websocket recvive msg is " << recv_data << std::endl; //打印接收的数据
//发送操作
self->AsyncSend(std::move(recv_data)); //异步发送,为了减少拷贝,用move()操作
self->Start(); //发送完,就继续监听对方发送数据
}
catch (std::exception& exp) { //接收异常
std::cout << "exception is " << exp.what() << std::endl; //打印错误信息
ConnectionMgr::GetInstance().RmvConnection(self->GetUid()); //将该连接从管理者移出
}
});
}

void Connection::AsyncSend(std::string msg) {
{
std::lock_guard<std::mutex>lck_guard(_send_mtx); //对队列加锁
int que_len = _send_que.size(); //取出队列的长度
_send_que.push(msg); //往队列添加发送的数据
if (que_len > 0) { //如果没有放入队列之前,队列长度>0,说明之前数据没有发送完,直接退出
return;
}
} //执行到这里的时候,进行析构,自动解锁
auto self = shared_from_this();
//创建一个buffer,参数是消息的首地址,长度和lambda表达式(捕获发送对方的结果)
_ws_ptr->async_write(boost::asio::buffer(msg.c_str(), msg.length()), [self](error_code err, std::size_t nsize) { //错误码 和 没有发生错误情况下,发送了多少
try {
if (err) {
std::cout << "async_send err is " << err.what() << std::endl; //打印错误
ConnectionMgr::GetInstance().RmvConnection(self->GetUid()); //通过uid移出连接
return;
}

std::string send_msg;
{ //局部作用域
std::lock_guard<std::mutex>lck_guard(self->_send_mtx); //对队列加锁
self->_send_que.pop(); //弹出队列首元素(上面的异步发送,是发送完成才回执行到这里),队首的元素就是刚刚发送完毕的
if (self->_send_que.empty()) { //再判断队列是否为空
return; //为空就返回
}
//不为空,说明还有数据,需要继续发送
send_msg = self->_send_que.front(); //把队首元素进行一个拷贝
} //自动解锁
self->AsyncSend(std::move(send_msg)); //异步发送,这里的移动操作字符串对于系统来说,差别不大,如果是结构体,用move效果会明显一点
}
catch (std::exception& exp) {
std::cout << "async_send exception is " << err.what() << std::endl; //打印错误
ConnectionMgr::GetInstance().RmvConnection(self->_uuid); //通过uid将异常连接移出
}
});
}

2.3 执行效果

1.启动服务器后,会跳出如下页面,这也表示服务器开始监听对端发来的连接

2.在WebSocket的一个在线测试网站进行测试

3.点击发送,接收服务器发送来的信息

4.服务器终端情况

3. windows配置和使用grpc

3.1 简介

当在windows环境配置好grpc,接下来是使用visual studio配置grpc,可以将之前编译好的库配置到项目中来完成grpc通信。

3.2 项目配置

1.创建Grpc-Server项目,在项目的根目录下创建一个名字为demo.proto的文件,编写程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";      //声明的proto的版本

package hello;

//给外部提供服务的,可以生成一个Greeter对象,外部可以调用这个接口,Greeter就可以返回对应的消息
service Greeter{
rpc SayHello(HelloRequest) returns(HelloReply){} //定义一个接口(一个请求,一个回包)
}

//定义一个请求的消息体
message HelloRequest{
string message = 1; //第一个是string类型的
}

//定义一个回复的消息体
message HelloReply{
string message = 1;
}

2.在demo.proto所在文件打开Powershell窗口(终端好像也可),然后利用grpc编译后生成的proc.exe生成proto的头文件和源文件,即执行如下命令生成demo.grpc.pb.hdemo.grpc.pb.cc文件(这两个文件是为grpc服务的)。

1
C:\cppsoft\grpc\visualpro\third_party\protobuf\Debug\protoc.exe  -I="." --grpc_out="." --plugin=protoc-gen-grpc="C:\cppsoft\grpc\visualpro\Debug\grpc_cpp_plugin.exe" "demo.proto"

命令解释:

  • C:\cppsoft\grpc\visualpro\third_party\protobuf\Debug\protoc.exe:是存放protoc.exe所在的路径,也可以将其配置到环境变量,然后直接使用protoc.exe就行

  • -I="." :指定 demo.proto所在的路径为当前路径

  • --grpc_out="." :表示生成的pb.h和pb.cc文件的输出位置为当前目录

  • --plugin=protoc-gen-grpc="C:\cppsoft\grpc\visualpro\Debug\grpc_cpp_plugin.exe":表示要用到的插件是protoc-gen-grpc,位置在C:\cppsoft\grpc\visualpro\Debug\grpc_cpp_plugin.exe

  • "demo.proto":要编译的proto文件

3.因为要序列化数据,所以需要生成grpc类需要的pb文件,即在demo.proto所在目录下打开powershell窗口,执行如下命令,就会生成demo.pb.h和demo.pb.cc文件(这两个文件是为消息服务的)。

1
C:\cppsoft\grpc\visualpro\third_party\protobuf\Debug\protoc.exe --cpp_out=. "demo.proto"

4.为项目中配置grpc库的包含目录和库目录。先配置Debug版本,方便调试。

右键vs里面的项目,选择:属性 —> c/c++ — > 常规 —> 附加包含目录 —> 编辑

点击编辑后,在弹出的窗口中添加如下文件目录(根据自己存放的文件目录添加):

1
2
3
4
5
C:\cppsoft\grpc\include
C:\cppsoft\grpc\third_party\protobuf\src
C:\cppsoft\grpc\third_party\abseil-cpp
C:\cppsoft\grpc\third_party\address_sorting\include
C:\cppsoft\grpc\third_party\re2

5.再配置库路径, 选择:链接器 —> 常规 —> 附加库目录 —> 编辑

点击编辑后,在弹出的窗口添加以下路径(根据自己存放的文件目录添加):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C:\cppsoft\grpc\visualpro\third_party\re2\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\types\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\synchronization\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\status\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\random\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\flags\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\debugging\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\container\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\hash\Debug
C:\cppsoft\grpc\visualpro\third_party\boringssl-with-bazel\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\numeric\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\time\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\base\Debug
C:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\strings\Debug
C:\cppsoft\grpc\visualpro\third_party\protobuf\Debug
C:\cppsoft\grpc\visualpro\third_party\zlib\Debug
C:\cppsoft\grpc\visualpro\Debug
C:\cppsoft\grpc\visualpro\third_party\cares\cares\lib\Debug

6.配置好库目录后,还要将要使用的库链接到项目。选择:链接器 —> 输入 —> 附加依赖项 —> 编辑

点击编辑后,在弹出的窗口添加依赖的库名字(以下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
libprotobufd.lib
gpr.lib
grpc.lib
grpc++.lib
grpc++_reflection.lib
address_sorting.lib
ws2_32.lib
cares.lib
zlibstaticd.lib
upb.lib
ssl.lib
crypto.lib
absl_bad_any_cast_impl.lib
absl_bad_optional_access.lib
absl_bad_variant_access.lib
absl_base.lib
absl_city.lib
absl_civil_time.lib
absl_cord.lib
absl_debugging_internal.lib
absl_demangle_internal.lib
absl_examine_stack.lib
absl_exponential_biased.lib
absl_failure_signal_handler.lib
absl_flags.lib
absl_flags_config.lib
absl_flags_internal.lib
absl_flags_marshalling.lib
absl_flags_parse.lib
absl_flags_program_name.lib
absl_flags_usage.lib
absl_flags_usage_internal.lib
absl_graphcycles_internal.lib
absl_hash.lib
absl_hashtablez_sampler.lib
absl_int128.lib
absl_leak_check.lib
absl_leak_check_disable.lib
absl_log_severity.lib
absl_malloc_internal.lib
absl_periodic_sampler.lib
absl_random_distributions.lib
absl_random_internal_distribution_test_util.lib
absl_random_internal_pool_urbg.lib
absl_random_internal_randen.lib
absl_random_internal_randen_hwaes.lib
absl_random_internal_randen_hwaes_impl.lib
absl_random_internal_randen_slow.lib
absl_random_internal_seed_material.lib
absl_random_seed_gen_exception.lib
absl_random_seed_sequences.lib
absl_raw_hash_set.lib
absl_raw_logging_internal.lib
absl_scoped_set_env.lib
absl_spinlock_wait.lib
absl_stacktrace.lib
absl_status.lib
absl_strings.lib
absl_strings_internal.lib
absl_str_format_internal.lib
absl_symbolize.lib
absl_synchronization.lib
absl_throw_delegate.lib
absl_time.lib
absl_time_zone.lib
absl_statusor.lib
re2.lib

当完成这些配置,就可以通过grpc来进行服务器和客户端的通信了。

4. grpc通信

4.1 简介

gRPC(Remote Procedure Call)是一种高性能、开源的远程过程调用(RPC)框架。它能够在不同的环境中进行跨语言的通信,并且使用HTTP/2作为其传输协议,提供诸如负载均衡、跟踪、健康检查和认证等功能。

gRPC通信过程包括服务器端和客户端的代码,其中通过协议定义文件(proto文件)来定义消息格式和服务接口。

4.2 proto文件

proto文件定义了通信协议的消息格式和服务接口。在这里的proto文件定义了一个名为hello的包,包含一个Greeter服务和两个消息类型HelloRequestHelloReply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";      //声明的proto的版本

package hello;

//给外部提供服务的,可以生成一个Greeter对象,外部可以调用这个接口,Greeter就可以返回对应的消息
service Greeter{
rpc SayHello(HelloRequest) returns(HelloReply){} //定义一个接口(一个请求,一个回包)
}

//定义一个请求的消息体
message HelloRequest{
string message = 1; //第一个是string类型的
}

//定义一个回复的消息体
message HelloReply{
string message = 1;
}

4.3 服务器实现

服务器代码实现了proto文件中定义的Greeter服务。服务器启动后,监听指定的端口,等待客户端的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//使用grpc的一些作用域
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext; //server上下文
using grpc::Status;
using hello::HelloRequest;
using hello::HelloReply;

using hello::Greeter; //要用到的一个服务

//final表示终极的继承,GreeterServicelmpl继承public Greeter::Service后,其它的类就不能再继承了
class GreeterServicelmpl final :public Greeter::Service {
//Status是返回的一个状态,grpc服务给别人提供调用的接口,都会返回一个状态
::grpc::Status SayHello(::grpc::ServerContext* ccontext, const::hello::HelloRequest* request, ::hello::HelloReply* response) {
std::string prefix("llfc grpc server has received:");
response->set_message(prefix + request->message()); //修改一下回应(进行拼接)
return Status::OK; //返回一个状态
} //重写了父类Greeter的一个Service函数接口
};

//启动服务
void RunServer() {
std::string server_address("0.0.0.0:50051");
GreeterServicelmpl service;
ServerBuilder builder; //创建服务的时候,需要ServerBuilder(规则)
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); //绑定端口。
builder.RegisterService(&service); //注册服务,将该服务注册给builder。服务就可以在后台执行了

std::unique_ptr<Server>server(builder.BuildAndStart()); //将端口和服务都绑定好的builder传递给server,
std::cout << "Server listening on " << server_address << std::endl;
server->Wait(); //阻塞,底层轮询
}

int main()
{
RunServer();
}

补:127.0.0.10.0.0.0的区别:127.0.0.0是本地回路地址,不能网络,如果是本地客户端请求本地服务器,是可以的,但其它客户端访问不到。

4.4 客户端实现

客户端代码创建一个与服务器通信的通道,通过这个通道,客户端可以调用服务器提供的服务(如SayHello)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using grpc::ClientContext;     //Clinent的上下文
using grpc::Channel; //发送消息,需要通道,通过该通道与服务器通信
using grpc::Status;
using hello::HelloReply;
using hello::HelloRequest;
using hello::Greeter;

//客户端不用继承服务,直接写即可
class FCClient {
public:
//参数是通道类型,利用通道与服务端通信
FCClient(std::shared_ptr<Channel>channel) :stub_(Greeter::NewStub(channel)) {
}
std::string SayHello(std::string name) {
ClientContext context; //构造客户端的上下文
HelloReply reply; //回复
HelloRequest request; //请求
request.set_message(name); //设置消息

//设置好请求,下面是发过去
//客户端发送消息,把request字符串通过protobuf序列化发送给服务器,服务器把数据修改好后,传回来,客户端收到数据后存到reply里
Status status = stub_->SayHello(&context, request, &reply); //调用的是服务端的SayHello,对reply进行了修改,所以这里第三个参数传的是地址
if (status.ok()) {
return reply.message(); //收到对方回应,如果是正常的就返回字符串即可
}
else {
return "failure " + status.error_message();
}
}
private:
std::unique_ptr<Greeter::Stub>stub_; //stub_可以理解为客户端
};

int main()
{
//进行通信就必须创建channel管道,相当于是asio里面的端点,与谁通信
auto channel = grpc::CreateChannel("127.0.0.1:50051", grpc::InsecureChannelCredentials());
FCClient client(channel);
//客户端调用了SayHello函数,把参数(字符串消息)传给了服务器,返回服务器的发过来的数据
std::string result = client.SayHello("hello lxx93.online!");
printf("get result [%s]\n", result.c_str()); //打印获取的消息
}

4.5 通信过程

1.定义服务接口和消息格式

  • 在proto文件中,定义了一个名为Greeter的服务,包含一个RPC方法SayHello,该方法接受一个HelloRequest消息并返回一个HelloReply消息。

2.生成代码

  • 使用protoc编译器从proto文件生成C++代码,包括消息类(如HelloRequestHelloReply)和服务接口类(如Greeter::Service)

3.实现服务器逻辑

  • 在服务器端,继承生成的Greeter::Service类,并实现SayHello方法。这个方法接收客户端的请求(HelloRequest),处理后返回响应(HelloReply)。

4.启动服务器

  • 服务器代码使用ServerBuilder配置和启动gRPC服务器,并在指定端口上监听客户端请求。

5.客户端调用

  • 客户端创建一个通道,通过这个通道与服务器通信。客户端创建一个Greeter::Stub对象,这是一个客户端代理,通过它调用服务器端的SayHello方法。
  • 在客户端调用SayHello方法时,客户端将HelloRequest消息序列化并发送给服务器。服务器接收请求后,处理并返回HelloReply消息。客户端接收响应并解码得到结果。

4.6 为什么客户端可以调用服务端的函数

客户端并不直接调用服务端的函数,而是通过gRPC框架生成的客户端存根(stub)来间接调用。客户端调用存根的SayHello方法,gRPC框架负责处理网络通信,将请求发送到服务器,并接收服务器的响应。

步骤如下(结合上面编写的服务器和客户端):

  1. 客户端构建请求:创建并填充HelloRequest消息。

  2. 发送请求:通过调用stub_->SayHello将请求发送到服务器。

  3. 服务器处理请求:服务器接收请求,调用GreeterServiceImpl::SayHello方法进行处理,并构建HelloReply响应。

  4. 发送响应:服务器将响应发送回客户端。

  5. 客户端接收响应:客户端接收并解析HelloReply消息,从而得到服务器处理后的结果