diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 663d2eb0..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "workbench.colorCustomizations": {}, - "python.pythonPath": "C:\\Users\\shbanerj\\.conda\\envs\\py-azurefunctions\\python.exe" -} \ No newline at end of file diff --git a/investigations/.gitignore b/investigations/.gitignore new file mode 100644 index 00000000..c9021b75 --- /dev/null +++ b/investigations/.gitignore @@ -0,0 +1,4 @@ +# Visual Studio - C# +**/obj +**/bin +*.user diff --git a/investigations/DotNetGrpc/DotNetGrpc.sln b/investigations/DotNetGrpc/DotNetGrpc.sln new file mode 100644 index 00000000..2ea59cbb --- /dev/null +++ b/investigations/DotNetGrpc/DotNetGrpc.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29424.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetGrpcService", "DotNetGrpcService\DotNetGrpcService.csproj", "{51C71ABB-1D35-4543-898C-0D34EFF832FA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {51C71ABB-1D35-4543-898C-0D34EFF832FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51C71ABB-1D35-4543-898C-0D34EFF832FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51C71ABB-1D35-4543-898C-0D34EFF832FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51C71ABB-1D35-4543-898C-0D34EFF832FA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BB4EB881-58FA-4A49-838A-0F0824C47920} + EndGlobalSection +EndGlobal diff --git a/investigations/DotNetGrpcClient/DotNetGrpcClient.csproj b/investigations/DotNetGrpc/DotNetGrpcClient/DotNetGrpcClient.csproj similarity index 99% rename from investigations/DotNetGrpcClient/DotNetGrpcClient.csproj rename to investigations/DotNetGrpc/DotNetGrpcClient/DotNetGrpcClient.csproj index b9261141..2cea1af5 100644 --- a/investigations/DotNetGrpcClient/DotNetGrpcClient.csproj +++ b/investigations/DotNetGrpc/DotNetGrpcClient/DotNetGrpcClient.csproj @@ -3,8 +3,8 @@ Exe netcoreapp3.0 + - @@ -16,4 +16,5 @@ + diff --git a/investigations/DotNetGrpcClient/Program.cs b/investigations/DotNetGrpc/DotNetGrpcClient/Program.cs similarity index 84% rename from investigations/DotNetGrpcClient/Program.cs rename to investigations/DotNetGrpc/DotNetGrpcClient/Program.cs index a8d9e08c..0bfa033b 100644 --- a/investigations/DotNetGrpcClient/Program.cs +++ b/investigations/DotNetGrpc/DotNetGrpcClient/Program.cs @@ -4,9 +4,9 @@ using DotNetGrpcService; using Grpc.Net.Client; -namespace GrpcGreeterClient +namespace DotNetGrpcClient { - class Program + class Program { static async Task Main(string[] args) { @@ -18,9 +18,6 @@ static async Task Main(string[] args) Console.WriteLine("Calling Python Endpoint..."); await CallEndpoint("http://localhost:50051", true); - - Console.WriteLine("Press any key to exit..."); - Console.ReadKey(); } static GrpcChannel GetChannel(string grpcEndpointAddress, bool isInsecure = false) @@ -35,8 +32,8 @@ static async Task CallEndpoint(string grpcEndpointAddress, bool isInsecure = fal var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( - new HelloRequest { Name = "GreeterClient" }); - Console.WriteLine("Greeting: " + reply.Message); + new HelloRequest { Name = "GreeterClient-DotNet" }); + Console.WriteLine($"Response: {reply.Message}"); } } -} \ No newline at end of file +} diff --git a/investigations/DotNetGrpcService/Protos/greet.proto b/investigations/DotNetGrpc/DotNetGrpcClient/Protos/greet.proto similarity index 99% rename from investigations/DotNetGrpcService/Protos/greet.proto rename to investigations/DotNetGrpc/DotNetGrpcClient/Protos/greet.proto index 62060ee9..55bc6ca8 100644 --- a/investigations/DotNetGrpcService/Protos/greet.proto +++ b/investigations/DotNetGrpc/DotNetGrpcClient/Protos/greet.proto @@ -18,4 +18,4 @@ message HelloRequest { // The response message containing the greetings. message HelloReply { string message = 1; -} +} \ No newline at end of file diff --git a/investigations/DotNetGrpc/DotNetGrpcService/DotNetGrpcService.csproj b/investigations/DotNetGrpc/DotNetGrpcService/DotNetGrpcService.csproj new file mode 100644 index 00000000..b7fb0d20 --- /dev/null +++ b/investigations/DotNetGrpc/DotNetGrpcService/DotNetGrpcService.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + + + + + + + + + + + diff --git a/investigations/DotNetGrpcService/Program.cs b/investigations/DotNetGrpc/DotNetGrpcService/Program.cs similarity index 100% rename from investigations/DotNetGrpcService/Program.cs rename to investigations/DotNetGrpc/DotNetGrpcService/Program.cs diff --git a/investigations/DotNetGrpcClient/Protos/greet.proto b/investigations/DotNetGrpc/DotNetGrpcService/Protos/greet.proto similarity index 100% rename from investigations/DotNetGrpcClient/Protos/greet.proto rename to investigations/DotNetGrpc/DotNetGrpcService/Protos/greet.proto diff --git a/investigations/DotNetGrpcService/Services/GreeterService.cs b/investigations/DotNetGrpc/DotNetGrpcService/Services/GreeterService.cs similarity index 89% rename from investigations/DotNetGrpcService/Services/GreeterService.cs rename to investigations/DotNetGrpc/DotNetGrpcService/Services/GreeterService.cs index 7d7fc516..79f28fea 100644 --- a/investigations/DotNetGrpcService/Services/GreeterService.cs +++ b/investigations/DotNetGrpc/DotNetGrpcService/Services/GreeterService.cs @@ -19,7 +19,7 @@ public override Task SayHello(HelloRequest request, ServerCallContex { return Task.FromResult(new HelloReply { - Message = "Hello " + request.Name + Message = $"Hello {request.Name} from .NET gRPC Server" }); } } diff --git a/investigations/DotNetGrpcService/Startup.cs b/investigations/DotNetGrpc/DotNetGrpcService/Startup.cs similarity index 98% rename from investigations/DotNetGrpcService/Startup.cs rename to investigations/DotNetGrpc/DotNetGrpcService/Startup.cs index d8bf433a..95646a1c 100644 --- a/investigations/DotNetGrpcService/Startup.cs +++ b/investigations/DotNetGrpc/DotNetGrpcService/Startup.cs @@ -31,7 +31,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { - endpoints.MapGrpcService(); endpoints.MapGet("/", async context => diff --git a/investigations/DotNetGrpcService/DotNetGrpcService.csproj b/investigations/DotNetGrpcService/DotNetGrpcService.csproj deleted file mode 100644 index 6dbc3bbd..00000000 --- a/investigations/DotNetGrpcService/DotNetGrpcService.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netcoreapp3.0 - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/investigations/DotNetGrpcService/DotNetGrpcService.sln b/investigations/DotNetGrpcService/DotNetGrpcService.sln deleted file mode 100644 index e2331799..00000000 --- a/investigations/DotNetGrpcService/DotNetGrpcService.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29316.153 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetGrpcService", "DotNetGrpcService.csproj", "{6D265B48-69E0-4511-9E77-A8B6501AAB48}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetGrpcClient", "..\DotNetGrpcClient\DotNetGrpcClient.csproj", "{3E1D7598-32EE-43DC-B9E6-22C3568742FF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6D265B48-69E0-4511-9E77-A8B6501AAB48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D265B48-69E0-4511-9E77-A8B6501AAB48}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D265B48-69E0-4511-9E77-A8B6501AAB48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D265B48-69E0-4511-9E77-A8B6501AAB48}.Release|Any CPU.Build.0 = Release|Any CPU - {3E1D7598-32EE-43DC-B9E6-22C3568742FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E1D7598-32EE-43DC-B9E6-22C3568742FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E1D7598-32EE-43DC-B9E6-22C3568742FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E1D7598-32EE-43DC-B9E6-22C3568742FF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {75B6A36C-EF88-4E3A-83FD-5BD41A270F0C} - EndGlobalSection -EndGlobal diff --git a/investigations/PythonGrpc/greet_client.py b/investigations/PythonGrpc/greet_client.py index bc4780dd..fe7289c5 100644 --- a/investigations/PythonGrpc/greet_client.py +++ b/investigations/PythonGrpc/greet_client.py @@ -4,23 +4,25 @@ import greet_pb2 - def getChannel(grpcEndpointAddress): return grpc.insecure_channel(grpcEndpointAddress) + def sendGreetings(grpcChannel): stub = greet_pb2_grpc.GreeterStub(grpcChannel) - reply = stub.SayHello(greet_pb2.HelloRequest(name='Shashank')) - return reply + reply = stub.SayHello(greet_pb2.HelloRequest(name='GreeterClient-Python')) + return reply.message -def main(): - print ("Send greetings to C# Server...") + +def main(): + print("Calling C# Endpoint...") channel = getChannel("localhost:5000") - print(sendGreetings(channel)) + print(f'Response: {sendGreetings(channel)}') - print ("Send greetings to Python Server...") + print("Calling Python Endpoint...") channel = getChannel("localhost:50051") - print(sendGreetings(channel)) + print(f'Response: {sendGreetings(channel)}') + -if __name__ == '__main__': +if __name__ == '__main__': main() diff --git a/investigations/PythonGrpc/greet_server.py b/investigations/PythonGrpc/greet_server.py index 825ea02f..706858d3 100644 --- a/investigations/PythonGrpc/greet_server.py +++ b/investigations/PythonGrpc/greet_server.py @@ -4,12 +4,14 @@ import greet_pb2_grpc import greet_pb2 + class GreetServer (greet_pb2_grpc.GreeterServicer): - + def SayHello(self, request, context): print(f'Received client request from {request.name}') - return greet_pb2.HelloReply(message='Hello ' + request.name) - + return greet_pb2.HelloReply(message=f'Hello {request.name} from Python gRPC Server') + + def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) greet_pb2_grpc.add_GreeterServicer_to_server(GreetServer(), server) @@ -20,5 +22,5 @@ def serve(): server.wait_for_termination() -if __name__ == '__main__': - serve() \ No newline at end of file +if __name__ == '__main__': + serve() diff --git a/investigations/PythonGrpc/requirements.txt b/investigations/PythonGrpc/requirements.txt new file mode 100644 index 00000000..daa9182c --- /dev/null +++ b/investigations/PythonGrpc/requirements.txt @@ -0,0 +1,2 @@ +flake8 +grpcio-tools \ No newline at end of file diff --git a/investigations/README.md b/investigations/README.md new file mode 100644 index 00000000..e70abfde --- /dev/null +++ b/investigations/README.md @@ -0,0 +1,207 @@ +# gRPC Investigations + +## Introduction + +This folder contains code which shows communication between cross language applications - C# and Python. It is primarly meant for upskilling and understanding the basics of gRPC communication between two different language plaforms and identify tools to support development. + +## Tools used to build this repository + +This code was built on a Windows 10 PC. But the tooling can be considered cross platform. + +- Microsoft Visual Studio 2019 +- Microsoft Visual Studio Code +- Python 3.6.9 on a virtual environemnt enabled via Anaconda + +## Structure of the code + +The repository consists of three folders: + +- **DotNetGrpc\DotNetGrpcService**: Contains the source code for the implementation of the gRPC server in C# on the .NET Core platform. + +- **DotNetGrpc\DotNetGrpcClient**: Contains the source code for the gRPC client implemented in C# on the .NET Core Platform. + +- **PythonGrpc**: Contains the source code for both the gRPC server and client and server using Python 3.6. + +## Client/Server Communication + +### gRPC Contract + +The implementation on both the platform conform the simplest contract, defined by the default .NET template for gRPC Server. + +The contract is defined in the ProtoBuf file ```greet.proto``` available in all the folders. + +The contract is defined as follows: + +- A ```Greeter``` service with a ```SayHello``` method. + - The ```SayHello``` method accepts a object of type - ```HelloRequest``` and responds with object of the type - ```HelloReply```. + - The object of type - ```HelloRequest``` contains a single field of ```string``` type called ```name```. + - The object of the type - ```HelloReply``` contains a single field of ```string``` type called ```message```. + +The code snippet from the protobuf file is shown as below: + + ``` protobuf +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings. +message HelloReply { + string message = 1; +} + + ``` + +## Communicating Across Platforms + +The code exibhits the ```Greet``` contract being used to communicate across platforms. When you launch the gRPC client, they try to communicate with the both the servers by sending the ```HelloRequest``` message, with a name field (GreeterClient) and the servere responds with a ```HelloReply``` containing the mssage field, which is displayed by the client. + +## Gotchas encountered during the investigation + +It was very easy to get C# client and C# server communicating over the gRPC channel. The same was for Python client and server code. The challange came when it came time for them communicate with cross platform. + +### Differences in startup + +The .NET/C# client and server are configured, by default to communicate on secure HTTPS/TLS channel. Python server and client are not. In order to make the communication work, we had to resolve the differences. + +### Resolving the difference - .NET/C# Server + +In order to make the Python client work out of the box, the .NET gRPC server was configured to run on the unenrypted HTTP port. + +To do this, the ```Kestrel``` section of the ```appsettings.json``` file needs to be created/modified as follows: + +``` json +... + + "Kestrel": { + "EndpointDefaults": { + "Url": "http://*.5000", + "Protocols": "Http2" + } + } + +... + +``` + +This allows HTTP communication on port 5000. This setting to be used for **DEVELOPMENT** environments. + +More information on this issue can be found here on this [Github issue](https://github.com/grpc/grpc-dotnet/issues/564). + +### Resolving the difference - .NET/C# Client + +Now that unencrypted channel is also available on the .NET/C# Server, the .NET client also needs to modified to support this, when communicating with the .NET and the Python gRPC server. + +To do that, add the following snippet of code, prior to creating a channel: + +``` C# +AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); +channel = GrpcChannel.ForAddress(grpcEndpointAddress); +``` + +More information of this and more can be found on the [Microsoft .NET gRPC Troubleshooting guide](https://docs.microsoft.com/en-US/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0). + +## Running the servers + +### .NET/C# gRPC Server + +1. Modify the ```appsettings.json``` file, ```investigations\DotNetGrpc\DotNetGrpcServer``` folder, based on the section above. +2. Launch the command shell +3. Starting from the root of the repository, navigate to ```investigations\DotNetGrpc\DotNetGrpcServer``` folder. +4. When executing for the **first time**, on the command shell, run ```dotnet build```. +5. Once the command is executed successfully, run ```dotnet run```. + +Sample Output of the .NET gRPC Server + +``` shell +$ dotnet run +info: Microsoft.Hosting.Lifetime[0] + Now listening on: http://localhost:5000 +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: C:\Projects\CSE\DurableFunctions\azure-functions-durable-python\investigations\DotNetGrpc\DotNetGrpcService +info: Microsoft.AspNetCore.Hosting.Diagnostics[1] +``` + +### Python gRPC Server + +1. Launch the command shell +2. Activate the virtual python environment (if applicable). +3. Starting from the root of the repository, navigate to ```investigations\PythonGrpc``` folder. +4. When executing for the **first time**, run ```pip install -r requirements.txt``` +5. Once the command is executed successfully, run ```python greet_server.py```. + +Sample Output of the .NET gRPC Server + +``` shell +$ python greet_server.py +Starting gRPC Server on port 50051 +Started. Waiting for client connections... +``` + +## Running the gRPC clients + +**Note** +Prior to executing the gRPC clients, ensure that both the .NET/C# and Python gRPC Servers are running. + +### .NET/C# Client + +1. Launch the command shell +2. Starting from the root of the repository, navigate to ```investigations\DotNetGrpc\DotNetGrpcClient``` folder. +3. When executing for the **first time**, on the command shell, run ```dotnet build```. +4. Once the command is executed successfully, run ```dotnet run```. + +Sample Execution of the .NET/C# client + +```bash +$ dotnet run +Calling C# Endpoint... +Response: Hello GreeterClient-DotNet from .NET gRPC Server +Calling Python Endpoint... +Response: Hello GreeterClient-DotNet from Python gRPC Server + +``` + +### Python Client + +1. Launch the command shell +2. Activate the virtual python environment (if applicable). +3. Starting from the root of the repository, navigate to ```investigations\PythonGrpc``` folder. +4. When executing for the **first time**, run ```pip install -r requirements.txt``` +5. Once the command is executed successfully, run ```python greet_client.py```. + +Sample Execution of the Python client + +```bash + +$ python greet_client.py +Calling C# Endpoint... +Response: Hello GreeterClient-Python from .NET gRPC Server +Calling Python Endpoint... +Response: Hello GreeterClient-Python from Python gRPC Server + +``` + +## Current State of client/server communications + +| Servers | C# Client | Python Client | +| ---------- | :--------:| :-------------:| +| **C# Server** | :heavy_check_mark:| :heavy_check_mark:| +| **Python Server** | :heavy_check_mark:| :heavy_check_mark:| + +## Resources + +- [Offical gRPC site](https://grpc.io) +- [gRPC Auth Guide](https://www.grpc.io/docs/guides/auth/) +- [gRPC with ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/grpc/?view=aspnetcore-3.0) +- [Microsoft .NET gRPC Troubleshooting guide](https://docs.microsoft.com/en-US/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0) +- [Krestel - Http2 support](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.0#http2-support) +- [PluralSight Course: Enhancing Application Communication with gRPC](https://app.pluralsight.com/library/courses/grpc-enhancing-application-communication/table-of-contents)