Skip to content

Commit 562ef79

Browse files
committed
Implement a new HTTP server/client sample
1 parent 2382a49 commit 562ef79

19 files changed

+1119
-0
lines changed

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ endif()
3434
option(GRAPHQL_BUILD_SCHEMAGEN "Build the schemagen tool." ON)
3535
option(GRAPHQL_BUILD_CLIENTGEN "Build the clientgen tool." ON)
3636
option(GRAPHQL_BUILD_TESTS "Build the tests and sample schema library." ON)
37+
option(GRAPHQL_BUILD_HTTP_SAMPLE "Build the HTTP client sample using C++20 coroutines with Boost.Beast/Boost.Asio." OFF)
3738

3839
if(GRAPHQL_BUILD_SCHEMAGEN)
3940
list(APPEND VCPKG_MANIFEST_FEATURES "schemagen")
@@ -47,6 +48,10 @@ if(GRAPHQL_BUILD_TESTS)
4748
list(APPEND VCPKG_MANIFEST_FEATURES "tests")
4849
endif()
4950

51+
if(GRAPHQL_BUILD_HTTP_SAMPLE)
52+
list(APPEND VCPKG_MANIFEST_FEATURES "http-sample")
53+
endif()
54+
5055
if(GRAPHQL_BUILD_SCHEMAGEN AND GRAPHQL_BUILD_CLIENTGEN)
5156
option(GRAPHQL_UPDATE_SAMPLES "Regenerate the sample schema sources whether or not we're building the tests." ON)
5257

samples/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ add_subdirectory(client)
77
add_subdirectory(learn)
88
add_subdirectory(today)
99
add_subdirectory(validation)
10+
11+
if(GRAPHQL_BUILD_HTTP_SAMPLE)
12+
find_package(Boost)
13+
if(Boost_FOUND)
14+
add_subdirectory(proxy)
15+
endif()
16+
endif()

samples/learn/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ target_link_libraries(learn_star_wars PRIVATE
2020
star_wars
2121
graphqljson)
2222

