This tutorial provides an overview of writing RCFProto servers and clients.
For fully functional RCFProto demo servers and clients, in all four supported languages, complete with build instructions for common toolsets, please refer to the demo directories in the RCFProto distributions.
Let's start with an example of a RCFProto client communicating with a RCFProto server.
The first thing we will need is a Protocol Buffers interface, to describe the interaction between the client and the server.
Protocol Buffers provides a simple language for defining message types and services. We won't cover the details of this language here, as it's features are well documented on the Protocol Buffers website. For the purposes of this tutorial, we want to define a service with a single method, that executes a search request and returns search hits.
Protocol Buffers messages and services are defined in .proto files. The
following file, named Tutorial.proto
,
defines a service named SearchService
,
with associated SearchRequest
and SearchResponse
message
types :
// Tutorial.proto // Enable generation of Protocol Buffers RPC services. option cc_generic_services = true; option java_generic_services = true; option py_generic_services = true; // Defining a search service. // Define a request. message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3 [default = 10]; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } optional Corpus corpus = 4 [default = UNIVERSAL]; } // Define a response message SearchResponse { repeated group Result = 1 { required string url = 2; optional string title = 3; repeated string snippets = 4; } } // Define the service. service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }
To make use of this interface in our program, we first need to use the
Protocol Buffers compiler, protoc
,
to generate the source code corresponding to the interface. This example
will be in C++, so we instruct protoc
to generate C++ source code:
protoc Tutorial.proto --cpp_out=.
This will produce two files, Tutorial.pb.cc
and Tutorial.pb.h
, in the same directory as Tutorial.proto
. Tutorial.pb.h
provides C++ definitions of message
and service types, and will be included into our source code. Tutorial.pb.cc
provides the corresponding source code for the message and service types,
and needs to be compiled into both the server and the client.
Here is the complete source code for a C++ RCFProto server, implementing the service we have defined:
#include <iostream> #include <RCFProto.hpp> // Include protoc-generated header. #include "../bin/Tutorial.pb.h" using namespace google::protobuf; // SearchService implementation. class SearchServiceImpl : public SearchService { public: // Search() method implementation. void Search( RpcController * controller, const SearchRequest * request, SearchResponse * response, Closure * done) { // Fill in the response. SearchResponse_Result * result = response->add_result(); result->set_title("First result"); result->set_url("http://acme.org/"); result->add_snippets("A snippet from acme.org."); result = response->add_result(); result->set_title("Second result"); result->set_url("http://acme.org/abc"); result->add_snippets("Another snippet from acme.org."); // Send response back to the client. done->Run(); } }; int main() { try { // Initialize RCFProto. RCF::init(); // Create server. RCF::RcfProtoServer server( RCF::TcpEndpoint("0.0.0.0", 50001) ); // Bind Protobuf service. SearchServiceImpl searchServiceImpl; server.bindService(searchServiceImpl); // Start the server. server.start(); // Wait for clients. std::cout << "Press Enter to exit." << std::endl; std::cin.get(); } catch(const RCF::Exception & e) { std::cout << "RCF::Exception: " << e.getErrorString() << std::endl; return 1; } return 0; }
Let's now step through this line by line.
We need to include RCFProto headers:
#include <RCFProto.hpp>
We need to include the generated headers for the Protocol Buffers message and service definitions:
// Include protoc-generated header. #include "../bin/Tutorial.pb.h"
SearchService
is an abstract
interface class generated by Protocol Buffers. Our implementation must
derive from this class, and implement the Search()
method:
// SearchService implementation. class SearchServiceImpl : public SearchService { public: // Search() method implementation. void Search( RpcController * controller, const SearchRequest * request, SearchResponse * response, Closure * done) { // Fill in the response. SearchResponse_Result * result = response->add_result(); result->set_title("First result"); result->set_url("http://acme.org/"); result->add_snippets("A snippet from acme.org."); result = response->add_result(); result->set_title("Second result"); result->set_url("http://acme.org/abc"); result->add_snippets("Another snippet from acme.org."); // Send response back to the client. done->Run(); } };
RpcController
and Closure
are classes from Protocol Buffers,
that allow us to control the server-side execution of a remote call. For
a simple implementation like this one, it's enough to call Closure::Run()
at the bottom of the implementation function, to signal that the response
is ready to be sent back to the client:
// Send response back to the client. done->Run();
In main()
,
we need to initialize RCFProto:
// Initialize RCFProto. RCF::init();
We create the server, and pass in the endpoint we want to the server to listen on:
// Create server. RCF::RcfProtoServer server( RCF::TcpEndpoint("0.0.0.0", 50001) );
We have provided 0.0.0.0
as the IP address of the TCP endpoint,
in order to listen on all available network interfaces of the local system.
We bind our SearchService
implementation to the server:
// Bind Protobuf service. SearchServiceImpl searchServiceImpl; server.bindService(searchServiceImpl);
Finally, we start the server and wait for clients to call in:
// Start the server. server.start(); // Wait for clients. std::cout << "Press Enter to exit." << std::endl; std::cin.get();
Here is the complete C++ code for a RCFProto client calling the server:
#include <iostream> #include <RCFProto.hpp> // Include protoc-generated header. #include "../bin/Tutorial.pb.h" // TextFormat::PrintToString() #include <google/protobuf/text_format.h> using namespace google::protobuf; int main() { try { // Initialize RCFProto. RCF::init(); // Create request object. SearchRequest request; request.set_query("something to search for"); request.set_result_per_page(10); request.set_page_number(0); request.set_corpus(SearchRequest_Corpus_NEWS); // Create response object. SearchResponse response; // Create channel. RCF::RcfProtoChannel channel( RCF::TcpEndpoint("127.0.0.1", 50001) ); // Create service stub. SearchService::Stub stub(&channel); // Print out request. std::string strRequest; TextFormat::PrintToString(request, &strRequest); std::cout << "Sending request:" << std::endl; std::cout << strRequest << std::endl; // Make a synchronous remote call to server. stub.Search(NULL, &request, &response, NULL); // Print out response. std::string strResponse; TextFormat::PrintToString(response, &strResponse); std::cout << "Received response:" << std::endl; std::cout << strResponse << std::endl; } catch(const RCF::Exception & e) { std::cout << "RCF::Exception: " << e.getErrorString() << std::endl; return 1; } return 0; }
Let's step through this line by line.
We need to include the RCFProto headers:
#include <RCFProto.hpp>
We need to include the generated Protocol Buffers headers:
// Include protoc-generated header. #include "../bin/Tutorial.pb.h"
We are also including the Protocol Buffers header for the TextFormat
class, in order to be able
to convert Protocol Buffers messages to text format, for log output:
// TextFormat::PrintToString() #include <google/protobuf/text_format.h>
In main()
,
we start by initializing RCFProto:
// Initialize RCFProto. RCF::init();
Protocol Buffers has defined the SearchRequest
and SearchResponse
classes
for us. We create a SearchRequest
object:
// Create request object. SearchRequest request; request.set_query("something to search for"); request.set_result_per_page(10); request.set_page_number(0); request.set_corpus(SearchRequest_Corpus_NEWS);
, as well as a SearchResponse
object:
// Create response object. SearchResponse response;
Before we can send the request, we need to create a RcfProtoChannel
to send it over. In this case the server is listening on TCP port 50001
on the local network interface.
// Create channel. RCF::RcfProtoChannel channel( RCF::TcpEndpoint("127.0.0.1", 50001) );
Protocol Buffers has generated a service stub type, which we instantiate:
// Create service stub. SearchService::Stub stub(&channel);
Let's print the request:
// Print out request. std::string strRequest; TextFormat::PrintToString(request, &strRequest); std::cout << "Sending request:" << std::endl; std::cout << strRequest << std::endl;
Protocol Buffers generates a service stub that can function both synchronously
and asynchronously. In this case we are making a synchronous remote call,
so we call Search()
with NULL
as the first
and fourth parameter:
// Make a synchronous remote call to server. stub.Search(NULL, &request, &response, NULL);
When the call to Search()
returns, the server response is in the
response
variable. Let's
print it out:
// Print out response. std::string strResponse; TextFormat::PrintToString(response, &strResponse); std::cout << "Received response:" << std::endl; std::cout << strResponse << std::endl;
The RCFProto bindings for C#, Java and Python are very similar to the C++ bindings. Generally any RCFProto class or method available in C++, will be available in C#, Java and Python, with the same spelling, but possibly a different leading case.
The following sections provide line-by-line translations of the client and server example above, in C#, Java and Python.
To use RCFProto in C#, you need to reference the RCFProto_NET.dll
and Google.ProtocolBuffers.dll
assemblies. RCFProto C# classes are
defined in the DeltaVSoft.RCFProto
namespace.
To generate C# code for your message and service types, use ProtoGen.exe
:
ProtoGen.exe Tutorial.proto -service_generator_type=GENERIC
For more information on building C# applications, see Appendix - Building.
using DeltaVSoft.RCFProto; // SearchService implementation. class SearchServiceImpl : SearchService { public override void Search( Google.ProtocolBuffers.IRpcController controller, SearchRequest request, System.Action<SearchResponse> done) { // Build the response. SearchResponse.Builder responseBuilder = SearchResponse.CreateBuilder(); SearchResponse.Types.Result result = SearchResponse.Types.Result.CreateBuilder() .SetTitle("First result") .SetUrl("http://acme.org/") .Build(); responseBuilder.AddResult(result); result = SearchResponse.Types.Result.CreateBuilder() .SetTitle("Second result") .SetUrl("http://acme.org/abc") .AddSnippets("Another snippet from acme.org.") .Build(); responseBuilder.AddResult(result); SearchResponse response = responseBuilder.Build(); // Send response back to the client. done(response); } } class TutorialServer { static int Main(string[] args) { try { // Initialize RCFProto. RCFProto.Init(); // Create server. RcfProtoServer server = new RcfProtoServer(new TcpEndpoint("0.0.0.0", 50001)); // Bind Protobuf service. SearchServiceImpl searchServiceImpl = new SearchServiceImpl(); server.BindService(searchServiceImpl); // Start the server. server.Start(); // Wait for clients. System.Console.WriteLine("Press Enter to exit."); System.Console.ReadLine(); } catch (System.Exception e) { System.Console.WriteLine("Exception: " + e.Message); return 1; } return 0; } }
using DeltaVSoft.RCFProto; class TutorialClient { static int Main(string[] args) { try { // Initialize RCFProto. RCFProto.Init(); // Create request object. SearchRequest request = SearchRequest.CreateBuilder() .SetQuery("something to search for") .SetResultPerPage(10) .SetPageNumber(0) .SetCorpus(SearchRequest.Types.Corpus.NEWS) .Build(); // Create channel. RcfProtoChannel channel = new RcfProtoChannel( new TcpEndpoint("127.0.0.1", 50001) ); // Create service stub. SearchService.Stub stub = new SearchService.Stub(channel); // Print out request. System.Console.WriteLine("Sending request:"); System.Console.WriteLine(request.ToString()); // Make a synchronous remote call to server. stub.Search(null, request, null); SearchResponse response = (SearchResponse)channel.GetResponse(); // Print out response. System.Console.WriteLine("Received response:"); System.Console.WriteLine(response.ToString()); } catch (System.Exception e) { System.Console.WriteLine("Exception: " + e.Message); return 1; } return 0; } }
To use RCFProto in Java, you need to have RCFProto.jar
and the relevant Google Protocol Buffers .jar
file, on your class path. RCFProto
Java classes are defined in the com.deltavsoft.rcfproto
namespace.
To generate Java code for your message and service types, use protoc
:
protoc Tutorial.proto --java_out=.
For more information on building Java applications, see Appendix - Building.
import com.deltavsoft.rcfproto.*; public class TutorialServer { // SearchService implementation. static class SearchServiceImpl extends Tutorial.SearchService { public void search( com.google.protobuf.RpcController controller, Tutorial.SearchRequest request, com.google.protobuf.RpcCallback<Tutorial.SearchResponse> done) { // Build the response. Tutorial.SearchResponse.Builder responseBuilder = Tutorial.SearchResponse.newBuilder(); Tutorial.SearchResponse.Result result = Tutorial.SearchResponse.Result.newBuilder() .setTitle("First result") .setUrl("http://acme.org/") .build(); responseBuilder.addResult(result); result = Tutorial.SearchResponse.Result.newBuilder() .setTitle("Second result") .setUrl("http://acme.org/abc") .addSnippets("Another snippet from acme.org.") .build(); responseBuilder.addResult(result); Tutorial.SearchResponse response = responseBuilder.build(); // Send response back to the client. done.run(response); } } public static void main(String[] args) { try { // Initialize RCFProto. RCFProto.init(); // Create server. RcfProtoServer server = new RcfProtoServer(new TcpEndpoint("0.0.0.0", 50001)); // Bind Protobuf service. SearchServiceImpl searchServiceImpl = new SearchServiceImpl(); server.bindService(searchServiceImpl); // Start the server. server.start(); // Wait for clients. System.out.println("Press Enter to exit."); java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); reader.readLine(); } catch (Exception e) { System.out.println("Exception: " + e.getMessage()); } } }
import com.deltavsoft.rcfproto.*; public class TutorialClient { public static void main(String[] args) { try { // Initialize RCFProto. RCFProto.init(); // Create request object. Tutorial.SearchRequest request = Tutorial.SearchRequest.newBuilder() .setQuery("something to search for") .setResultPerPage(10) .setPageNumber(0) .setCorpus(Tutorial.SearchRequest.Corpus.NEWS) .build(); // Create channel. RcfProtoChannel channel = new RcfProtoChannel( new TcpEndpoint("127.0.0.1", 50001) ); // Create service stub. Tutorial.SearchService.Stub stub = Tutorial.SearchService.newStub(channel); // Print out request. System.out.println("Sending request:"); System.out.println(request.toString()); // Make a synchronous remote call to server. stub.search(null, request, null); Tutorial.SearchResponse response = (Tutorial.SearchResponse)channel.getResponse(); // Print out response. System.out.println("Received response:"); System.out.println(response.toString()); } catch (Exception e) { System.out.println("Exception: " + e.getMessage()); } } }
To use RCFProto in Python, you need to install the RCFProto and Google
Protocol Buffers Python modules. RCFProto Python classes are defined in
the deltavsoft.rcfproto
package.
To generate Python code for your message and service types, use protoc
:
protoc Tutorial.proto --python_out=.
For more information on building Python applications, see Appendix - Building.
import sys # Import RCFProto. from deltavsoft.rcfproto import * # Import protoc-generated module with message and service types. import Tutorial_pb2 # SearchService implementation. class SearchServiceImpl(Tutorial_pb2.SearchService) : def Search(self, controller, request, done): # Build the response. response = Demo_pb2.SearchResponse() result = response.result.add() result.title = 'First result' result.url = 'http://acme.org' result = response.result.add() result.title = 'Second result' result.url = 'http://acme.org/abc' result.snippets.append('Another snippet from acme.org.') # Send response back to the client. done(response) # Initialize RCFProto. init() # Create server. server = RcfProtoServer( TcpEndpoint("0.0.0.0", 50001) ); # Bind Protobuf service. svc = SearchServiceImpl(); server.BindService(svc); # Start the server. server.Start(); # Wait for clients. print('Press Enter to exit.'); sys.stdin.readline()
# Import RCFProto. from deltavsoft.rcfproto import * # Import protoc-generated module with message and service types. import Tutorial_pb2 # Initialize RCFProto. init(); # Create request object. request = Tutorial_pb2.SearchRequest() request.query = 'Something to search for' request.result_per_page = 10 request.page_number = 0 request.corpus = Tutorial_pb2.SearchRequest.NEWS # Create channel. channel = RcfProtoChannel( TcpEndpoint("127.0.0.1", 50001) ); # Create service stub. stub = Tutorial_pb2.SearchService_Stub(channel); # Print out request. print('Sending request:'); print( str(request) ); # Make a synchronous remote call to server. stub.Search(None, request, None); response = channel.GetResponse(); # Print out response. print 'Received response:'; print( str(response) );
RCFProto client-side configuration is done using the RcfProtoChannel
class.
For example, to set connect timeouts and remote call timeouts:
RCF::RcfProtoChannel channel( RCF::TcpEndpoint("127.0.0.1", 50001) ); // 5s connect timeout. channel.setConnectTimeoutMs(5*1000); // 10s remote call timeout. channel.setRemoteCallTimeoutMs(10*1000);
You can configure transport protocols:
// Enable transport level compression. channel.setEnableCompression(true); // Configure NTLM transport protocol. channel.setTransportProtocol(RCF::Tp_Ntlm);
For more information on client-side configuration, see the relevant reference
documentation for RcfProtoChannel
.
RCFProto server-side configuration is done using the RcfProtoServer
and RcfProtoSession
classes.
Configuring server endpoints:
// RcfProtoServer listening on single endpoint. RCF::RcfProtoServer server( RCF::TcpEndpoint(50001) );
// RcfProtoServer listening on multiple endpoints. RCF::RcfProtoServer server; server.addEndpoint( RCF::TcpEndpoint(50001) ); server.addEndpoint( RCF::Win32NamedPipeEndpoint("MyPipe") );
Configuring server-side threading:
// Configure a thread pool with number of threads varying from 1 to 50. RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(1, 50) ); server.setThreadPool(threadPoolPtr);
Configuring which transport protocols clients are required to use:
std::vector<RCF::TransportProtocol> protocols; protocols.push_back(RCF::Tp_Ntlm); protocols.push_back(RCF::Tp_Kerberos); server.setSupportedTransportProtocols(protocols);
From the server-side implementation of a remote call, you have access to
a RcfProtoSession
object,
representing the current client session. Here is how you would obtain the
current RcfProtoSession
in
C++:
// Search() method implementation. void Search( RpcController * controller, const SearchRequest * request, SearchResponse * response, Closure * done) { RCF::RcfProtoController * rcfController = static_cast<RCF::RcfProtoController *>(controller); RCF::RcfProtoSession * pSession = rcfController->getSession(); // Fill in the response. // ... // Send it back to the client. done->Run(); }
Here is how you obtain the current RcfProtoSession
in C#:
// Search() method implemenation. public override void Search( Google.ProtocolBuffers.IRpcController controller, SearchRequest request, System.Action<SearchResponse> done) { // Obtain RcfProtoSession for current client connection. RcfProtoController rcfController = (RcfProtoController)controller; RcfProtoSession session = rcfController.GetSession(); // Create response. SearchResponse response = SearchResponse.CreateBuilder().Build(); // Send it back to the client. done(response); }
Here is how you obtain the current RcfProtoSession
in Java:
public void search( com.google.protobuf.RpcController controller, Tutorial.SearchRequest request, com.google.protobuf.RpcCallback<Tutorial.SearchResponse> done) { // Obtain RcfProtoSession for current client connection. RcfProtoController rcfController = (RcfProtoController)controller; RcfProtoSession session = rcfController.getSession(); // Create response. Tutorial.SearchResponse response = Tutorial.SearchResponse.newBuilder().build(); // Send it back to the client. done.run(response); }
Here is how you obtain the current RcfProtoSession
in Python:
# Search() method implementation. def Search(self, controller, request, done): # Obtain RcfProtoSession for current client connection. session = controller.GetSession(); # Create response. response = Tutorial_pb2.SearchResponse() # Send it back to the client. done(response)
From RcfProtoSession
, you
can retrieve various details about the client.
You can determine the transport type and transport protocol in use:
RCF::TransportType transportType = pSession->getTransportType(); RCF::TransportProtocol protocol = pSession->getTransportProtocol();
If the transport protocol is NTLM or Kerberos, or if the transport is a Win32 named pipe, you can determine the Windows username of the client:
if ( (protocol == RCF::Tp_Ntlm || protocol == RCF::Tp_Kerberos) || transportType == RCF::Tt_Win32NamedPipe) { std::string clientUsername = pSession->getClientUsername(); }
For more information on server-side configuration, see the relevant reference
documentation for RcfProtoServer
and RcfProtoSession
.
When you call remote methods on a client stub, RCFProto's default behavior is to make a synchronous call. This means that the method does not return until a response is received from the server, or an error or timeout occurs. Synchronous calls are conceptually simple and easy to program. However, they also have the drawback that during the call, your application does not have control of the calling thread. In some situations this may not be acceptable, in which case you will need to use asynchronous remote calls instead.
When making an asynchronous call, you need to pass in a completion handler that RCFProto will call when the call completes. The call is initiated on your own application thread, but the thread does not block and returns immediately to the application. Meanwhile the call completes on a background RCFProto thread, and when it does, your completion callback is called.
Here is an example of an asynchronous call in C++:
// Remote call completion handler. void onCallCompletion(RCF::RcfProtoController * pController, SearchRequest * pRequest, SearchResponse * pResponse) { // Check for any errors first. if (pController->Failed()) { std::cout << "Error: " << pController->ErrorText() << std::endl; } else { std::string strResponse; TextFormat::PrintToString(*pResponse, &strResponse); std::cout << "Received response:" << std::endl; std::cout << strResponse << std::endl; } }
// Executing an asynchronous remote call. channel.setAsynchronousRpcMode(true); RCF::RcfProtoController controller; RCF::BoostBindClosure closure( boost::bind(&onCallCompletion, &controller, &request, &response) ); stub.Search(&controller, &request, &response, &closure);
RcfProtoController
is used
to monitor the asynchronous remote call while it is in progress:
// RcfProtoController is used to check on the progress of the asynchronous remote call, or to cancel it. // Poll for completion... while (!controller.Completed()) { Sleep(500); } // ... or cancel the remote call. controller.StartCancel(); // After the remote call completes, check if it failed. if (controller.Failed()) { std::string errorMsg = controller.ErrorText(); }
Here is an example of an asynchronous remote call in C#:
// Remote call completion handler. static void onCallCompletion(RcfProtoController controller, SearchRequest request, SearchResponse response) { // Check for any errors first. if (controller.Failed) { System.Console.WriteLine( "Error: " + controller.ErrorText ); } else { System.Console.WriteLine( "Received response:" ); System.Console.WriteLine( response.ToString() ); } }
// Executing an asynchronous remote call. channel.SetAsynchronousRpcMode(true); RcfProtoController controller = new RcfProtoController(); System.Action<SearchResponse> done = delegate(SearchResponse response) { onCallCompletion(controller, request, response); }; stub.Search(controller, request, done);
// RcfProtoController is used to check on the progress of the asynchronous remote call, or to cancel it. // Poll for completion... while ( !controller.Completed ) { System.Threading.Thread.Sleep(500); } // ... or cancel the remote call. controller.StartCancel(); // After the remote call completes, check if it failed. if (controller.Failed) { string errorMsg = controller.ErrorText; }
Here is an example of an asynchronous remote call in Java:
// Remote call completion handler. static class OnRemoteCallCompleted implements RpcCallback<Tutorial.SearchResponse> { OnRemoteCallCompleted(RcfProtoController controller) { mController = controller; } public void run(Tutorial.SearchResponse response) { if ( mController.failed() ) { System.out.println( "Error: " + mController.errorText() ); } else { System.out.println("Received response:"); System.out.println(response.toString()); } } private RcfProtoController mController; }
// Executing an asynchronous remote call. channel.setAsynchronousRpcMode(true); RcfProtoController controller = new RcfProtoController(); OnRemoteCallCompleted done = new OnRemoteCallCompleted(controller); stub.search(controller, request, done);
// RcfProtoController is used to check on the progress of the asynchronous remote call, or to cancel it. // Poll for completion... while ( !controller.completed() ) { Thread.sleep(500); } // ... or cancel the remote call. controller.startCancel(); // After the remote call completes, check if it failed. if (controller.failed()) { String errorMsg = controller.errorText(); }
Here is an example of an asynchronous remote call in Python:
# Remote call completion handler. def OnRemoteCallCompleted(controller, response): if controller.Failed(): print "Error: " + controller.ErrorText() else: print( 'Received response:' ) print( str(response) ) # Executing an asynchronous remote call. channel.SetAsynchronousRpcMode(True) controller = RcfProtoController() stub.Search(controller, request, lambda response: OnRemoteCallCompleted(controller, response))
# RcfProtoController is used to check on the progress of the asynchronous remote call, or to cancel it. # Poll for completion... while not controller.Completed(): time.sleep(.5); # ... or cancel the remote call. controller.StartCancel(); # After the remote call completes, check if it failed. if controller.Failed(): errorMsg = controller.ErrorText()
This section provides an overview of the various transports that RCFProto supports.
TcpEndpoint
represents
a TCP endpoint. It is important to remember that if you construct a TcpEndpoint
without passing in an IP
address, the loopback address 127.0.0.1
is assumed.
For servers, this means that the server will only be accessible from the
local machine. To create a TCP endpoint that is accessible from across
the network, use 0.0.0.0
(for IPV4), or ::0
(for IPV6).
Here are some examples of servers and clients using TcpEndpoint
.
// Listen on port 50001 on all available IPv4 network interfaces. RCF::RcfProtoServer server( RCF::TcpEndpoint("0.0.0.0", 50001) );
// Listen on port 50001 on all available IPv6 network interfaces. RCF::RcfProtoServer server( RCF::TcpEndpoint("::0", 50001) );
// Listen on loopback IPv4 network interface. RCF::RcfProtoServer server( RCF::TcpEndpoint("127.0.0.1", 50001) );
// Listen on loopback IPv4 network interface. RCF::RcfProtoServer server( RCF::TcpEndpoint(50001) );
// Listen on dynamically assigned port on loopback IPv4 network interface. RCF::RcfProtoServer server( RCF::TcpEndpoint(0) ); server.start(); int port = server.getIpServerTransport().getPort();
// Connect to server on port 50001 of server.corp.com . RCF::RcfProtoChannel channel( RCF::TcpEndpoint("server.corp.com", 50001) );
RCFProto provides HTTP and HTTPS based transports, in case you need to tunnel through an HTTP proxy to reach your server.
Here are some examples of servers and clients using HttpEndpoint
and HttpsEndpoint
.
// Listen on port 80 of all available IPv4 network interfaces. RCF::RcfProtoServer server( RCF::HttpEndpoint("0.0.0.0", 80) );
// Connect to port 80 of server.corp.com. RCF::RcfProtoChannel channel( RCF::HttpEndpoint("server.corp.com", 80) ); // Configure HTTP proxy. channel.setHttpProxy("proxy.corp.com"); channel.setHttpProxyPort(8080);
// Listen on port 443 of all available IPv4 network interfaces. RCF::RcfProtoServer server( RCF::HttpsEndpoint("0.0.0.0", 443) ); // Configure certificate for OpenSSL-based SSL. RCF::CertificatePtr certPtr( new RCF::PemCertificate("/path/to/certificate.pem", "password") ); server.setCertificate(certPtr);
// Connect to port 443 of server.corp.com. RCF::RcfProtoChannel channel( RCF::HttpEndpoint("server.corp.com", 443) ); // Configure HTTP proxy. channel.setHttpProxy("proxy.corp.com"); channel.setHttpProxyPort(8080);
Certificate configuration for HTTPS is done in the same way as for SSL transports (see further down).
For communication between processes on a single machine, RCF provides Win32 named pipe transport (for Windows), and UNIX local domain socket transports (for UNIX):
// Listen on Win32 named pipe named "MyPipe". RCF::RcfProtoServer server( RCF::Win32NamedPipeEndpoint("MyPipe") );
// Connect to Win32 named pipe named "MyPipe". RCF::RcfProtoChannel channel( RCF::Win32NamedPipeEndpoint("MyPipe") );
// Listen on UNIX local socket named "MyLocalSocket". RCF::RcfProtoServer server( RCF::UnixLocalEndpoint("MyLocalSocket") );
// Connect to UNIX local socket named "MyLocalSocket". RCF::RcfProtoChannel channel( RCF::UnixLocalEndpoint("MyLocalSocket") );
RCFProto supports several different transport protocols, for purposes of compression, encryption and authentication.
Compression can be enabled for a RCFProtoChannel
.
The compression algorithm is stateful and compresses data across multiple
calls for better compression.
channel.setEnableCompression(true);
RCFProto uses the zlib library to implement compression.
On Windows, RCFProto supports NTLM encryption and authentication:
channel.setTransportProtocol(RCF::Tp_Ntlm); // Leave username and password blank, to connect using the logged on users credentials: channel.connect(); // Or specify username and password explicitly: channel.disconnect(); channel.setUsername("SomeDomain\\SomeUser"); channel.setPassword("SomePassword"); channel.connect();
When the NTLM transport protocol is in use, server-side code is able to securely determine the Windows username of the client:
// When using NTLM or Kerberos transport protocols, the client username is available to server-side code. RCF::RcfProtoSession * session = controller.getSession(); std::string clientUsername = session->getClientUsername();
RCFProto also supports Kerberos encryption and authentication:
channel.setTransportProtocol(RCF::Tp_Kerberos); // Kerberos requires the client to specify the SPN (Server Principal Name) of the server. // The Kerberos protocol will verify that the server is running as this account. channel.setKerberosSpn("SomeDomain\\ServerAccount"); // As for NTML, username and password are optional. If not specified, the current users credentials are used. channel.setUsername("SomeDomain\\SomeUser"); channel.setPassword("SomePassword");
When the Kerberos transport protocol is in use, server-side code is able to securely determine the Windows username of the client:
// When using NTLM or Kerberos transport protocols, the client username is available to server-side code. RCF::RcfProtoSession * session = controller.getSession(); std::string clientUsername = session->getClientUsername();
RCFProto provides two separate SSL implementations. One is based on the Schannel SSPI package in Windows, and the other is based on OpenSSL.
On Windows, it's a good idea to explicitly specify which SSL implementation you want to use. This can be done globally:
RCF::setDefaultSslImplementation(RCF::Si_Schannel);
, or for individual servers and clients:
RCF::RcfProtoServer server( RCF::TcpEndpoint(50001) ); server.setSslImplementation(RCF::Si_Schannel);
RCF::RcfProtoChannel channel( RCF::TcpEndpoint(50001) ); channel.setSslImplementation(RCF::Si_Schannel);
Certificate validation can be done through custom validation callbacks, or through OpenSSL-specific or Schannel-specific means.
Here is an example of a custom validation callback:
// Custom certificate validation callback. bool onValidateCertificate(RCF::Certificate * pCert) { RCF::Win32Certificate * pWin32Cert = dynamic_cast<RCF::Win32Certificate *>(pCert); if (pWin32Cert) { // Schannel based certificate. std::cout << "Server certificate details: " << RCF::toAstring(pWin32Cert->getCertificateName()) << std::endl; std::cout << "Server certificate issuer details: " << RCF::toAstring(pWin32Cert->getIssuerName()) << std::endl; } RCF::X509Certificate * pX509Cert = dynamic_cast<RCF::X509Certificate *>(pCert); if (pX509Cert) { // OpenSSL based certificate. std::cout << "Server certificate details: " << pX509Cert->getCertificateName() << std::endl; std::cout << "Server certificate issuer details: " << pX509Cert->getIssuerName() << std::endl; } // Return false or throw an exception to indicate that you don't consider the certificate valid. //return false; //throw std::runtime_error("Invalid server certificate. Reason: ???"); // Return true to indicate that you consider the certificate valid. return true; }
A number of methods are available on RcfProtoChannel
and RcfProtoServer
to configure
certificate and certificate validation. Here is an example of certificate
configuration using OpenSSL:
Server-side OpenSSL configuration:
// Server-side configuration. RCF::RcfProtoServer server( RCF::TcpEndpoint(50001) ); server.setSslImplementation(RCF::Si_OpenSsl); RCF::CertificatePtr certPtr( new RCF::PemCertificate( "/path/to/certificate.pem", "password") ); // Set the certificate that this server will present to clients. server.setCertificate(certPtr);
Client-side OpenSSL configuration:
// Client-side configuration. RCF::RcfProtoChannel channel( RCF::TcpEndpoint(50001) ); channel.setSslImplementation(RCF::Si_OpenSsl); // Certificate validation - provide certificate authority certificate. OpenSSL will verify // the server certificate against the provided CA certificate. RCF::CertificatePtr caCertPtr( new RCF::PemCertificate("/path/to/caCertificate.pem", "") ); channel.setCaCertificate(caCertPtr); // Certificate validation - custom certificate validation callback. channel.setCertificateValidationCallback(onValidateCertificate);
Here is an example of certificate configuration using Schannel:
Server-side Schannel configuration:
// Server-side configuration. RCF::RcfProtoServer server( RCF::TcpEndpoint(50001) ); server.setSslImplementation(RCF::Si_Schannel); RCF::CertificatePtr certPtr( new RCF::PfxCertificate( "/path/to/certificate.pfx", "password", "certificate name") ); // Set the certificate that this server will present to clients. server.setCertificate(certPtr);
Client-side Schannel configuration:
// Client-side configuration. RCF::RcfProtoChannel channel( RCF::TcpEndpoint(50001) ); channel.setSslImplementation(RCF::Si_Schannel); // Certificate validation - use default Schannel validation. Schannel will check that // the server certificate matches the provided name, and that a corresponding certificate // authority is present in the Windows "Trusted Root Certification Authorities" // certificate store. channel.setEnableSchannelCertificateValidation(RCF::toTstring("certificate name")); // Certificate validation - custom certificate validation callback. channel.setCertificateValidationCallback(onValidateCertificate);