diff --git a/cmake/config.cmake b/cmake/config.cmake index aad156ae2..f1e2e97f2 100644 --- a/cmake/config.cmake +++ b/cmake/config.cmake @@ -1,5 +1,5 @@ message(STATUS "-------------YLT CONFIG SETTING-------------") -option(YLT_ENABLE_SSL "Enable ssl support" OFF) +option(YLT_ENABLE_SSL "Enable ssl support" ON) message(STATUS "YLT_ENABLE_SSL: ${YLT_ENABLE_SSL}") if (YLT_ENABLE_SSL) find_package(OpenSSL REQUIRED) diff --git a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp index e40103ca9..3b59f6957 100644 --- a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp +++ b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp @@ -109,6 +109,11 @@ class coro_rpc_server_base { conn_timeout_duration_(config.conn_timeout_duration), flag_{stat::init}, is_enable_tcp_no_delay_(config.is_enable_tcp_no_delay) { +#ifdef YLT_ENABLE_SSL + if (config.ssl_config) { + init_ssl_context_helper(context_, config.ssl_config.value()); + } +#endif init_address(config.address); } @@ -118,7 +123,7 @@ class coro_rpc_server_base { } #ifdef YLT_ENABLE_SSL - void init_ssl_context(const ssl_configure &conf) { + void init_ssl(const ssl_configure &conf) { use_ssl_ = init_ssl_context_helper(context_, conf); } #endif diff --git a/include/ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp b/include/ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp index ec931679b..f65895885 100644 --- a/include/ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp +++ b/include/ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp @@ -32,6 +32,9 @@ struct config_base { std::chrono::steady_clock::duration conn_timeout_duration = std::chrono::seconds{0}; std::string address = "0.0.0.0"; +#ifdef YLT_ENABLE_SSL + std::optional ssl_config = std::nullopt; +#endif }; struct config_t : public config_base { diff --git a/src/coro_rpc/tests/test_coro_rpc_client.cpp b/src/coro_rpc/tests/test_coro_rpc_client.cpp index a600594ff..7ced03611 100644 --- a/src/coro_rpc/tests/test_coro_rpc_client.cpp +++ b/src/coro_rpc/tests/test_coro_rpc_client.cpp @@ -84,7 +84,7 @@ TEST_CASE("testing client") { future.wait(); coro_rpc_server server(2, coro_rpc_server_port); #ifdef YLT_ENABLE_SSL - server.init_ssl_context( + server.init_ssl( ssl_configure{"../openssl_files", "server.crt", "server.key"}); #endif auto res = server.async_start(); @@ -172,7 +172,7 @@ TEST_CASE("testing client with inject server") { }); coro_rpc_server server(2, coro_rpc_server_port); #ifdef YLT_ENABLE_SSL - server.init_ssl_context( + server.init_ssl( ssl_configure{"../openssl_files", "server.crt", "server.key"}); #endif auto res = server.async_start(); @@ -242,10 +242,10 @@ class SSLClientTester { inject("server key", server_key_path, server_key); inject("dh", dh_path, dh); ssl_configure config{base_path, server_crt_path, server_key_path, dh_path}; - server.init_ssl_context(config); + server.init_ssl(config); server.template register_handler(); auto res = server.async_start(); - CHECK_MESSAGE(res, "server start timeout"); + CHECK_MESSAGE(!res.hasResult(), "server start timeout"); std::promise promise; auto future = promise.get_future(); diff --git a/src/coro_rpc/tests/test_coro_rpc_server.cpp b/src/coro_rpc/tests/test_coro_rpc_server.cpp index 829481bfb..444776be0 100644 --- a/src/coro_rpc/tests/test_coro_rpc_server.cpp +++ b/src/coro_rpc/tests/test_coro_rpc_server.cpp @@ -37,7 +37,7 @@ struct CoroServerTester : ServerTester { server(2, config.port, config.address, config.conn_timeout_duration) { #ifdef YLT_ENABLE_SSL if (use_ssl) { - server.init_ssl_context( + server.init_ssl( ssl_configure{"../openssl_files", "server.crt", "server.key"}); } #endif diff --git a/website/Doxyfile b/website/Doxyfile index ca2b1e5fc..5b3ef1679 100644 --- a/website/Doxyfile +++ b/website/Doxyfile @@ -5,6 +5,8 @@ INPUT=../include/ylt/struct_pack.hpp \ ../include/ylt/coro_rpc/coro_rpc_server.hpp \ ../include/ylt/coro_rpc/coro_rpc_client.hpp \ docs/en/coro_rpc/coro_rpc_introduction.md \ + docs/en/coro_rpc/coro_rpc_server.md \ + docs/en/coro_rpc/coro_rpc_client.md \ docs/en/struct_pack/struct_pack_intro.md MARKDOWN_SUPPORT = YES diff --git a/website/docs/en/coro_rpc/coro_rpc_client.md b/website/docs/en/coro_rpc/coro_rpc_client.md new file mode 100644 index 000000000..4b3daf5ac --- /dev/null +++ b/website/docs/en/coro_rpc/coro_rpc_client.md @@ -0,0 +1,261 @@ +# Introduction to coro_rpc Client + +## Base Usage + +class `coro_rpc::coro_rpc_client` is the client side of coro_rpc, allowing users to send RPC requests to the server. + +Below, we will demonstrate the basic usage of rpc_client. + +```cpp +using namespace async_simple; +using namespace coro_rpc; +int add(int a,int b); +Lazy example() { + coro_rpc_client client; + coro_rpc::err_code ec = co_await client.connect("localhost:9001"); + if (ec) { /*check if connection error*/ + std::cout<(1,2); + /*rpc_result is type of expected, which T is rpc return type*/ + if (!result.has_value()) { + /*call result.error() to get rpc error message*/ + std::cout<<"error code:"<set_resp_attachment(ctx->get_req_attachment()); +} +Lazy example(coro_rpc_client& client, std::string_view attachment) { + client.set_req_attachment(attachment); + rpc_result result = co_await client.call(); + if (result.has_value()) { + assert(result.get_resp_attachment()==attachment); + co_return std::move(result.release_resp_attachment()); + } + co_return ""; +} +``` + +By default, the RPC client will wait for 5 seconds after sending a request/establishing a connection. If no response is received after 5 seconds, it will return a timeout error. Users can also customize the wait duration by calling the `call_for` function. + +```cpp +client.connect("127.0.0.1:9001", std::chrono::seconds{10}); +auto result = co_await client.call_for(std::chrono::seconds{10},1,2); +assert(result.value() == 3); +``` + +The duration can be any `std::chrono::duration`` type, common examples include `std::chrono::seconds`` and `std::chrono::milliseconds``. Notably, if the duration is set to zero, it indicates that the function call will never time out. + +## SSL support + +coro_rpc supports using OpenSSL to encrypt connections. After installing OpenSSL and importing yalantinglibs into your project with CMake's `find_package` or `fetch_content`, you can enable SSL support by setting the CMake option YLT_ENABLE_SSL=ON. Alternatively, you might manually add the YLT_ENABLE_SSL macro and manually link to OpenSSL. + +Once SSL support has been enabled, users can invoke the `init_ssl`` function before establishing a connection to the server. This will create an encrypted link between the client and the server. It’s important to note that the coro_rpc server must also be compiled with SSL support enabled, and the `init_ssl` method must be called to enable SSL support before starting the server. + +```cpp +client.init_ssl("./","server.crt"); +``` + + +The first string represents the base path where the SSL certificate is located, the second string represents the relative path of the SSL certificate relative to the base path. + +## Conversion and compile-time checking of RPC parameters + +coro_rpc will perform compile-time checks on the validity of arguments during invocation. For example, for the following rpc function: + +```cpp +inline std::string echo(std::string str) { return str; } +``` + +Next, when the current client invokes the rpc function: + +```cpp +client.call(42); // Parameter does not match, compilation error +client.call(); // Missing parameter, compilation error +client.call("", 0); // Extra parameters, compilation error +client.call("hello, coro_rpc"); // The string literal can be converted to std::string, compilation succeeds +``` + +## Connect Option + +The `coro_rpc_client` provides an `init_config`` function for configuring connection options. The following code snippet lists the configurable options. + +```cpp +using namespace coro_rpc; +using namespace std::chrono; +void set_config(coro_rpc_client& client) { + client.init_config(config{ + .timeout_duration = 5s, // Timeout duration for requests and connections + .host = "localhost", // Server hostname + .port = "9001", // Server port + .enable_tcp_no_delay = true, // Whether to disable socket-level delayed sending of requests + /* The following options are available only when SSL support is activated */ + .ssl_cert_path = "./server.crt", // Path to the SSL certificate + .ssl_domain = "localhost" + }); +} +``` + +## Calling Model + +Each `coro_rpc_client` is bound to a specific IO thread. By default, it selects a connection via round-robin from the global IO thread pool. Users can also manually bind it to a specific IO thread. + +```cpp +auto executor=coro_io::get_global_executor(); +coro_rpc_client client(executor),client2(executor); +// Both clients are bound to the same IO thread. +``` + +Each time a coroutine-based IO task is initiated (such as `connect`, `call`, `send_request`), the client internally submits the IO event to the operating system. When the IO event is completed, the coroutine is then resumed on the bound IO thread to continue execution. For example, in the following code, the task switches to the IO thread for execution after calling connect. + +```cpp +/*run in thread 1*/ +coro_rpc_client cli; +co_await cli.connect("localhost:9001"); +/*run in thread 2*/ +do_something(); +``` + +## Connection Pool and Load Balancing + +`coro_io` offers a connection pool `client_pool` and a load balancer `channel`. Users can manage `coro_rpc`/`coro_http` connections through the `client_pool`, and can use `channel` to achieve load balancing among multiple hosts. For more details, please refer to the documentation of `coro_io`. + +## Connection Reuse + +The `coro_rpc_client` can achieve connection reuse through the `send_request`` function. This function is thread-safe, allowing multiple threads to call the `send_request` method on the same client concurrently. The return value of the function is `Lazy>>`. The first `co_await` waits for the request to be sent, and the second `co_await` waits for the rpc result to return. + + +Connection reuse allows us to reduce the number of connections under high concurrency, eliminating the need to create new connections. It also improves the throughput of each connection. + +Here's a simple example code snippet: + +```cpp +using namespace coro_rpc; +using namespace async_simple::coro; +std::string_view echo(std::string_view); +Lazy example(coro_rpc_client& client) { + // Wait for the request to be fully sent + Lazy handler = co_await client.send_request("Hello"); + // Then wait for the server to return the RPC request result + async_rpc_result result = co_await handler; + if (result) { + assert(result->result() == "Hello"); + } + else { + // error handle + std::cout< example(coro_rpc_client& client) { + std::vector> handlers; + // First, send 10 requests consecutively + for (int i=0;i<10;++i) { + handlers.push_back(co_await client.send_request(std::to_string(i))); + } + // Next, wait for all the requests to return + std::vector results = co_await collectAll(std::move(handlers)); + for (int i=0;i<10;++i) { + assert(results[i]->result() == std::to_string(i)); + } + co_return; +} +``` + +### Attachment + +When using the `send_request` method, since multiple requests might be sent simultaneously, we should not call the `set_req_attachment` method to send an attachment to the server, nor should we call the `get_resp_attachment` and `release_resp_attachment` methods to get the attachment returned by the server. + +Instead, we can set the attachment when sending a request by calling the `send_request_with_attachment` function. Additionally, we can retrieve the attachment by calling the `->get_attachment()` and `->release_buffer()` methods of `async_rpc_result`. + + + +```cpp +using namespace coro_rpc; +using namespace async_simple::coro; +int add(int a, int b); +Lazy example(coro_rpc_client& client) { + async_rpc_result result = co_await co_await client.send_request_with_attachment("Hello", 1, 2); + assert(result->result() == 3); + assert(result->get_attachment() == "Hello"); + co_return std::move(result->release_buffer().resp_attachment_buf_); +} +``` + + +### Execution order + +When the called rpc function is a coroutine rpc function or a callback rpc function, the rpc requests may not necessarily be executed in order. The server might execute multiple rpc requests simultaneously. +For example, suppose there is the following code: + +```cpp +using namespace async_simple::coro; +Lazy sleep(int seconds) { + co_await coro_io::sleep(1s * seconds); // Yield the coroutine here + co_return; +} +``` + +Server registration and startup: +```cpp +using namespace coro_rpc; +void start() { + coro_rpc_server server(/* thread = */1,/* port = */ 8801); + server.register_handler(); + server.start(); +} +``` + +The client consecutively calls the sleep function twice on the same connection, sleeping for 2 seconds the first time and 1 second the second time. +```cpp +using namespace async_simple::coro; +using namespace coro_rpc; +Lazy call() { + coro_rpc_client cli,cli2; + co_await cli.connect("localhost:8801"); + co_await cli2.connect("localhost:8801"); + auto handler1 = co_await cli.send_request(2); + auto handler2 = co_await cli.send_request(1); + auto handler3 = co_await cli2.send_request(0); + handler2.start([](auto&&){ + std::cout<<"handler2 return"< -Lazy say_hello(){ +Lazy say_hello() { coro_rpc_client client; co_await client.connect("localhost", /*port =*/"9000"); while (true){ @@ -262,77 +262,6 @@ Lazy say_hello(){ One core feature of coro_rpc is stackless coroutine where users could write asynchronous code in a synchronous manner, which is more simple and easy to understand. -# More features - -## Real-time Tasks and Non-Real-time Tasks - -The examples shown earlier do not demonstrate how responses are sent back to the client with the results, because by default the coro_rpc framework will help the user to serialize and send the results to client automatically. And the user is completely unaware and only needs to focus on the business logic. It should be noted that, in this scenario, the response callback is executed in the I/O thread, which is suitable for real-time critical scenarios, with the disadvantage of blocking the I/O thread. What if the user does not want to execute the business logic in the io thread, but rather in a thread or thread pool and delays sending messages? - -coro_rpc has taken this problem into account. coro_rpc considers that RPC tasks are divided into real-time and Non-real-time tasks. real-time tasks are executed in the I/O thread and sent to the client immediately, with better timeliness and lower latency; Non-real-time tasks can be scheduled in a separate thread, and the requests are sent to the server at some point in the future; coro_rpc supports both kinds of tasks. - - -Switch to time-delayed task - -```cpp -#include -#include - -//Real-time tasks -std::string echo(std::string str) { return str; } - -//Non-Real-time tasks, requests handled in separate thread -void delay_echo(coro_connection conn, std::string str) { - std::thread([conn, str]{ - conn.response_msg(str); //requests handled in separate thread - }).detach(); -} -``` - -## Asynchronous mode - -It is recommended to use coroutine on server development. However, the asynchronous call mode is also supported if user does not prefer coroutine. - -- coroutine based rpc server -```cpp -#include -std::string hello() { return "hello coro_rpc"; } - -int main() { - coro_rpc_server server(/*thread_num =*/10, /*port =*/9000); - server.register_handler(); - server.start(); -} -``` - -- Asynchronous rpc server - -```cpp -#include -std::string hello() { return "hello coro_rpc"; } - -int main() { - async_rpc_server server(/*thread_num =*/10, /*port =*/9000); - server.register_handler(); - server.start(); -} -``` - -Compile-time syntax checks -coro_rpc does a compile-time check on the legality of the arguments when it is called, e.g.: - -```cpp -inline std::string echo(std::string str) { return str; } -``` - -When client is called via: - -```cpp -client.call(42); //Parameter mismatch, compile error -client.call(); //Missing parameters, compile error -client.call("", 0); //Redundant parameters, compile error -client.call("hello, coro_rpc");//Parameters match, OK -``` - # Benchmark ## System Configuration diff --git a/website/docs/en/coro_rpc/coro_rpc_server.md b/website/docs/en/coro_rpc/coro_rpc_server.md new file mode 100644 index 000000000..f18f646f4 --- /dev/null +++ b/website/docs/en/coro_rpc/coro_rpc_server.md @@ -0,0 +1,433 @@ +# coro_rpc Server Introduction + +## Server registration and startup + +### Function registration + +Before starting the RPC server, we need to call the `register_handler<>` function to register all RPC functions. Registration is not thread-safe and cannot be done after the RPC server has started. + +```cpp +void hello(); +Lazy echo(std::string_view); +int add(int a, int b); +int regist_rpc_funtion(coro_rpc_server& server) { + server.register_handler(); +} +``` + +### Start the server + +We can start a server by calling the `.start()` method, which will block the current thread until the server exits. + +```cpp +int start_server() { + coro_rpc_server server; + regist_rpc_funtion(server); + coro_rpc::err_code ec = server.start(); + /*block util server down*/ +} +``` + +If you do not want to block the current thread, we also allow asynchronously starting a server by calling `async_start()`. After this function returns, it ensures that the server has already started listening on the port (or an error has occurred). Users can check `async_simple::Future::hasResult()` to determine whether the server is currently started successfully and running normally. Calling the `async_simple::Future::get()` method can then be used to wait for the server to stop. + +```cpp +int start_server() { + coro_rpc_server server; + regist_rpc_funtion(server); + async_simple::Future ec = server.async_start(); /*won't block here */ + assert(!ec.hasResult()) /* check if server start success */ + auto err = ec.get(); /*block here util server down then return err code*/ +} +``` + +coro_rpc supports the registration and calling of three types of RPC functions: + +1. Ordinary RPC Functions +2. Coroutine RPC Functions +3. Callback RPC Functions + +## Ordinary RPC Functions + +If a function is neither a coroutine nor its first parameter is of type `coro_rpc::context`, then this RPC function is an ordinary function. +For example, the following functions are ordinary functions: + + +```cpp +int add(int a, int b); +std::string_view echo(std::string_view str); +struct dummy { + std::string_view echo(std::string_view str) { return str; } +}; +``` + +### Calling model + +Synchronous execution is a definite characteristic of ordinary functions. When a connection submits a request for an ordinary function, the server will execute that function on the I/O thread associated with that connection, and it will continue to do so until the function has been completed. Only then will the result be sent back to the client, and subsequent requests from that connection will be addressed. For instance, if a client sends two requests, A and B, in sequence, we guarantee that B will be executed only after A has finished. + +It's important to note that performing time-consuming operations within a function can not only block the current connection but may also impede other connections that are bound to the same I/O thread. Therefore, in scenarios where performance is of high concern, one should not register ordinary functions that are too taxing. Instead, one might want to consider the use of coroutine functions or callback functions as an alternative. + +### Retrieving Context Information + +When a function is called by coro_rpc_server, the following code can be used to obtain context information about the connection. + +```cpp +using namespace coro_rpc; +void test() { + context_info_t* ctx = coro_rpc::get_context(); + if (ctx->has_closed()) { // Check if the connection has been closed + throw std::runtime_error("Connection is closed!"); + } + // Retrieve the connection ID and request ID + ELOGV(INFO, "Call function echo_with_attachment, connection ID: %d, request ID: %d", + ctx->get_connection_id(), ctx->get_request_id()); + // Obtain the client's IP and port as well as the server's IP and port + ELOGI << "Remote endpoint: " << ctx->get_remote_endpoint() << ", local endpoint: " + << ctx->get_local_endpoint(); + // Get the name of the RPC function + ELOGI << "RPC function name: " << ctx->get_rpc_function_name(); + // Get the request attachment + std::string_view sv{ctx->get_request_attachment()}; + // Release the request attachment + std::string str = ctx->release_request_attachment(); + // Set the response attachment + ctx->set_response_attachment(std::move(str)); +} +``` + +An attachment is an additional piece of data that accompanies an RPC request. coro_rpc does not serialize it. Users can obtain a view of the request's accompanying attachment or release it from the context for separate manipulation. Similarly, users can also set the attachment to be sent back to the RPC client in the response. + +### Error Handling + +We allow the termination of an RPC call and the return of RPC error codes and error messages to the user by throwing a `coro_rpc::rpc_error` exception. + +```cpp +void rpc() { + throw coro_rpc::rpc_error{coro_rpc::errc::io_error}; // Return a custom error code + throw coro_rpc::rpc_error{10404}; // Return a custom error code + throw coro_rpc::rpc_error{10404, "404 Not Found"}; // Return a custom error code and error message +} +``` + +An RPC error code is a 16-bit unsigned integer. The range 0-255 is reserved for error codes used by the RPC framework itself, whereas user-defined error codes can be any integer within [256, 65535]. When an RPC returns a user-defined error code, the connection will not be closed. However, if the returned error code is one reserved by the RPC framework and indicates a severe RPC error, it will result in the disconnection of the RPC link. + + +## Coroutine RPC Functions + +If an RPC function has a return type of `async_simple::coro::Lazy`, then it's considered a coroutine function. Compared to ordinary functions, coroutine functions are asynchronous, which means they can yield the I/O thread while waiting for events to complete, thus improving concurrency performance. + +For instance, the following RPC function uses a coroutine to submit a heavy computation task to the global thread pool, thereby avoiding blocking the current I/O thread. + +```cpp +using namespace async_simple::coro; +int heavy_calculate(int value); +Lazy calculate(int value) { + auto val = co_await coro_io::post([value](){return heavy_calculate(value);}); //将任务提交到全局线程池执行,让出当前IO线程,直到任务完成。 + co_return val; +} +``` + +Users can also use async_simple::Promise to submit tasks to a custom thread pool: + +```cpp +using namespace async_simple::coro; +void heavy_calculate(int value); +Lazy calculate(int value) { + async_simple::Promise p; + std::thread th([&p,value](){ + auto ret = heavy_calculate(value); + p.setValue(ret); // Task completed, wake up the RPC function + }); + th.detach(); + auto ret = co_await p.get_future(); // Wait for the task to complete + co_return ret; +} +``` + +### Calling Model + +When a connection submits a coroutine function request, the server will start a new coroutine on the I/O thread that the connection is bound to and execute the function within this new coroutine. Once the coroutine function completes, the server will send the RPC result back to the client based on its return value. If the coroutine yields during execution, the I/O thread will continue to execute other tasks, such as handling the next request or managing other connections bound to the same I/O thread. + +For example, consider the following code: + +```cpp +using namespace async_simple::coro; +Lazy sleep(int seconds) { + co_await coro_io::sleep(1s * seconds); // Yield the coroutine here + co_return; +} +``` + +Then the server register and start: +```cpp +using namespace coro_rpc; +void start() { + coro_rpc_server server(/* thread = */1,/* port = */ 8801); + server.register_handler(); + server.start(); +} +``` + +The client invokes the sleep function twice consecutively on the same connection, sleeping for 2 seconds the first time and 1 second the second time. + +```cpp +using namespace async_simple::coro; +using namespace coro_rpc; +Lazy call() { + coro_rpc_client cli,cli2; + co_await cli.connect("localhost:8801"); + co_await cli2.connect("localhost:8801"); + auto handler1 = co_await cli.send_request(2); + auto handler2 = co_await cli.send_request(1); + auto handler3 = co_await cli2.send_request(0); + handler2.start([](auto&&){ + std::cout<<"handler2 return"< test() { + context_info_t* ctx = co_await coro_rpc::get_context_in_coro(); +} +``` + +### Error Handling + +Similar to regular functions, we can return RPC errors by throwing the `coro_rpc::rpc_error` exception, allowing for customized RPC error codes and messages. + + +## Callback RPC Functions + +We also support the more traditional callback functions to implement asynchronous RPC calls. The syntax for a callback function is as follows: +```cpp +void echo(coro_rpc::context, std::string_view param); +``` + +If a function's return type is `void` and the first parameter is of type `coro_rpc::context`, then this function is a callback function. The `coro_rpc::context` is similar to a smart pointer, holding the callback handle and context information for this RPC call. + +For example, in the code below, we copy `coro_rpc::context` to another thread, which then sleeps for 30 seconds before returning the result to the RPC client by calling `coro_rpc::context::response_msg()`. + +```cpp +using namespace std::chrono; +void echo(coro_rpc::context ctx, std::string_view param) { + std::thread th([ctx, param](){ + std::this_thread::sleep_for(30s); + ctx.response_msg(param); + }); + return; +} +``` + +It should be noted that view types in the RPC function parameters, such as std::string_view and std::span, will have their underlying data become invalid after all copies of the coro_rpc::context object for this RPC call are destructed. + +### Calling Model + +When a connection receives a request for a callback function, the I/O thread allocated to that connection will immediately execute the function until it is completed, after which other requests will be processed. Since callback functions do not have return values, the server does not immediately reply to the client after the RPC function is executed. + +When the user calls `coro_rpc::context::response_msg` or `coro_rpc::context::response_error`, the RPC server will be notified, and only then will the result be sent to the client. Therefore, users must ensure that they actively call one of the callback functions at some point in their code. + + +### Obtaining Context Information + +In callback functions, we can call `coro_rpc::context::get_context_info()` to obtain the coroutine's context information. Additionally, `coro_io::get_context()` can be used to obtain context information before the RPC function returns. However, after the RPC function has returned, the context information pointed to by `coro_io::get_context()` may be modified or invalid. Therefore, it's recommended to use `coro_rpc::context::get_context_info()` to obtain context information. + +```cpp +void echo(coro_rpc::context ctx) { + context_info_t* info = ctx.get_context_info(); + return; +} +``` + +### Error Handling + +In callback functions, one should not and cannot return RPC errors by throwing exceptions, because the error might not occur within the call stack of the RPC function. Instead, we can use the coro_rpc::context::response_error() function to return RPC errors. + +```cpp +void echo(coro_rpc::context ctx) { + ctx.response_error(10015); // Custom RPC error code + ctx.response_error(10015, "my error msg"); // Custom RPC error code and error message + ctx.response_error(coro_rpc::errc::io_error); // Using the built-in error code of the RPC framework + return; +} +``` + +The RPC error code is a 16-bit unsigned integer. The range 0-255 is reserved for error codes used by the RPC framework, and user-defined error codes can be any integer between [256, 65535]. When an RPC returns a user-defined error code, the connection will not be terminated. However, if an error code from the RPC framework is returned, it is considered a serious RPC error, leading to the disconnection of the RPC link. + +## Connections and I/O Threads + +The server internally has an I/O thread pool, the size of which defaults to the number of logical threads of the CPU. After the server starts, it launches a listening task on one of the I/O threads to accept connections from clients. Each time a connection is accepted, the server selects an I/O thread through round-robin to bind it to. Subsequently, all steps including data transmission, serialization, RPC routing, etc., of that connection are executed on this I/O thread. The RPC functions are also executed on the same I/O thread. + +This means if your RPC functions will block the current thread (e.g., thread sleep, synchronous file read/write), it is better to make them asynchronous to avoid blocking the I/O thread, thereby preventing other requests from being blocked. For example, `async_simple` provides coroutine locks such as `Mutex` and `Spinlock`, and components such as `Promise` and `Future` that wrap asynchronous tasks as coroutine tasks. `coro_io` offers coroutine-based asynchronous file read/write, asynchronous read/write of sockets, sleep, and the `period_timer` timer. It also allows submitting high-CPU-load tasks to the global blocking task thread pool through `coro_io::post`. coro_rpc/coro_http offer coroutine-based asynchronous RPC and HTTP calls, respectively. easylog by default submits log content to a background thread for writing, ensuring the foreground does not block. + + +## Parameter and Return Value Types + +coro_rpc allows users to register rpc functions with multiple parameters (up to 64), and the types of arguments and return values can be user-defined aggregate structures. They also support various data structures provided by the C++ standard library and many third-party libraries. For details, see: [struct_pack type system](https://alibaba.github.io/yalantinglibs/en/struct_pack/struct_pack_type_system.html) + +If your rpc argument or return value type is not supported by the struct_pack type system, we also allow users to register their own structures or custom serialization algorithms. For more details, see: [Custom feature](https://alibaba.github.io/yalantinglibs/en/struct_pack/struct_pack_intro.html#custom-type) + +## RPC Return Value Construction and Checking + +Furthermore, for callback functions, coro_rpc will try to construct the return value type from the parameter list. If it fails to construct, it will lead to a compilation failure. + +```cpp +void echo(coro_rpc::context ctx) { + ctx.response_msg(); // Unable to construct std::string. Compilation fails. + ctx.response_msg(42); // Unable to construct std::string. Compilation fails. + ctx.response_msg(42,'A'); // Able to construct std::string, compilation passes. + ctx.response_msg("Hello"); // Able to construct std::string, compilation passes. + return; +} +``` + +## SSL Support + +coro_rpc supports encrypting connections with OpenSSL. After installing OpenSSL and importing yalantinglibs into your project using cmake's `find_package/fetch_content`, you can enable SSL support by turning on the cmake option `YLT_ENABLE_SSL=ON`. Alternatively, you can manually add the macro `YLT_ENABLE_SSL` and manually link OpenSSL. + +Once SSL support is enabled, users can call the `init_ssl` function before connecting to the server. This will establish an encrypted link between the client and the server. It should be noted that the coro_rpc server also must have SSL support enabled at compile time. + +```cpp +coro_rpc_server server; +server.init_ssl({ + .base_path = "./", // Base path of ssl files. + .cert_file = "server.crt", // Path of the certificate relative to base_path. + .key_file = "server.key" // Path of the private key relative to base_path. +}); +``` + +After enabling SSL support, the server will reject all non-SSL connections. + +## Advanced Settings + +We provide the coro_rpc::config_t class, which allows users to set the details of the server: + +```cpp +struct config_base { + bool is_enable_tcp_no_delay = true; /* Whether to respond immediately to tcp requests */ + uint16_t port = 9001; /* Listening port */ + unsigned thread_num = std::thread::hardware_concurrency(); /* Number of connections used internally by rpc server, default is the number of logical cores */ + std::chrono::steady_clock::duration conn_timeout_duration = + std::chrono::seconds{0}; /* Timeout duration for rpc requests, 0 seconds means rpc requests will not automatically timeout */ + std::string address="0.0.0.0"; /* Listening address */ + /* The following settings are only applicable if SSL is enabled */ + std::optional ssl_config = std::nullopt; // Configure whether to enable ssl +}; +struct ssl_configure { + std::string base_path; // Base path of ssl files. + std::string cert_file; // Path of the certificate relative to base_path. + std::string key_file; // Path of the private key relative to base_path. + std::string dh_file; // Path of the dh_file relative to base_path (optional). +} +int start() { + coro_rpc::config_t config{}; + coro_rpc_server server(config); + /* Register rpc function here... */ + server.start(); +} +``` + + +## Registration and Invocation of Special RPC Functions + +### Registration and Invocation of Member Functions + +coro_rpc supports registering and invoking member functions: + +For example, consider the following function: + +```cpp +struct dummy { + std::string_view echo(std::string_view str) { return str; } + Lazy coroutine_echo(std::string_view str) {co_return str;} + void callback_echo(coro_rpc::context ctx, std::string_view str) { + ctx.response_msg(str); + } +}; +``` + +The server can register these functions like this: + +```cpp +#include "rpc_service.h" +#include +int main() { + coro_rpc_server server; + dummy d{}; + server.register_handler<&dummy::echo,&dummy::coroutine_echo,&dummy::callback_echo>(&d); // regist member function + server.start(); +} +``` + +It's important to note that the lifecycle of the registered dummy type must be considered to ensure it remains alive while the server is running. Otherwise, the invoking behavior is undefined. + +The client can call these functions like this: + +```cpp +#include "rpc_service.h" +#include + +Lazy test_client() { + coro_rpc_client client; + co_await client.connect("localhost", /*port =*/"9000"); + + // calling RPC + { + auto result = co_await client.call<&dummy::echo>("hello"); + assert(result.value() == "hello"); + } + { + auto result = co_await client.call<&dummy::coroutine_echo>("hello"); + assert(result.value() == "hello"); + } + { + auto result = co_await client.call<&dummy::callback_echo>("hello"); + assert(result.value() == "hello"); + } +} +``` + +### Specialized Template Functions + +coro_rpc allows users to register and call specialized template functions. + +For example, consider the following function: + +```cpp +template +T echo(T param) { return param; } +``` + +The server can register these functions like this: + +```cpp +#include +using namespace coro_rpc; +int main() { + coro_rpc_server server; + server.register_handler,echo,echo>>(&d); // Register specialized template functions + server.start(); +} +``` + +The client can call like this: +```cpp +using namespace coro_rpc; +using namespace async_simple::coro; +Lazy rpc_call(coro_rpc_client& cli) { + assert(co_await cli.call>(42).value() == 42); + assert(co_await cli.call>("Hello").value() == "Hello"); + assert(co_await cli.call>(std::vector{1,2,3}).value() == std::vector{1,2,3}); +} +``` \ No newline at end of file diff --git a/website/docs/zh/coro_rpc/coro_rpc_client.md b/website/docs/zh/coro_rpc/coro_rpc_client.md index a6a97b1d6..c3251bbf3 100644 --- a/website/docs/zh/coro_rpc/coro_rpc_client.md +++ b/website/docs/zh/coro_rpc/coro_rpc_client.md @@ -2,7 +2,7 @@ ## 基本使用 -coro_rpc_client是coro_rpc的客户端,用户可以通过它向服务器发送rpc请求。 +类`coro_rpc::coro_rpc_client`是coro_rpc的客户端,用户可以通过它向服务器发送rpc请求。 下面我们展示rpc_client的基本使用方法。 @@ -49,7 +49,7 @@ Lazy example(coro_rpc_client& client, std::string_view attachment) ``` 默认情况下,rpc客户端发送请求/建立连接后会等待5秒,如果5秒后仍未收到响应,则会返回超时错误。 -用户也可以通过调用call_for函数自定义等待的时长。 +用户也可以通过调用`call_for`函数自定义等待的时长。 ```cpp client.connect("127.0.0.1:9001", std::chrono::seconds{10}); @@ -57,20 +57,22 @@ auto result = co_await client.call_for(std::chrono::seconds{10},1,2); assert(result.value() == 3); ``` -时长可以是任一的std::chrono::duration类型,常见的如`std::chrono::seconds`,`std::chrono::millseconds`。 +时长可以是任一的`std::chrono::duration`类型,常见的如`std::chrono::seconds`,`std::chrono::millseconds`。 特别的,如果时长为0,代表该函数调用永远也不会超时。 ## SSL支持 coro_rpc支持使用openssl对连接进行加密。在安装openssl并使用cmake find_package/fetch_content 将yalantinglibs导入到你的工程后,可以打开cmake选项`YLT_ENABLE_SSL=ON`启用ssl支持。或者,你也可以手动添加宏`YLT_ENABLE_SSL`并手动链接openssl。 -当启用ssl支持后,用户可以调用`init_ssl`函数,然后再连接到服务器。这会使得客户端与服务器之间建立加密的链接。需要注意的是,coro_rpc服务端在编译时也必须启用ssl支持。 +当启用ssl支持后,用户可以调用`init_ssl`函数,然后再连接到服务器。这会使得客户端与服务器之间建立加密的链接。需要注意的是,coro_rpc服务端在编译时也必须启用ssl支持,并且在启动服务器之前也需要调用`init_ssl`方法来启用SSL支持。 ```cpp client.init_ssl("./","server.crt"); ``` -第一个字符串代表SSL证书所在的路径,第二个字符串代表SSL证书的文件名。 +第一个字符串代表SSL证书所在的基本路径,第二个字符串代表SSL证书相对于基本路径的相对路径。 + +当建立连接时,客户端会使用该证书校验服务端发来的证书,以避免中间人攻击。因此,客户端必须持有服务端使用的证书或其根证书。 ## RPC参数的转换与编译期检查 @@ -80,13 +82,13 @@ coro_rpc会在调用的时候对参数的合法性做编译期检查,比如, inline std::string echo(std::string str) { return str; } ``` -client调用rpc +接下来,当前client调用rpc函数时: ```cpp -client.call(42);//参数不匹配,编译报错 -client.call();//缺少参数,编译报错 -client.call("", 0);//多了参数,编译报错 -client.call("hello, coro_rpc");//该字符串字面量可以转换到std::string,编译成功 +client.call(42);// The argument does not match, a compilation error occurs. +client.call();// Missing argument, compilation error occurs. +client.call("", 0);// There are too many arguments, a compilation error occurs. +client.call("hello, coro_rpc");// The string literal can be converted to std::string, compilation succeeds. ``` ## 连接选项 @@ -111,12 +113,12 @@ void set_config(coro_rpc_client& client) { ## 调用模型 -每一个coro_rpc_client都会绑定到某个IO线程上,默认通过轮转法从全局IO线程池中选择一个连接,用户也可以手动绑定到特定的IO线程上。 +每一个`coro_rpc_client`都会绑定到某个IO线程上,默认通过轮转法从全局IO线程池中选择一个连接,用户也可以手动绑定到特定的IO线程上。 ```cpp auto executor=coro_io::get_global_executor(); coro_rpc_client client(executor),client2(executor); -//两个客户端都被绑定到同一个io线程上 +// 两个客户端都被绑定到同一个io线程上 ``` 每次发起一个基于协程的IO任务(如`connect`,`call`,`send_request`),客户端内部会将IO事件提交给操作系统,当IO事件完成后,再将协程恢复到绑定的IO线程上继续执行。 @@ -133,11 +135,11 @@ do_something(); ## 连接池与负载均衡 -`coro_io`提供了连接池`client_pool`与负载均衡器`channel`。用户可以通过连接池`client_pool`来管理coro_rpc连接,可以使用`channel`实现多个host之间的负载均衡。具体请见`coro_io`的文档。 +`coro_io`提供了连接池`client_pool`与负载均衡器`channel`。用户可以通过连接池`client_pool`来管理`coro_rpc`/`coro_http`连接,可以使用`channel`实现多个host之间的负载均衡。具体请见`coro_io`的文档。 ## 连接复用 -coro_rpc_client 可以通过 `send_request`函数实现连接复用。该函数是线程安全的,允许多个线程同时调用同一个client的 `send_request`方法。该函数返回值为`Lazy`。第一次`co_await`可以等待请求发送,再次`co_await`则等待rpc返回结果。 +`coro_rpc_client` 可以通过 `send_request`函数实现连接复用。该函数是线程安全的,允许多个线程同时调用同一个client的 `send_request`方法。该函数返回值为`Lazy`。第一次`co_await`可以等待请求发送,再次`co_await`则等待rpc返回结果。 连接复用允许我们在高并发下减少连接的个数,无需创建新的连接。同时也能提高每个连接的吞吐量。 @@ -184,9 +186,9 @@ Lazy example(coro_rpc_client& client) { } ``` -### attachment +### Attachment -使用send_request方法时,不允许调用`set_req_attachment`方法向服务器发送attachment,同样也不允许调用`get_resp_attachment`和`release_resp_attachment`方法来获取服务器返回的attachment。 +使用`send_request`方法时,由于可能同时发送多个请求,因此我们不能调用`set_req_attachment`方法向服务器发送attachment,同样也不能调用`get_resp_attachment`和`release_resp_attachment`方法来获取服务器返回的attachment。 我们可以通过调用`send_request_with_attachment`函数,在发送请求时设置attachment。我们也可以通过调用async_rpc_result的`->get_attachment()`方法和`->release_buffer()`方法来获取attachment。 @@ -212,7 +214,7 @@ Lazy example(coro_rpc_client& client) { ```cpp using namespace async_simple::coro; Lazy sleep(int seconds) { - co_await coro_io::sleep(1s * seconds); //在此处让出协程 + co_await coro_io::sleep(1s * seconds); // 在此处让出协程 co_return; } ``` diff --git a/website/docs/zh/coro_rpc/coro_rpc_introduction.md b/website/docs/zh/coro_rpc/coro_rpc_introduction.md index 2c07e4958..1207e0060 100644 --- a/website/docs/zh/coro_rpc/coro_rpc_introduction.md +++ b/website/docs/zh/coro_rpc/coro_rpc_introduction.md @@ -1,7 +1,7 @@ # coro_rpc简介 -coro_rpc是用C++20开发的基于无栈协程和编译期反射的高性能的rpc库,在单机上echo测试qps达到2000万(详情见benchmark部分) +coro_rpc是用C++20开发的基于无栈协程和编译期反射的高性能的rpc库,在96核cpu的单机上echo测试qps达到2000万(pipeline模式)或450万(ping-pong模式,2000连接)(详情见benchmark部分) ,性能远高于grpc和brpc等rpc库。然而高性能不是它的主要特色,coro_rpc的主要特色是易用性,免安装,包含头文件就可以用,几行代码就可以完成一个rpc服务器和客户端。 coro_rpc的设计理念是:以易用性为核心,回归rpc本质,让用户专注于业务逻辑而不是rpc框架细节,几行代码就可以完成rpc开发。 diff --git a/website/docs/zh/coro_rpc/coro_rpc_server.md b/website/docs/zh/coro_rpc/coro_rpc_server.md index 431b5c013..3400c17c1 100644 --- a/website/docs/zh/coro_rpc/coro_rpc_server.md +++ b/website/docs/zh/coro_rpc/coro_rpc_server.md @@ -26,7 +26,7 @@ int start_server() { } ``` -如果不想阻塞当前线程,我们也允许通过`async_start()`异步启动一个服务器,该函数返回后,保证服务器已经开始监听端口(或发生错误)。用户可以通过检查`Future::hasResult()`来判断服务器当前是否启动成功并正常运行。调用`Future::get()`方法则可以等待服务器停止。 +如果不想阻塞当前线程,我们也允许通过`async_start()`异步启动一个服务器,该函数返回后,保证服务器已经开始监听端口(或发生错误)。用户可以通过检查`async_simple::Future::hasResult()`来判断服务器当前是否启动成功并正常运行。调用`async_simple::Future::get()`方法则可以等待服务器停止。 ```cpp int start_server() { @@ -91,7 +91,7 @@ void test() { } ``` -attachment是rpc请求额外附带的一段数据,coro_rpc不会对其进行序列化,用户可以获取请求附带的attachment的视图,或者将其从上下文中释放单独移动走。同样用户也可以设置回复给rpc客户端的attachment。 +An attachment is an additional piece of data that comes with an RPC request. Coro_rpc does not serialize it, allowing users to obtain a view of the attachment that accompanies the request, or to release it from the context and move it separately. Similarly, users can also set the attachment to be sent back to the RPC client. ### 错误处理 @@ -236,7 +236,7 @@ void echo(coro_rpc::context ctx, std::string_view param) { ### 获取上下文信息 -在协程函数中,我们可以调用`coro_rpc::context::get_context_info()`来获取协程的上下文信息。此外,在rpc函数返回之前也可以使用`coro_io::get_context()`获取上下文信息。 +在回调函数中,我们可以调用`coro_rpc::context::get_context_info()`来获取协程的上下文信息。此外,在rpc函数返回之前也可以使用`coro_io::get_context()`获取上下文信息。但是当rpc函数返回以后,通过`coro_io::get_context()`指向的上下文信息可能会被修改或变得无效,因此我们还是建议使用`coro_rpc::context::get_context_info()`来获取上下文信息。 ```cpp void echo(coro_rpc::context ctx) { @@ -264,17 +264,11 @@ rpc错误码是一个16位的无符号整数。其中,0-255是保留给rpc框 ## 连接与IO线程 -服务器会将rpc连接绑定到其内部的一个IO线程池中。每次新建连接时,通过轮转法,选择一个固定的IO线程绑定到连接上。随后的rpc函数将会在该IO线程上执行。 +服务器内部有一个IO线程池,其大小默认为cpu的逻辑线程数目。当服务器启动后,它会在某个IO线程上启动一个监听任务,接收客户端发来的连接。每次接收连接时,服务器会通过轮转法,选择一个IO线程将其绑定到连接上。随后,该连接上各请求收发数据,序列化,rpc路由等步骤都会在该IO线程上执行。rpc函数也同样会在该IO线程上执行。 + +这意味着,如果你的rpc函数会阻塞当前线程(例如线程sleep,同步读写文件),那么最好通过异步化来避免阻塞io线程,从而避免阻塞其他请求。例如,`async_simple::coro`提供了协程锁`Mutex`和`Spinlock`,提供了将异步任务包装为协程任务的`Promise`和`Future`组件。`coro_io`提供了基于协程的异步文件读写,socket的异步读写,`sleep`和定时器`period_timer`,还可通过`coro_io::post`将重CPU任务提交给全局的阻塞任务线程池。`coro_rpc`/`coro_http`提供了基于协程的异步rpc调用和http调用。`easylog`默认会将日志内容提交给后台线程写入,从而保证前台不阻塞。 -客户端默认会将rpc连接绑定到通过`coro_io::get_global_executor()`获取的固定IO线程上。每次调用协程函数`connect`,`call`,`send_request`,都会让出该协程,并当IO任务完成后,切换到对应的IO线程上继续执行协程。 -你也可以手动指定绑定的IO线程。 -```cpp -auto executor = coro_io::get_global_executor(); -coro_rpc_client cli(executor); -co_await cli.connect("127.0.0.1:8801"); //协程会在此刻让出,当IO任务完成时,切换到executor对应的io线程上执行. -//以下代码会在executor对应的IO线程上执行。 -``` ## 参数与返回值类型 @@ -296,27 +290,46 @@ void echo(coro_rpc::context ctx) { } ``` +## SSL支持 + +coro_rpc支持使用openssl对连接进行加密。在安装openssl并使用cmake find_package/fetch_content 将yalantinglibs导入到你的工程后,可以打开cmake选项`YLT_ENABLE_SSL=ON`启用ssl支持。或者,你也可以手动添加宏`YLT_ENABLE_SSL`并手动链接openssl。 + +当启用ssl支持后,用户可以调用`init_ssl`函数,然后再连接到服务器。这会使得客户端与服务器之间建立加密的链接。需要注意的是,coro_rpc服务端在编译时也必须启用ssl支持。 + +```cpp +coro_rpc_server server; +server.init_ssl({ + .base_path = "./", // ssl文件的基本路径 + .cert_file = "server.crt", // 证书相对于base_path的路径 + .key_file = "server.key" // 私钥相对于base_path的路径 +}); +``` + +启用ssl支持后,服务器将拒绝一切非ssl连接。 + ## 高级设置 我们提供了coro_rpc::config_t类,用户可以通过该类型设置server的细节: ```cpp struct config_base { - bool is_enable_tcp_no_delay = true; - uint16_t port = 9001; - unsigned thread_num = std::thread::hardware_concurrency(); - std::chrono::steady_clock::duration conn_timeout_duration = - std::chrono::seconds{0}; - std::string address="0.0.0.0"; -}; -coro_rpc::config_t config{ - .is_enable_tcp_no_delay = true /*tcp请求是否立即响应*/ - .port = 8801 /*监听端口*/ - .thread_num = std::thread::hardware_concurrency()/*rpc server内部使用的连接数,默认为逻辑核数*/ - .conn_timeout_duration = std::chrono::seconds{0} /*rpc请求的超时时间,0秒代表rpc请求不会自动超时*/ - .address="0.0.0.0" /*监听地址*/ + bool is_enable_tcp_no_delay = true; /*tcp请求是否立即响应*/ + uint16_t port = 9001; /*监听端口*/ + unsigned thread_num = std::thread::hardware_concurrency(); /*rpc server内部使用的连接数,默认为逻辑核数*/ + std::chrono::steady_clock::duration conn_timeout_duration = + std::chrono::seconds{0}; /*rpc请求的超时时间,0秒代表rpc请求不会自动超时*/ + std::string address="0.0.0.0"; /*监听地址*/ + /*下面设置只有启用SSL才有*/ + std::optional ssl_config = std::nullopt; // 配置是否启用ssl }; +struct ssl_configure { + std::string base_path; // ssl文件的基本路径 + std::string cert_file; // 证书相对于base_path的路径 + std::string key_file; // 私钥相对于base_path的路径 + std::string dh_file; // dh_file相对于base_path的路径(可选) +} int start() { + coro_rpc::config_t config{}; coro_rpc_server server(config); /*regist rpc function here... */ server.start(); @@ -350,9 +363,10 @@ struct dummy { int main() { coro_rpc_server server; dummy d{}; - server.register_handler<&dummy::echo,&dummy::coroutine_echo,&dummy::callback_echo>(&d); //注册成员函数 + server.register_handler<&dummy::echo,&dummy::coroutine_echo,&dummy::callback_echo>(&d); // 注册成员函数 server.start(); } +``` 需要注意的时,必须注意被注册的dummy类型的生命周期,保证在服务器启动时dummy始终存活。否则调用行为是未定义的。 @@ -400,7 +414,7 @@ T echo(T param) { return param; } using namespace coro_rpc; int main() { coro_rpc_server server; - server.register_handler,echo,echo>>(&d); //注册成员函数 + server.register_handler,echo,echo>>(&d); // 注册特化的模板函数 server.start(); } ```