23+
if(GRAPHQL_BUILD_HTTP_SAMPLE)
24+
find_package(Boost)
25+
if(Boost_FOUND)
26+
add_executable(server server.cpp)
27+
target_link_libraries(server PRIVATE
28+
star_wars
29+
graphqljson)
30+
endif()
31+
endif()
32+
2333
if(WIN32 AND BUILD_SHARED_LIBS)
2434
add_custom_command(OUTPUT copied_sample_dlls
2535
COMMAND ${CMAKE_COMMAND} -E copy_if_different
@@ -38,4 +48,8 @@ if(WIN32 AND BUILD_SHARED_LIBS)
3848
add_custom_target(copy_learn_sample_dlls DEPENDS copied_sample_dlls)
3949

4050
add_dependencies(learn_star_wars copy_learn_sample_dlls)
51+
52+
if(GRAPHQL_BUILD_HTTP_SAMPLE AND Boost_FOUND)
53+
add_dependencies(server copy_learn_sample_dlls)
54+
endif()
4155
endif()

samples/learn/server.cpp

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
#include "StarWarsData.h"
5+
6+
#include "graphqlservice/JSONResponse.h"
7+
8+
#ifdef _MSC_VER
9+
#include <SDKDDKVer.h>
10+
#endif // _MSC_VER
11+
12+
#include <boost/beast/core.hpp>
13+
#include <boost/beast/http.hpp>
14+
#include <boost/beast/version.hpp>
15+
16+
#include <boost/asio/ip/tcp.hpp>
17+
18+
#include <boost/asio/awaitable.hpp>
19+
#include <boost/asio/co_spawn.hpp>
20+
#include <boost/asio/use_awaitable.hpp>
21+
22+
#include <boost/config.hpp>
23+
24+
#include <algorithm>
25+
#include <cstdio>
26+
#include <cstdlib>
27+
#include <iostream>
28+
#include <iterator>
29+
#include <memory>
30+
#include <sstream>
31+
#include <stdexcept>
32+
#include <string>
33+
#include <string_view>
34+
#include <thread>
35+
#include <vector>
36+
37+
using namespace std::literals;
38+
39+
constexpr auto c_host = "127.0.0.1"sv;
40+
constexpr unsigned short c_port = 8080;
41+
constexpr auto c_target = "/graphql"sv;
42+
43+
using namespace graphql;
44+
45+
namespace beast = boost::beast;
46+
namespace http = beast::http;
47+
namespace net = boost::asio;
48+
using tcp = boost::asio::ip::tcp;
49+
50+
using tcp_stream = typename beast::tcp_stream::rebind_executor<
51+
net::use_awaitable_t<>::executor_with_default<net::any_io_executor>>::other;
52+
53+
// Based on:
54+
// https://www.boost.org/doc/libs/1_82_0/libs/beast/example/http/server/awaitable/http_server_awaitable.cpp
55+
int main()
56+
{
57+
auto service = star_wars::GetService();
58+
59+
std::cout << "Created the service..." << std::endl;
60+
61+
const auto address = net::ip::make_address(c_host);
62+
63+
// The io_context is required for all I/O.
64+
net::io_context ioc;
65+
66+
// Spawn a listening port.
67+
net::co_spawn(
68+
ioc,
69+
[](tcp::endpoint endpoint, std::shared_ptr<service::Request> service)
70+
-> net::awaitable<void> {
71+
// Open the acceptor.
72+
auto acceptor =
73+
net::use_awaitable.as_default_on(tcp::acceptor(co_await net::this_coro::executor));
74+
acceptor.open(endpoint.protocol());
75+
76+
// Allow address reuse.
77+
acceptor.set_option(net::socket_base::reuse_address(true));
78+
79+
// Bind to the server address.
80+
acceptor.bind(endpoint);
81+
82+
// Start listening for connections.
83+
acceptor.listen(net::socket_base::max_listen_connections);
84+
85+
while (true)
86+
{
87+
net::co_spawn(
88+
acceptor.get_executor(),
89+
[](tcp_stream stream, std::shared_ptr<service::Request> service)
90+
-> net::awaitable<void> {
91+
// This buffer is required to persist across reads
92+
beast::flat_buffer buffer;
93+
94+
bool keepAlive = false;
95+
96+
// This lambda is used to send messages
97+
try
98+
{
99+
do
100+
{
101+
// Set the timeout.
102+
stream.expires_after(std::chrono::seconds(30));
103+
104+
// Read a request
105+
http::request<http::string_body> req;
106+
co_await http::async_read(stream, buffer, req);
107+
108+
// Handle the request
109+
http::response<http::string_body> msg;
110+
111+
try
112+
{
113+
if (req.method() != http::verb::post
114+
|| req.target() != c_target)
115+
{
116+
throw std::runtime_error(
117+
"Only POST requests to /graphql are supported.");
118+
}
119+
else
120+
{
121+
std::ostringstream oss;
122+
123+
oss << req.body();
124+
125+
auto payload = response::parseJSON(oss.str());
126+
127+
if (payload.type() != response::Type::Map)
128+
{
129+
throw std::runtime_error("Invalid request!");
130+
}
131+
132+
const auto queryItr = payload.find("query"sv);
133+
134+
if (queryItr == payload.end()
135+
|| queryItr->second.type() != response::Type::String)
136+
{
137+
throw std::runtime_error("Invalid request!");
138+
}
139+
140+
peg::ast query;
141+
142+
query = peg::parseString(
143+
queryItr->second.get<response::StringType>());
144+
145+
if (!query.root)
146+
{
147+
throw std::runtime_error("Unknown error!");
148+
}
149+
150+
const auto operationNameItr =
151+
payload.find("operationName"sv);
152+
const auto operationName =
153+
(operationNameItr != payload.end()
154+
&& operationNameItr->second.type()
155+
== response::Type::String)
156+
? std::string_view { operationNameItr->second
157+
.get<response::StringType>() }
158+
: std::string_view {};
159+
160+
const auto variablesItr = payload.find("variables"sv);
161+
auto variables = (variablesItr != payload.end()
162+
&& variablesItr->second.type()
163+
== response::Type::String)
164+
? response::parseJSON(operationNameItr->second
165+
.get<response::StringType>())
166+
: response::Value {};
167+
168+
msg = http::response<http::string_body> { http::status::ok,
169+
req.version() };
170+
msg.set(http::field::server, BOOST_BEAST_VERSION_STRING);
171+
msg.set(http::field::content_type, "application/json");
172+
msg.keep_alive(req.keep_alive());
173+
msg.body() = response::toJSON(
174+
service
175+
->resolve(
176+
{ query, operationName, std::move(variables) })
177+
.get());
178+
msg.prepare_payload();
179+
}
180+
}
181+
catch (const std::runtime_error& ex)
182+
{
183+
msg = http::response<http::string_body> {
184+
http::status::bad_request,
185+
req.version()
186+
};
187+
msg.set(http::field::server, BOOST_BEAST_VERSION_STRING);
188+
msg.set(http::field::content_type, "text/plain");
189+
msg.keep_alive(req.keep_alive());
190+
msg.body() = "Error: "s + ex.what();
191+
msg.prepare_payload();
192+
}
193+
194+
// Determine if we should close the connection
195+
keepAlive = msg.keep_alive();
196+
197+
// Send the response
198+
co_await beast::async_write(stream,
199+
http::message_generator { std::move(msg) },
200+
net::use_awaitable);
201+
202+
} while (keepAlive);
203+
}
204+
catch (boost::system::system_error& se)
205+
{
206+
if (se.code() != http::error::end_of_stream)
207+
throw;
208+
}
209+
210+
// Send a TCP shutdown
211+
beast::error_code ec;
212+
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
213+
214+
// At this point the connection is closed gracefully
215+
// we ignore the error because the client might have
216+
// dropped the connection already.
217+
}(tcp_stream(co_await acceptor.async_accept()), service),
218+
[](std::exception_ptr exp) {
219+
if (exp)
220+
{
221+
try
222+
{
223+
std::rethrow_exception(exp);
224+
}
225+
catch (const std::exception& ex)
226+
{
227+
std::cerr << "Session error: " << ex.what() << std::endl;
228+
}
229+
}
230+
});
231+
}
232+
}(tcp::endpoint(address, c_port), std::move(service)),
233+
[](std::exception_ptr exp) {
234+
if (exp)
235+
{
236+
std::rethrow_exception(exp);
237+
}
238+
});
239+
240+
try
241+
{
242+
ioc.run();
243+
}
244+
catch (const std::runtime_error& ex)
245+
{
246+
std::cerr << ex.what() << std::endl;
247+
return 1;
248+
}
249+
250+
return 0;
251+
}

samples/proxy/CMakeLists.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
cmake_minimum_required(VERSION 3.15)
5+
6+
add_subdirectory(schema)
7+
add_subdirectory(client)
8+
9+
add_executable(proxy proxy.cpp)
10+
target_link_libraries(proxy PRIVATE
11+
proxy_schema
12+
proxyrelay_client
13+
graphqljson)
14+
target_include_directories(proxy PRIVATE
15+
client
16+
schema)
17+
18+
if(WIN32 AND BUILD_SHARED_LIBS)
19+
add_custom_command(OUTPUT copied_sample_dlls
20+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
21+
$<TARGET_FILE:graphqlservice>
22+
$<TARGET_FILE:graphqljson>
23+
$<TARGET_FILE:graphqlpeg>
24+
$<TARGET_FILE:graphqlresponse>
25+
$<TARGET_FILE:graphqlclient>
26+
${CMAKE_CURRENT_BINARY_DIR}
27+
COMMAND ${CMAKE_COMMAND} -E touch copied_sample_dlls
28+
DEPENDS
29+
graphqlservice
30+
graphqljson
31+
graphqlpeg
32+
graphqlresponse
33+
graphqlclient)
34+
35+
add_custom_target(copy_proxy_sample_dlls DEPENDS copied_sample_dlls)
36+
37+
add_dependencies(proxy copy_proxy_sample_dlls)
38+
endif()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
cmake_minimum_required(VERSION 3.15)
5+
6+
# Normally this would be handled by find_package(cppgraphqlgen CONFIG).
7+
include(${CMAKE_CURRENT_SOURCE_DIR}/../../../cmake/cppgraphqlgen-functions.cmake)
8+
9+
if(GRAPHQL_UPDATE_SAMPLES AND GRAPHQL_BUILD_CLIENTGEN)
10+
update_graphql_client_files(proxyrelay ../schema/schema.proxy.graphql query.relay.graphql ProxyRelay proxyrelay)
11+
endif()
12+
13+
add_graphql_client_target(proxyrelay)

0 commit comments

Comments
 (0)