├── .github └── workflows │ └── run_test_case.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── drafs ├── examples └── route_guide │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── priv │ └── route_guide.proto │ ├── rebar.config │ └── src │ ├── route_guide.app.src │ ├── route_guide.erl │ └── route_guide_svr.erl ├── include └── grpc.hrl ├── priv ├── health.proto └── reflection.proto ├── rebar.config ├── src ├── client │ ├── grpc_client.erl │ └── grpc_client_sup.erl ├── grpc.app.src ├── grpc.appup.src ├── grpc.erl ├── grpc_app.erl ├── grpc_frame.erl ├── grpc_health_svr.erl ├── grpc_lib.erl ├── grpc_reflection_svr.erl ├── grpc_stream.erl ├── grpc_stream_h.erl ├── grpc_sup.erl └── grpc_utils.erl └── test ├── certs ├── ca.pem ├── cert.pem └── key.pem ├── greeter.proto ├── greeter_svr.erl ├── grpc_SUITE.erl ├── grpc_performance_SUITE.erl ├── grpc_test2_SUITE.erl ├── route_guide.proto ├── route_guide_svr.erl ├── test.proto └── test_svr.erl /.github/workflows/run_test_case.yaml: -------------------------------------------------------------------------------- 1 | name: Run test cases 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | windows: 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: erlef/setup-beam@v1 13 | with: 14 | otp-version: '27' 15 | rebar3-version: '3.23.0' 16 | - run: rebar3 xref 17 | - run: rebar3 dialyzer 18 | - run: rebar3 eunit 19 | - run: rebar3 ct 20 | 21 | linux: 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | builder: 27 | - ghcr.io/emqx/emqx-builder/5.4-3:1.17.3-27.2-1 28 | os: 29 | - ubuntu24.04 30 | - ubuntu22.04 31 | - ubuntu20.04 32 | - debian12 33 | - debian11 34 | - debian10 35 | - amzn2023 36 | - el9 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Code analyze 41 | run: | 42 | version=$(echo ${{ github.ref }} | sed -r "s .*/.*/(.*) \1 g") 43 | docker run --rm --privileged multiarch/qemu-user-static:register --reset 44 | docker run --rm -i --name ${{ matrix.os }} -v $(pwd):/repos ${{ matrix.builder }}-${{ matrix.os }} \ 45 | sh -c "cd /repos && make xref && make dialyzer" 46 | - name: Run tests 47 | run: | 48 | version=$(echo ${{ github.ref }} | sed -r "s .*/.*/(.*) \1 g") 49 | docker run --rm --privileged multiarch/qemu-user-static:register --reset 50 | docker run --rm -i --name ${{ matrix.os }} -v $(pwd):/repos ${{ matrix.builder }}-${{ matrix.os }} \ 51 | sh -c "cd /repos && make eunit && make ct" 52 | 53 | docker: 54 | runs-on: ubuntu-latest 55 | 56 | strategy: 57 | matrix: 58 | builder: 59 | - ghcr.io/emqx/emqx-builder/5.4-3:1.17.3-27.2-1 60 | 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Code analyze 64 | run: | 65 | docker run --rm -i --name alpine -v $(pwd):/repos ${{ matrix.builder }}-alpine3.15.1 \ 66 | sh -c "cd /repos && make xref && make dialyzer" 67 | - name: Run tests 68 | run: | 69 | docker run --rm -i --name alpine -v $(pwd):/repos ${{ matrix.builder }}-alpine3.15.1 \ 70 | sh -c "cd /repos && make eunit && make ct" 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3/ 2 | _build 3 | .erlang.mk 4 | grpc.d 5 | .eunit 6 | deps 7 | *.o 8 | *.beam 9 | *.plt 10 | erl_crash.dump 11 | ebin/*.beam 12 | rel/example_project 13 | .concrete/DEV_MODE 14 | .rebar 15 | .erlang.mk 16 | ebin 17 | logs 18 | grpc.d 19 | *.DS_Store 20 | *.swp 21 | rebar.lock 22 | rebar3.crashdump 23 | 24 | ## Automatically generated files 25 | src/grpc_health_pb.erl 26 | src/grpc_health_v_1_health_bhvr.erl 27 | src/grpc_health_v_1_health_client.erl 28 | src/grpc_reflection_pb.erl 29 | src/grpc_reflection_v_1alpha_server_reflection_bhvr.erl 30 | src/grpc_reflection_v_1alpha_server_reflection_client.erl 31 | test/grpc_greeter_pb.erl 32 | test/greeter_bhvr.erl 33 | test/greeter_client.erl 34 | test/grpc_route_guide_pb.erl 35 | test/routeguide_route_guide_bhvr.erl 36 | test/routeguide_route_guide_client.erl 37 | test/grpc_health_pb.erl 38 | test/grpc_health_v_1_health_bhvr.erl 39 | test/grpc_health_v_1_health_client.erl 40 | test/grpc_reflection_pb.erl 41 | test/grpc_reflection_v_1alpha_server_reflection_bhvr.erl 42 | test/grpc_reflection_v_1alpha_server_reflection_client.erl 43 | test/grpc_test_pb.erl 44 | test/test_bhvr.erl 45 | test/test_client.erl 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## shallow clone for speed 2 | 3 | REBAR_GIT_CLONE_OPTIONS += --depth 1 4 | export REBAR_GIT_CLONE_OPTIONS 5 | 6 | REBAR = rebar3 7 | all: compile 8 | 9 | compile: 10 | $(REBAR) compile 11 | 12 | escript: 13 | $(REBAR) as escript escriptize 14 | 15 | ct: 16 | $(REBAR) as test ct -v 17 | 18 | eunit: 19 | $(REBAR) as test eunit 20 | 21 | xref: 22 | $(REBAR) xref 23 | 24 | dialyzer: 25 | $(REBAR) dialyzer 26 | 27 | cover: 28 | $(REBAR) cover 29 | 30 | clean: 31 | $(REBAR) clean 32 | 33 | distclean: 34 | @rm -rf _build 35 | @rm -f data/app.*.config data/vm.*.args rebar.lock 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC 2 | 3 | An implementation of a gRPC server and client in Erlang. HTTP/2 based RPC. 4 | 5 | NOTE: The prototypes for this repository are from [Bluehouse-Technology/grpc](https://github.com/Bluehouse-Technology/grpc) 6 | and [tsloughter/grpcbox](https://github.com/tsloughter/grpcbox) 7 | Many thanks to these two repositories for helping me with the implementation :) 8 | 9 | ## TODOs 10 | 11 | - [x] Unary RPC 12 | - [x] Input streaming/ Output streaming/ Bidirectional streaming 13 | - [x] Custom metadata 14 | - [x] Https 15 | - [ ] Encoding: identity, gzip, deflate, snappy 16 | - [ ] Timeout, Error Handling 17 | - [ ] Benchmark 18 | 19 | 20 | ## Installation 21 | 22 | The application is built from rebar3, so you can introduce it in your 23 | rebar.config. 24 | 25 | In addition, you need to introduce `grpc_plugin` plugin to generate client-side 26 | and server-side code from the `.proto` file. 27 | 28 | So a regular rebar.config would look something like the following: 29 | 30 | ```erl 31 | {plugins, 32 | [{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.1"}}} 33 | ]}. 34 | 35 | {deps, 36 | [{grpc, {git, "https://github.com/emqx/grpc", {branch, "master"}}} 37 | ]}. 38 | 39 | %% Configurations for grpc_plugin 40 | {grpc, 41 | [ {type, all} 42 | , {protos, ["priv/"]} 43 | , {out_dir, "src/"} 44 | , {gpb_opts, []} %% The gpb compile options, see: https://github.com/tomas-abrahamsson/gpb/blob/master/src/gpb_compile.erl#L120 45 | ]}. 46 | 47 | %% Integrate grpc_plugin generation/clean into rebar3's complie/clean 48 | {provider_hooks, 49 | [{pre, [{compile, {grpc, gen}}, 50 | {clean, {grpc, clean}}]} 51 | ]}. 52 | ``` 53 | 54 | For more detailed examples, you can see the examples/route_guide project. 55 | 56 | ## Dependencies 57 | 58 | - [cowboy](https://github.com/emqx/cowboy) is used for the server. 59 | - [gun](https://github.com/emqx/gun) is used for the client. 60 | - [gpb](https://github.com/tomas-abrahamsson/gpb) is used to encode and 61 | decode the protobuf messages. This is a 'build' dependency: gpb is 62 | required to create some modules from the .proto files, but the modules 63 | are self contained and don't have a runtime depedency on gpb. 64 | 65 | ## License 66 | 67 | Apache 2.0 68 | 69 | -------------------------------------------------------------------------------- /drafs: -------------------------------------------------------------------------------- 1 | > This document logs some rough ideas for grpc 2 | 3 | - Support pass the server's env to grpc-server callback function? 4 | - How to get the server options/env in the callback function? 5 | -------------------------------------------------------------------------------- /examples/route_guide/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | *~ 20 | rebar.lock 21 | src/route_guide_pb.erl 22 | src/routeguide_route_guide_bhvr.erl 23 | src/routeguide_route_guide_client.erl 24 | -------------------------------------------------------------------------------- /examples/route_guide/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2021, JianBo He . 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /examples/route_guide/README.md: -------------------------------------------------------------------------------- 1 | # route_guide 2 | 3 | The example application for grpc 4 | 5 | ## Build 6 | 7 | ``` 8 | rebar3 compile 9 | ``` 10 | 11 | ## Usage 12 | 13 | 1. Start the rebar3 shell 14 | 15 | ``` 16 | rebar3 shell 17 | ``` 18 | 19 | 2. Start the `route_guide` services in the rebar3 shell 20 | 21 | ```erl 22 | 1> route_guide:start_services(). 23 | Start service route_guide on 10000 successfully! 24 | ``` 25 | 26 | 3. Start the client channel 27 | 28 | ```erl 29 | 2> route_guide:start_client_channel(). 30 | Start client channel channel1 for http://127.0.0.1:10000 successfully! 31 | ``` 32 | 33 | 4. Call the `get_feature` method of route_guide services 34 | 35 | ```erl 36 | 3> routeguide_route_guide_client:get_feature(#{latitude => 1, longitude => 1}, #{channel => channel1}). 37 | {ok,#{name => <<>>}, 38 | [{<<"grpc-message">>,<<>>},{<<"grpc-status">>,<<"0">>}]} 39 | ``` 40 | 41 | ## Project Structure 42 | 43 | ``` 44 | ├── priv 45 | │   └── route_guide.proto %% The grpc services defined file 46 | └── src 47 | ├── route_guide.app.src 48 | ├── route_guide.erl %% mainly example codes 49 | ├── route_guide_pb.erl %% generated by gpb 50 | ├── route_guide_svr.erl %% the route_guide service implementation 51 | ├── routeguide_route_guide_bhvr.erl %% generated by grpc-plugin 52 | └── routeguide_route_guide_client.erl %% generated by grpc-plugin 53 | ``` 54 | -------------------------------------------------------------------------------- /examples/route_guide/priv/route_guide.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015, Google Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | syntax = "proto3"; 31 | 32 | option java_multiple_files = true; 33 | option java_package = "io.grpc.examples.routeguide"; 34 | option java_outer_classname = "RouteGuideProto"; 35 | 36 | package routeguide; 37 | 38 | // Interface exported by the server. 39 | service RouteGuide { 40 | // A simple RPC. 41 | // 42 | // Obtains the feature at a given position. 43 | // 44 | // A feature with an empty name is returned if there's no feature at the given 45 | // position. 46 | rpc GetFeature(Point) returns (Feature) {} 47 | 48 | // A server-to-client streaming RPC. 49 | // 50 | // Obtains the Features available within the given Rectangle. Results are 51 | // streamed rather than returned at once (e.g. in a response message with a 52 | // repeated field), as the rectangle may cover a large area and contain a 53 | // huge number of features. 54 | rpc ListFeatures(Rectangle) returns (stream Feature) {} 55 | 56 | // A client-to-server streaming RPC. 57 | // 58 | // Accepts a stream of Points on a route being traversed, returning a 59 | // RouteSummary when traversal is completed. 60 | rpc RecordRoute(stream Point) returns (RouteSummary) {} 61 | 62 | // A Bidirectional streaming RPC. 63 | // 64 | // Accepts a stream of RouteNotes sent while a route is being traversed, 65 | // while receiving other RouteNotes (e.g. from other users). 66 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 67 | } 68 | 69 | // Points are represented as latitude-longitude pairs in the E7 representation 70 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 71 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 72 | // the range +/- 180 degrees (inclusive). 73 | message Point { 74 | int32 latitude = 1; 75 | int32 longitude = 2; 76 | } 77 | 78 | // A latitude-longitude rectangle, represented as two diagonally opposite 79 | // points "lo" and "hi". 80 | message Rectangle { 81 | // One corner of the rectangle. 82 | Point lo = 1; 83 | 84 | // The other corner of the rectangle. 85 | Point hi = 2; 86 | } 87 | 88 | // A feature names something at a given point. 89 | // 90 | // If a feature could not be named, the name is empty. 91 | message Feature { 92 | // The name of the feature. 93 | string name = 1; 94 | 95 | // The point where the feature is detected. 96 | Point location = 2; 97 | } 98 | 99 | // A RouteNote is a message sent while at a given point. 100 | message RouteNote { 101 | // The location from which the message is sent. 102 | Point location = 1; 103 | 104 | // The message to be sent. 105 | string message = 2; 106 | } 107 | 108 | // A RouteSummary is received in response to a RecordRoute rpc. 109 | // 110 | // It contains the number of individual points received, the number of 111 | // detected features, and the total distance covered as the cumulative sum of 112 | // the distance between each point. 113 | message RouteSummary { 114 | // The number of points received. 115 | int32 point_count = 1; 116 | 117 | // The number of known features passed while traversing the route. 118 | int32 feature_count = 2; 119 | 120 | // The distance covered in metres. 121 | int32 distance = 3; 122 | 123 | // The duration of the traversal in seconds. 124 | int32 elapsed_time = 4; 125 | } 126 | -------------------------------------------------------------------------------- /examples/route_guide/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | 3 | {plugins, 4 | [{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.1"}}} 5 | ]}. 6 | 7 | {deps, 8 | [{grpc, {git, "https://github.com/emqx/grpc", {branch, "master"}}} 9 | ]}. 10 | 11 | {grpc, 12 | [ {type, all} 13 | , {protos, ["priv/"]} 14 | , {out_dir, "src/"} 15 | , {gpb_opts, [{module_name_suffix, "_pb"}]} 16 | ]}. 17 | 18 | {provider_hooks, 19 | [{pre, [{compile, {grpc, gen}}, 20 | {clean, {grpc, clean}}]} 21 | ]}. 22 | -------------------------------------------------------------------------------- /examples/route_guide/src/route_guide.app.src: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {application, route_guide, 3 | [{description, "An OTP application"}, 4 | {vsn, "0.1.0"}, 5 | {registered, []}, 6 | {mod, {route_guide, []}}, 7 | {applications, [kernel,stdlib,grpc]}, 8 | {env,[]}, 9 | {modules, []}, 10 | {licenses, ["Apache 2.0"]}, 11 | {links, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /examples/route_guide/src/route_guide.erl: -------------------------------------------------------------------------------- 1 | -module(route_guide). 2 | 3 | -behaviour(supervisor). 4 | -behaviour(application). 5 | 6 | -export([start/2, stop/1]). 7 | -export([init/1]). 8 | 9 | -export([start_services/0, start_client_channel/0, 10 | stop_services/0, stop_client_channel/0]). 11 | 12 | %%-------------------------------------------------------------------- 13 | %% APIs 14 | 15 | -define(SERVER_NAME, route_guide). 16 | -define(CHANN_NAME, channel1). 17 | 18 | start_services() -> 19 | Services = #{protos => [route_guide_pb], 20 | services => #{'routeguide.RouteGuide' => route_guide_svr} 21 | }, 22 | Options = [], 23 | {ok, _} = grpc:start_server(?SERVER_NAME, 10000, Services, Options), 24 | io:format("Start service ~s on 10000 successfully!~n", [?SERVER_NAME]). 25 | 26 | start_client_channel() -> 27 | ClientOps = #{}, 28 | SvrAddr = "http://127.0.0.1:10000", 29 | {ok, _} = grpc_client_sup:create_channel_pool( 30 | ?CHANN_NAME, 31 | SvrAddr, 32 | ClientOps 33 | ), 34 | io:format("Start client channel ~s for ~s successfully!~n~n" 35 | "Call the 'routeguide_route_guide_client' module exported functions " 36 | "to use it. e.g:~n" 37 | " routeguide_route_guide_client:get_feature(#{latitude => 1" 38 | ", longitude => 1}, #{channel => channel1}).~n", 39 | [?CHANN_NAME, SvrAddr]). 40 | 41 | stop_services() -> 42 | grpc:stop_server(?SERVER_NAME). 43 | 44 | stop_client_channel() -> 45 | grpc_client_sup:stop_channel_pool(?CHANN_NAME). 46 | 47 | %%-------------------------------------------------------------------- 48 | %% APIs for application 49 | 50 | start(_StartType, _StartArgs) -> 51 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 52 | 53 | stop(_State) -> 54 | ok. 55 | 56 | %%-------------------------------------------------------------------- 57 | %% callbacks for supervisor 58 | 59 | init([]) -> 60 | SupFlags = #{strategy => one_for_all, 61 | intensity => 0, 62 | period => 1}, 63 | ChildSpecs = [], 64 | {ok, {SupFlags, ChildSpecs}}. 65 | -------------------------------------------------------------------------------- /examples/route_guide/src/route_guide_svr.erl: -------------------------------------------------------------------------------- 1 | -module(route_guide_svr). 2 | 3 | -behavior(routeguide_route_guide_bhvr). 4 | 5 | -compile(export_all). 6 | -compile(nowarn_export_all). 7 | 8 | -define(LOG(Fmt, Args), io:format(standard_error, "[Svr] " ++ Fmt, Args)). 9 | 10 | %%-------------------------------------------------------------------- 11 | %% Callbacks 12 | 13 | get_feature(Request, _Md) -> 14 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Request]), 15 | {ok, #{}, _Md}. 16 | 17 | list_features(Stream, _Md) -> 18 | {eos, [Request], NStream} = grpc_stream:recv(Stream), 19 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Request]), 20 | 21 | grpc_stream:reply(Stream, [#{name => "City1", location => #{latitude => 1, longitude => 1}}]), 22 | grpc_stream:reply(Stream, [#{name => "City2", location => #{latitude => 2, longitude => 2}}]), 23 | grpc_stream:reply(Stream, [#{name => "City3", location => #{latitude => 3, longitude => 3}}]), 24 | {ok, NStream}. 25 | 26 | record_route(Stream, _Md) -> 27 | LoopRecv = fun _Lp(St, Acc) -> 28 | case grpc_stream:recv(St) of 29 | {more, Reqs, NSt} -> 30 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 31 | 32 | _Lp(NSt, Acc ++ Reqs); 33 | {eos, Reqs, NSt} -> 34 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 35 | {NSt, Acc ++ Reqs} 36 | end 37 | end, 38 | {NStream, Points} = LoopRecv(Stream, []), 39 | grpc_stream:reply(NStream, #{point_count => length(Points)}), 40 | {ok, NStream}. 41 | 42 | route_chat(Stream, _Md) -> 43 | grpc_stream:reply(Stream, [#{name => "City1", location => #{latitude => 1, longitude => 1}}]), 44 | grpc_stream:reply(Stream, [#{name => "City2", location => #{latitude => 2, longitude => 2}}]), 45 | LoopRecv = fun _Lp(St) -> 46 | case grpc_stream:recv(St) of 47 | {more, Reqs, NSt} -> 48 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 49 | _Lp(NSt); 50 | {eos, Reqs, NSt} -> 51 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 52 | NSt 53 | end 54 | end, 55 | NStream = LoopRecv(Stream), 56 | grpc_stream:reply(NStream, [#{name => "City3", location => #{latitude => 3, longitude => 3}}]), 57 | {ok, NStream}. 58 | -------------------------------------------------------------------------------- /include/grpc.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -ifndef(GRPC_HRL). 18 | -define(GRPC_HRL, true). 19 | 20 | -define(GRPC_STATUS_OK, <<"0">>). 21 | -define(GRPC_STATUS_CANCELLED, <<"1">>). 22 | -define(GRPC_STATUS_UNKNOWN, <<"2">>). 23 | -define(GRPC_STATUS_INVALID_ARGUMENT, <<"3">>). 24 | -define(GRPC_STATUS_DEADLINE_EXCEEDED, <<"4">>). 25 | -define(GRPC_STATUS_NOT_FOUND, <<"5">>). 26 | -define(GRPC_STATUS_ALREADY_EXISTS , <<"6">>). 27 | -define(GRPC_STATUS_PERMISSION_DENIED, <<"7">>). 28 | -define(GRPC_STATUS_RESOURCE_EXHAUSTED, <<"8">>). 29 | -define(GRPC_STATUS_FAILED_PRECONDITION, <<"9">>). 30 | -define(GRPC_STATUS_ABORTED, <<"10">>). 31 | -define(GRPC_STATUS_OUT_OF_RANGE, <<"11">>). 32 | -define(GRPC_STATUS_UNIMPLEMENTED, <<"12">>). 33 | -define(GRPC_STATUS_INTERNAL, <<"13">>). 34 | -define(GRPC_STATUS_UNAVAILABLE, <<"14">>). 35 | -define(GRPC_STATUS_DATA_LOSS, <<"15">>). 36 | -define(GRPC_STATUS_UNAUTHENTICATED, <<"16">>). 37 | 38 | -type grpc_status() :: binary(). %% GRPC_STATUS_OK...GRPC_STATUS_UNAUTHENTICATED 39 | 40 | -type grpc_message() :: binary(). 41 | 42 | -type grpc_status_name() :: atom(). 43 | 44 | %% Logger 45 | 46 | -define(LOG(Level, Format), ?LOG(Level, Format, [])). 47 | 48 | -define(LOG(Level, Format, Args), 49 | begin 50 | (logger:log(Level,#{},#{report_cb => fun(_) -> {(Format), (Args)} end})) 51 | end). 52 | -endif. 53 | -------------------------------------------------------------------------------- /priv/health.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // The canonical version of this proto can be found at 16 | // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto 17 | 18 | syntax = "proto3"; 19 | 20 | package grpc.health.v1; 21 | 22 | option csharp_namespace = "Grpc.Health.V1"; 23 | option go_package = "google.golang.org/grpc/health/grpc_health_v1"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "HealthProto"; 26 | option java_package = "io.grpc.health.v1"; 27 | 28 | message HealthCheckRequest { 29 | string service = 1; 30 | } 31 | 32 | message HealthCheckResponse { 33 | enum ServingStatus { 34 | UNKNOWN = 0; 35 | SERVING = 1; 36 | NOT_SERVING = 2; 37 | SERVICE_UNKNOWN = 3; // Used only by the Watch method. 38 | } 39 | ServingStatus status = 1; 40 | } 41 | 42 | service Health { 43 | // If the requested service is unknown, the call will fail with status 44 | // NOT_FOUND. 45 | rpc Check(HealthCheckRequest) returns (HealthCheckResponse); 46 | 47 | // Performs a watch for the serving status of the requested service. 48 | // The server will immediately send back a message indicating the current 49 | // serving status. It will then subsequently send a new message whenever 50 | // the service's serving status changes. 51 | // 52 | // If the requested service is unknown when the call is received, the 53 | // server will send a message setting the serving status to 54 | // SERVICE_UNKNOWN but will *not* terminate the call. If at some 55 | // future point, the serving status of the service becomes known, the 56 | // server will send a new message with the service's serving status. 57 | // 58 | // If the call terminates with status UNIMPLEMENTED, then clients 59 | // should assume this method is not supported and should not retry the 60 | // call. If the call terminates with any other status (including OK), 61 | // clients should retry the call with appropriate exponential backoff. 62 | rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); 63 | } 64 | -------------------------------------------------------------------------------- /priv/reflection.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // Service exported by server reflection 15 | 16 | 17 | // Warning: this entire file is deprecated. Use this instead: 18 | // https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto 19 | 20 | syntax = "proto3"; 21 | 22 | package grpc.reflection.v1alpha; 23 | 24 | //grpc.reflection.v1alphaoption deprecated = true; 25 | option java_multiple_files = true; 26 | option java_package = "io.grpc.reflection.v1alpha"; 27 | option java_outer_classname = "ServerReflectionProto"; 28 | 29 | service ServerReflection { 30 | // The reflection service is structured as a bidirectional stream, ensuring 31 | // all related requests go to a single server. 32 | rpc ServerReflectionInfo(stream ServerReflectionRequest) 33 | returns (stream ServerReflectionResponse); 34 | } 35 | 36 | // The message sent by the client when calling ServerReflectionInfo method. 37 | message ServerReflectionRequest { 38 | string host = 1; 39 | // To use reflection service, the client should set one of the following 40 | // fields in message_request. The server distinguishes requests by their 41 | // defined field and then handles them using corresponding methods. 42 | oneof message_request { 43 | // Find a proto file by the file name. 44 | string file_by_filename = 3; 45 | 46 | // Find the proto file that declares the given fully-qualified symbol name. 47 | // This field should be a fully-qualified symbol name 48 | // (e.g. .[.] or .). 49 | string file_containing_symbol = 4; 50 | 51 | // Find the proto file which defines an extension extending the given 52 | // message type with the given field number. 53 | ExtensionRequest file_containing_extension = 5; 54 | 55 | // Finds the tag numbers used by all known extensions of extendee_type, and 56 | // appends them to ExtensionNumberResponse in an undefined order. 57 | // Its corresponding method is best-effort: it's not guaranteed that the 58 | // reflection service will implement this method, and it's not guaranteed 59 | // that this method will provide all extensions. Returns 60 | // StatusCode::UNIMPLEMENTED if it's not implemented. 61 | // This field should be a fully-qualified type name. The format is 62 | // . 63 | string all_extension_numbers_of_type = 6; 64 | 65 | // List the full names of registered services. The content will not be 66 | // checked. 67 | string list_services = 7; 68 | } 69 | } 70 | 71 | // The type name and extension number sent by the client when requesting 72 | // file_containing_extension. 73 | message ExtensionRequest { 74 | // Fully-qualified type name. The format should be . 75 | string containing_type = 1; 76 | int32 extension_number = 2; 77 | } 78 | 79 | // The message sent by the server to answer ServerReflectionInfo method. 80 | message ServerReflectionResponse { 81 | string valid_host = 1; 82 | ServerReflectionRequest original_request = 2; 83 | // The server set one of the following fields accroding to the message_request 84 | // in the request. 85 | oneof message_response { 86 | // This message is used to answer file_by_filename, file_containing_symbol, 87 | // file_containing_extension requests with transitive dependencies. As 88 | // the repeated label is not allowed in oneof fields, we use a 89 | // FileDescriptorResponse message to encapsulate the repeated fields. 90 | // The reflection service is allowed to avoid sending FileDescriptorProtos 91 | // that were previously sent in response to earlier requests in the stream. 92 | FileDescriptorResponse file_descriptor_response = 4; 93 | 94 | // This message is used to answer all_extension_numbers_of_type requst. 95 | ExtensionNumberResponse all_extension_numbers_response = 5; 96 | 97 | // This message is used to answer list_services request. 98 | ListServiceResponse list_services_response = 6; 99 | 100 | // This message is used when an error occurs. 101 | ErrorResponse error_response = 7; 102 | } 103 | } 104 | 105 | // Serialized FileDescriptorProto messages sent by the server answering 106 | // a file_by_filename, file_containing_symbol, or file_containing_extension 107 | // request. 108 | message FileDescriptorResponse { 109 | // Serialized FileDescriptorProto messages. We avoid taking a dependency on 110 | // descriptor.proto, which uses proto2 only features, by making them opaque 111 | // bytes instead. 112 | repeated bytes file_descriptor_proto = 1; 113 | } 114 | 115 | // A list of extension numbers sent by the server answering 116 | // all_extension_numbers_of_type request. 117 | message ExtensionNumberResponse { 118 | // Full name of the base type, including the package name. The format 119 | // is . 120 | string base_type_name = 1; 121 | repeated int32 extension_number = 2; 122 | } 123 | 124 | // A list of ServiceResponse sent by the server answering list_services request. 125 | message ListServiceResponse { 126 | // The information of each service may be expanded in the future, so we use 127 | // ServiceResponse message to encapsulate it. 128 | repeated ServiceResponse service = 1; 129 | } 130 | 131 | // The information of a single service used by ListServiceResponse to answer 132 | // list_services request. 133 | message ServiceResponse { 134 | // Full name of a registered service, including its package name. The format 135 | // is . 136 | string name = 1; 137 | } 138 | 139 | // The error code and error message sent by the server when an error occurs. 140 | message ErrorResponse { 141 | // This field uses the error codes defined in grpc::StatusCode. 142 | int32 error_code = 1; 143 | string error_message = 2; 144 | } 145 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | 3 | {minimum_otp_vsn, "21.0"}. 4 | 5 | {erl_opts, [debug_info]}. 6 | 7 | {plugins, 8 | [ {grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.3"}}} 9 | ]}. 10 | 11 | {grpc, 12 | [ {type, all} 13 | , {protos, ["priv/"]} 14 | , {out_dir, "src/"} 15 | , {gpb_opts, [{module_name_prefix, "grpc_"}, 16 | {module_name_suffix, "_pb"}]} 17 | ]}. 18 | 19 | {provider_hooks, 20 | [{pre, [{compile, {grpc, gen}}, 21 | {clean, {grpc, clean}}]} 22 | ]}. 23 | 24 | {deps, 25 | [ {gpb, "~> 4.21"} 26 | , {gproc, "1.0.0"} 27 | , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} 28 | , {gun, "2.1.0"} 29 | ]}. 30 | 31 | {xref_checks, 32 | [undefined_function_calls,undefined_functions,locals_not_used, 33 | deprecated_function_calls, deprecated_functions 34 | ]}. 35 | 36 | {xref_ignores, [grpc_health_pb,grpc_reflection_pb, 37 | grpc_greeter_pb,grpc_route_guide_pb]}. 38 | 39 | {dialyzer, [{plt_apps, all_deps}]}. 40 | 41 | {profiles, 42 | [{test, 43 | [{cover_enabled, true}, 44 | {cover_opts, [verbose]}, 45 | {cover_excl_mods, [grpc_health_pb,grpc_health_v_1_health_client, 46 | grpc_health_v_1_health_bhvr, 47 | grpc_reflection_pb, 48 | grpc_reflection_v_1alpha_server_reflection_client, 49 | grpc_reflection_v_1alpha_server_reflection_bhvr, 50 | grpc_route_guide_pb,routeguide_route_guide_bhvr, 51 | routeguide_route_guide_client, 52 | ct_greeter_pb,greeter_bhvr,greeter_client]}, 53 | {grpc, 54 | [{type, all}, 55 | {protos, ["priv/", "test/"]}, 56 | {out_dir, "test/"}, 57 | {gpb_opts, [{module_name_prefix, "grpc_"}, 58 | {module_name_suffix, "_pb"}]} 59 | ]} 60 | ]}]}. 61 | -------------------------------------------------------------------------------- /src/client/grpc_client.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_client). 18 | 19 | -behaviour(gen_server). 20 | 21 | -include("grpc.hrl"). 22 | 23 | %% APIs 24 | -export([ unary/4 25 | , health_check/2]). 26 | 27 | -export([ open/3 28 | , send/2 29 | , send/3 30 | , recv/1 31 | , recv/2 32 | ]). 33 | 34 | -export([start_link/4]). 35 | 36 | %% gen_server callbacks 37 | -export([ init/1 38 | , handle_call/3 39 | , handle_cast/2 40 | , handle_info/2 41 | , handle_continue/2 42 | , terminate/2 43 | , code_change/3 44 | ]). 45 | 46 | -export_type([ client_options/0 47 | , options/0 48 | , grpcstream/0]). 49 | 50 | -record(state, { 51 | %% Pool name 52 | pool, 53 | %% The worker id in the pool 54 | id, 55 | %% Server address 56 | server :: server(), 57 | %% Gun connection pid 58 | gun_pid :: undefined | pid(), 59 | %% The Monitor referebce for gun connection pid 60 | mref :: undefined | reference(), 61 | %% Clean timer reference 62 | tref :: undefined | reference(), 63 | %% Encoding for gRPC packets 64 | %% XXX: Bad impl. 65 | encoding :: grpc_frame:encoding(), 66 | %% Streams 67 | streams :: #{gun:stream_ref() => stream()}, 68 | %% Client options 69 | client_opts :: client_options(), 70 | %% Flush timer reference 71 | flush_timer_ref :: undefined | reference(), 72 | %% Gun connection state 73 | gun_state :: up | down 74 | }). 75 | 76 | -type request() :: map(). 77 | 78 | -type response() :: map(). 79 | 80 | -type eos_msg() :: {eos, list()}. 81 | 82 | -type options() :: 83 | #{ channel => term() 84 | %% The grpc-encoding method 85 | %% Default is identity 86 | , encoding => grpc_frame:encoding() 87 | %% The timeout to receive the response of request. The clock starts 88 | %% ticking when the request is sent 89 | %% 90 | %% Time is in milliseconds 91 | %% 92 | %% Default is infinity 93 | , timeout => non_neg_integer() 94 | %% Connection time-out time, used during the initial request, 95 | %% when the client is connecting to the server 96 | %% 97 | %% Time is in milliseconds 98 | %% 99 | %% Default equal to timeout option 100 | , connect_timeout => non_neg_integer() 101 | %% Whether to exit the client process 102 | %% when the connection process exits 103 | %% Default is true, as in gen.erl and gen_server.erl 104 | , exit_on_connection_crash => boolean() 105 | }. 106 | 107 | -type def() :: 108 | #{ path := binary() 109 | , service := atom() 110 | , message_type := binary() 111 | , marshal := function() 112 | , unmarshal := function() 113 | }. 114 | 115 | -type server() :: {http | https, string(), inet:port_number()}. 116 | 117 | -type client_options() :: 118 | #{ encoding => grpc_frame:encoding() 119 | , gun_opts => gun:opts() 120 | , stream_batch_size => non_neg_integer() 121 | , stream_batch_delay_ms => non_neg_integer() 122 | }. 123 | 124 | -define(DEFAULT_GUN_OPTS, 125 | #{protocols => [http2], 126 | connect_timeout => 5000, 127 | http2_opts => #{keepalive => 60000}, 128 | tcp_opts => [{nodelay, true}] 129 | }). 130 | 131 | -define(STREAM_RESERVED_TIMEOUT, 15000). 132 | 133 | -define(DEFAULT_STREAMING_DELAY, 20). 134 | -define(DEFAULT_STREAMING_BATCH_SIZE, 16384). 135 | 136 | -type stream_state() :: idle | open | closed. 137 | 138 | -type stream() :: #{ st := {LocalState :: stream_state(), 139 | RemoteState :: stream_state()} 140 | , mqueue := list() 141 | , hangs := list() 142 | , recvbuff := binary() 143 | , sendbuff := iolist() 144 | , sendbuff_size := non_neg_integer() 145 | , sendbuff_last_flush_ts := non_neg_integer() 146 | , encoding := grpc_frame:encoding() 147 | }. 148 | 149 | -type client_pid() :: pid(). 150 | 151 | -type grpcstream() :: #{ client_pid := client_pid() 152 | , stream_ref := reference() 153 | , def := def() 154 | }. 155 | 156 | -dialyzer({nowarn_function, [have_buffered_bytes/1]}). 157 | 158 | %%-------------------------------------------------------------------- 159 | %% APIs 160 | %%-------------------------------------------------------------------- 161 | 162 | -spec start_link(term(), pos_integer(), server(), client_options()) 163 | -> {ok, pid()} | ignore | {error, term()}. 164 | start_link(Pool, Id, Server, Opts) when is_map(Opts) -> 165 | gen_server:start_link(?MODULE, [Pool, Id, Server, Opts], []). 166 | 167 | %%-------------------------------------------------------------------- 168 | %% gRPC APIs 169 | 170 | -spec unary(def(), request(), grpc:metadata(), options()) 171 | -> {ok, response(), grpc:metadata()} 172 | | {error, {grpc_status_name(), grpc_message()}} 173 | | {error, term()}. 174 | %% @doc Unary function call 175 | unary(Def, Req, Metadata, Options) -> 176 | case open(Def, Metadata, Options) of 177 | {ok, GStream} -> 178 | _ = send(GStream, Req, fin, Options), 179 | case recv(GStream, Options) of 180 | {ok, [Resp]} when is_map(Resp) -> 181 | {ok, [{eos, Trailers}]} = recv(GStream, Options), 182 | {ok, Resp, Trailers}; 183 | {ok, [{eos, Trailers}]} -> 184 | %% Not data responed, only error trailers 185 | {error, trailers_to_error(Trailers)}; 186 | {ok, [Resp, Trailers]} -> 187 | {ok, Resp, Trailers}; 188 | {error, _} = E -> E 189 | end; 190 | E -> E 191 | end. 192 | 193 | -spec open(def(), grpc:metadata(), options()) 194 | -> {ok, grpcstream()} 195 | | {error, term()}. 196 | 197 | open(Def, Metadata, Options) -> 198 | ClientPid = pick( 199 | maps:get(channel, Options, undefined), 200 | maps:get(key_dispatch, Options, self()) 201 | ), 202 | ConnectTimeout = connect_timeout(Options), 203 | case call(ClientPid, {open, Def, Metadata, Options}, Options#{timeout => ConnectTimeout}) of 204 | {ok, StreamRef} -> 205 | {ok, #{stream_ref => StreamRef, client_pid => ClientPid, def => Def}}; 206 | {error, _} = Error -> Error 207 | end. 208 | 209 | connect_timeout(Options) -> 210 | maps:get( 211 | connect_timeout, 212 | Options, 213 | timeout(Options) 214 | ). 215 | 216 | timeout(Options) -> 217 | maps:get(timeout, Options, infinity). 218 | 219 | exit_on_connection_crash(Options) -> 220 | maps:get(exit_on_connection_crash, Options, true). 221 | 222 | -spec send(grpcstream(), request()) -> ok. 223 | send(GStream, Req) -> 224 | send(GStream, Req, nofin). 225 | 226 | -spec send(grpcstream(), request(), fin | nofin) -> ok | no_return(). 227 | send(GStream, Req, IsFin) -> 228 | send(GStream, Req, IsFin, #{}). 229 | 230 | -spec send(grpcstream(), request(), fin | nofin, options()) -> ok. 231 | send(_GStream = #{ 232 | def := Def, 233 | client_pid := ClientPid, 234 | stream_ref := StreamRef 235 | }, Req, IsFin, Options) -> 236 | #{marshal := Marshal} = Def, 237 | Bytes = Marshal(Req), 238 | case call(ClientPid, {send, StreamRef, Bytes, IsFin}, Options) of 239 | ok -> ok; 240 | {error, R} -> error(R) 241 | end. 242 | 243 | -spec recv(grpcstream()) 244 | -> {ok, [response() | eos_msg()]} 245 | | {error, term()}. 246 | recv(GStream) -> 247 | recv(GStream, #{}). 248 | 249 | -spec recv(grpcstream(), timeout() | options()) 250 | -> {ok, [response() | eos_msg()]} 251 | | {error, term()} | no_return(). 252 | recv(GStream, Timeout) when is_integer(Timeout) -> 253 | recv(GStream, #{timeout => Timeout}); 254 | recv(#{def := Def, 255 | client_pid := ClientPid, 256 | stream_ref := StreamRef}, Options) -> 257 | Timeout = timeout(Options), 258 | Unmarshal = maps:get(unmarshal, Def), 259 | Endts = case Timeout of 260 | infinity -> infinity; 261 | _ -> erlang:system_time(millisecond) + Timeout 262 | end, 263 | case call(ClientPid, {read, StreamRef, Endts}, Options) of 264 | {error, _} = E -> E; 265 | {IsMore, Frames} -> 266 | Msgs = lists:map(fun({eos, Trailers}) -> {eos, Trailers}; 267 | (Bin) -> Unmarshal(Bin) 268 | end, Frames), 269 | {IsMore, Msgs} 270 | end. 271 | 272 | -spec health_check(pid(), options()) -> ok | {error, term()}. 273 | health_check(Worker, Options) -> 274 | ConnectTimeout = connect_timeout(Options), 275 | try 276 | call(Worker, health_check, Options#{timeout => ConnectTimeout}) 277 | catch 278 | exit:{timeout, _Details} -> 279 | {error, timeout}; 280 | exit:Reason when 281 | Reason =:= normal; 282 | Reason =:= {shutdown, normal} 283 | -> 284 | %% Race condition: gun went down while checking health? 285 | %% Try again. 286 | health_check(Worker, Options); 287 | exit:Reason -> 288 | {error, {grpc_client_down, Reason}} 289 | end. 290 | 291 | %%-------------------------------------------------------------------- 292 | %% gen_server callbacks 293 | %%-------------------------------------------------------------------- 294 | 295 | init([Pool, Id, Server = {_, _, _}, ClientOpts0]) -> 296 | Encoding = maps:get(encoding, ClientOpts0, identity), 297 | GunOpts = maps:get(gun_opts, ClientOpts0, #{}), 298 | Opts = ClientOpts0#{gun_opts => maps:merge(?DEFAULT_GUN_OPTS, GunOpts)}, 299 | true = gproc_pool:connect_worker(Pool, {Pool, Id}), 300 | {ok, ensure_clean_timer( 301 | #state{ 302 | pool = Pool, 303 | id = Id, 304 | gun_pid = undefined, 305 | server = Server, 306 | encoding = Encoding, 307 | streams = #{}, 308 | client_opts = Opts, 309 | gun_state = down}), {continue, connect}}. 310 | 311 | handle_continue(connect, State) -> 312 | case do_connect(State) of 313 | {error, Reason} -> 314 | logger:error("[grpc_client] connect to ~p failed: ~p", [State#state.server, Reason]), 315 | {noreply, State}; 316 | NState -> 317 | {noreply, NState} 318 | end. 319 | 320 | handle_call(Req, From, State = #state{gun_pid = undefined}) -> 321 | case do_connect(State) of 322 | {error, Reason} -> 323 | {reply, {error, Reason}, State#state{gun_state = down}}; 324 | NState -> 325 | handle_call(Req, From, NState) 326 | end; 327 | 328 | handle_call(health_check, _From, State = #state{gun_state = up}) -> 329 | {reply, ok, State}; 330 | 331 | handle_call(health_check, _From, State = #state{gun_state = down}) -> 332 | case do_connect(State) of 333 | {error, Reason} -> 334 | {reply, {error, Reason}, State#state{gun_state = down}}; 335 | NState -> 336 | {reply, ok, NState} 337 | end; 338 | 339 | handle_call({open, #{path := Path, 340 | message_type := MessageType 341 | }, Metadata, Options}, 342 | _From, 343 | State = #state{gun_pid = GunPid, streams = Streams, encoding = Encoding}) -> 344 | Timeout = maps:get(timeout, Options, infinity), 345 | Headers = assemble_grpc_headers(atom_to_binary(Encoding, utf8), 346 | MessageType, 347 | Timeout, 348 | Metadata 349 | ), 350 | StreamRef = gun:post(GunPid, Path, Headers), 351 | Stream = #{st => {open, idle}, 352 | mqueue => [], 353 | hangs => [], 354 | recvbuff => <<>>, 355 | sendbuff => [], 356 | sendbuff_size => 0, 357 | sendbuff_last_flush_ts => 0, 358 | encoding => Encoding 359 | }, 360 | NState = State#state{streams = Streams#{StreamRef => Stream}}, 361 | {reply, {ok, StreamRef}, NState}; 362 | 363 | handle_call(_Req = {send, StreamRef, Bytes, IsFin}, 364 | _From, 365 | State = #state{gun_pid = GunPid, streams = Streams, encoding = Encoding, 366 | client_opts = ClientOpts}) -> 367 | case maps:get(StreamRef, Streams, undefined) of 368 | Stream = #{st := {open, _RS}} -> 369 | NBytes = grpc_frame:encode(Encoding, Bytes), 370 | BatchSize = maps:get( 371 | stream_batch_size, 372 | ClientOpts, 373 | ?DEFAULT_STREAMING_BATCH_SIZE 374 | ), 375 | NStream = maybe_send_data(NBytes, IsFin, StreamRef, Stream, GunPid, BatchSize), 376 | NStreams = Streams#{StreamRef => NStream}, 377 | {reply, ok, ensure_flush_timer(State#state{streams = NStreams})}; 378 | #{st := {closed, _RS}} -> 379 | {reply, {error, closed}, State#state{gun_state = down}}; 380 | undefined -> 381 | {reply, {error, not_found}, State}; 382 | _S -> 383 | {reply, {error, bad_stream}, State} 384 | end; 385 | 386 | handle_call(_Req = {read, StreamRef, Endts}, 387 | From, 388 | State = #state{streams = Streams}) -> 389 | case maps:get(StreamRef, Streams, undefined) of 390 | undefined -> 391 | {reply, {error, not_found}, State}; 392 | Stream -> 393 | handle_stream_handle_result( 394 | stream_handle({read, From, StreamRef, Endts}, Stream), 395 | StreamRef, 396 | Streams, 397 | State) 398 | end; 399 | 400 | handle_call(_Request, _From, State) -> 401 | {reply, {error, unkown_request}, State}. 402 | 403 | handle_cast(_Msg, State) -> 404 | {noreply, State}. 405 | 406 | handle_info({timeout, TRef, clean_stopped_stream}, 407 | State = #state{tref = TRef, streams = Streams}) -> 408 | Nowts = erlang:system_time(millisecond), 409 | NStreams = maps:filter( 410 | fun(_, #{stopped := Stoppedts}) -> 411 | Nowts < Stoppedts + ?STREAM_RESERVED_TIMEOUT; 412 | (_, _) -> true 413 | end, Streams), 414 | {noreply, ensure_clean_timer(State#state{streams = NStreams, tref = undefined})}; 415 | 416 | handle_info({timeout, TRef, flush_streams_sendbuff}, 417 | State0 = #state{flush_timer_ref = TRef}) -> 418 | State = State0#state{flush_timer_ref = undefined}, 419 | Nowts = erlang:system_time(millisecond), 420 | {noreply, ensure_flush_timer(flush_streams(Nowts, State))}; 421 | 422 | handle_info({gun_up, GunPid, http2}, State = #state{gun_pid = GunPid}) -> 423 | {noreply, State#state{gun_state = up}}; 424 | 425 | handle_info({gun_down, GunPid, http2, Reason, KilledStreamRefs}, 426 | State = #state{gun_pid = GunPid, streams = Streams}) -> 427 | Nowts = erlang:system_time(millisecond), 428 | %% Reply killed streams error 429 | _ = maps:fold(fun(_, #{hangs := Hangs}, _Acc) -> 430 | lists:foreach(fun({From, Endts}) -> 431 | Endts > Nowts andalso 432 | gen_server:reply(From, {error, {connection_down, Reason}}) 433 | end, Hangs) 434 | end, [], maps:with(KilledStreamRefs, Streams)), 435 | {noreply, State#state{streams = maps:without(KilledStreamRefs, Streams), 436 | gun_state = down}}; 437 | 438 | handle_info({'DOWN', MRef, process, GunPid, Reason}, 439 | State = #state{mref = MRef, gun_pid = GunPid, streams = Streams}) -> 440 | Nowts = erlang:system_time(millisecond), 441 | _ = maps:fold(fun(_, #{hangs := Hangs}, _Acc) -> 442 | lists:foreach(fun({From, Endts}) -> 443 | Endts > Nowts andalso 444 | gen_server:reply(From, {error, {connection_down, Reason}}) 445 | end, Hangs) 446 | end, [], Streams), 447 | {noreply, State#state{gun_pid = undefined, 448 | streams = #{}, 449 | gun_state = down}}; 450 | 451 | handle_info(Info, State = #state{streams = Streams}) when is_tuple(Info) -> 452 | Ls = [gun_response, gun_trailers, gun_data, gun_error], 453 | case lists:member(element(1, Info), Ls) of 454 | true -> 455 | StreamRef = element(3, Info), 456 | case StreamRef of 457 | {stop, {goaway, StreamID, ErrCode, _AddData}, Reason} -> 458 | %% Ignore the goaway message; the state of the stream 459 | %% will be cleared in the gun_down event 460 | ?LOG(debug, "[gRPC Client] Stream ~w goaway, " 461 | "error_code: ~0p, details: ~0p", 462 | [StreamID, ErrCode, Reason]), 463 | {noreply, State}; 464 | _ -> 465 | case maps:get(StreamRef, Streams, undefined) of 466 | undefined -> 467 | ?LOG(warning, "[gRPC Client] Unknown stream ref: ~0p, " 468 | "event: ~0p", [StreamRef, Info]), 469 | {noreply, State}; 470 | Stream -> 471 | handle_stream_handle_result( 472 | stream_handle(Info, Stream), 473 | StreamRef, 474 | Streams, 475 | State) 476 | end 477 | end; 478 | _ -> 479 | ?LOG(warning, "[gRPC Client] Unexpected info: ~p~n", [Info]), 480 | {noreply, State} 481 | end. 482 | 483 | terminate(_Reason, #state{pool = Pool, id = Id}) -> 484 | gproc_pool:disconnect_worker(Pool, {Pool, Id}). 485 | 486 | %% downgrade to Vsn 487 | code_change({down, _Vsn}, 488 | State = #state{ 489 | client_opts = ClientOpts, 490 | flush_timer_ref = TRef 491 | }, [Vsn]) -> 492 | NState = 493 | case re:run(Vsn, "0\\.6\\.[0-6]$", [{capture, none}]) of 494 | match -> 495 | GunOpts = maps:get(gun_opts, ClientOpts, ?DEFAULT_GUN_OPTS), 496 | _ = is_reference(TRef) andalso erlang:cancel_timer(TRef), 497 | %% flush all streams to avoid buffered data lost 498 | State1 = flush_streams(infinity, State), 499 | list_to_tuple( 500 | lists:droplast(lists:droplast(tuple_to_list(State1))) 501 | ++ [GunOpts] 502 | ); 503 | _ -> State 504 | end, 505 | {ok, NState}; 506 | %% upgrade from Vsn 507 | code_change(_Vsn, 508 | {state, 509 | Pool, Id, Server, GunPid, MRef, 510 | TRef, Encoding, Streams, GunOpts}, [Vsn]) -> 511 | NState = 512 | case re:run(Vsn, "0\\.6\\.[0-6]$", [{capture, none}]) of 513 | match -> 514 | ClientOpts = #{encoding => Encoding, 515 | gun_opts => GunOpts}, 516 | NStreams = maps:map( 517 | fun(_, Stream) -> 518 | Stream#{ 519 | sendbuff => [], 520 | sendbuff_size => 0, 521 | sendbuff_last_flush_ts => 0 522 | } 523 | end, Streams), 524 | {state, Pool, Id, Server, GunPid, MRef, 525 | TRef, Encoding, NStreams, ClientOpts, undefined}; 526 | _ -> error({bad_vsn_in_code_change, Vsn}) 527 | end, 528 | {ok, NState}. 529 | 530 | %%-------------------------------------------------------------------- 531 | %% Handle stream handle 532 | 533 | handle_stream_handle_result(ok, _StreamRef, Streams, State) -> 534 | {noreply, State#state{streams = Streams}}; 535 | handle_stream_handle_result({ok, Stream}, StreamRef, Streams, State) -> 536 | {noreply, State#state{streams = Streams#{StreamRef => Stream}}}; 537 | handle_stream_handle_result({ok, Events, Stream}, StreamRef, Streams, State) -> 538 | _ = run_events(Events), 539 | {noreply, State#state{streams = Streams#{StreamRef => Stream}}}; 540 | % shutdown on gun error 541 | handle_stream_handle_result({shutdown, Reason, Stream}, StreamRef, Streams, State) 542 | when Reason =:= normal orelse Reason =:= {stream_error,no_error,'Stream reset by server.'} -> 543 | ?LOG(debug, "[gRPC Client] Stream shutdown reason: ~p, stream: ~s", [Reason, format_stream(Stream)]), 544 | {noreply, State#state{streams = maps:remove(StreamRef, Streams)}}; 545 | handle_stream_handle_result({shutdown, Reason, Stream}, StreamRef, Streams, State) -> 546 | ?LOG(error, "[gRPC Client] Stream shutdown reason: ~p, stream: ~s", [Reason, format_stream(Stream)]), 547 | {noreply, State#state{streams = maps:remove(StreamRef, Streams)}}; 548 | % self-induced shutdown 549 | handle_stream_handle_result({shutdown, _Reason, Events, _Stream}, StreamRef, Streams, State) -> 550 | _Reason /= normal andalso 551 | ?LOG(error, "[gRPC Client] Stream shutdown reason: ~p, stream: ~s", 552 | [_Reason, format_stream(_Stream)]), 553 | _ = run_events(Events), 554 | {noreply, State#state{streams = maps:remove(StreamRef, Streams)}}. 555 | 556 | run_events([]) -> 557 | ok; 558 | run_events([{reply, From, Msg}|Es]) -> 559 | gen_server:reply(From, Msg), 560 | run_events(Es). 561 | 562 | %%-------------------------------------------------------------------- 563 | %% Streams handle 564 | %%-------------------------------------------------------------------- 565 | 566 | %% api calls 567 | 568 | stream_handle({read, From, _StreamRef, EndTs}, 569 | Stream = #{mqueue := [], hangs := Hangs}) -> 570 | {ok, Stream#{hangs => [{From, EndTs}|Hangs]}}; 571 | 572 | stream_handle({read, From, _StreamRef, _EndTs}, 573 | Stream = #{st := {_LS, open}, mqueue := MQueue}) when MQueue /= [] -> 574 | {ok, [{reply, From, {ok, MQueue}}], Stream#{mqueue => []}}; 575 | 576 | stream_handle({read, From, _StreamRef, _EndTs}, 577 | Stream = #{st := {_LS, closed}, mqueue := MQueue}) when MQueue /= [] -> 578 | {shutdown, normal, [{reply, From, {ok, MQueue}}], Stream#{mqueue => []}}; 579 | 580 | stream_handle({read, From, _StreamRef, _EndTs}, 581 | Stream = #{st := {closed, closed}, mqueue := MQueue}) -> 582 | {shutdown, normal, [{reply, From, {ok, MQueue}}], Stream#{mqueue => []}}; 583 | 584 | %% gun msgs 585 | 586 | stream_handle({gun_response, _GunPid, _StreamRef, IsFin, _Status, Headers}, 587 | Stream = #{st := {_LS, idle}}) -> 588 | case IsFin of 589 | nofin -> 590 | {ok, Stream#{st => {_LS, open}}}; 591 | fin -> 592 | handle_remote_closed(Headers, Stream) 593 | end; 594 | 595 | stream_handle({gun_trailers, _GunPid, _StreamRef, Trailers}, 596 | Stream = #{st := {_LS, open}}) -> 597 | handle_remote_closed(Trailers, Stream); 598 | 599 | stream_handle({gun_data, _GunPid, _StreamRef, nofin, Data}, 600 | Stream = #{st := {_LS, open}, 601 | recvbuff := Acc, 602 | encoding := Encoding}) -> 603 | NData = <>, 604 | case grpc_frame:split(NData, Encoding) of 605 | {Rest, []} -> 606 | {ok, Stream#{recvbuff => Rest}}; 607 | {Rest, Frames} -> 608 | case clean_hangs(Stream#{recvbuff => Rest}) of 609 | NStream = #{hangs := [], mqueue := MQueue} -> 610 | {ok, NStream#{mqueue => MQueue ++ Frames}}; 611 | NStream = #{hangs := [{From, _}|NHangs], mqueue := MQueue} -> 612 | {ok, [{reply, From, {ok, MQueue ++ Frames}}], NStream#{hangs => NHangs}} 613 | end 614 | end; 615 | 616 | stream_handle({gun_data, _GunPid, _StreamRef, fin, Data}, 617 | Stream = #{st := {_LS, open}, 618 | recvbuff := Acc, 619 | encoding := Encoding}) -> 620 | NData = <>, 621 | case grpc_frame:split(NData, Encoding) of 622 | {<<>>, []} -> 623 | handle_remote_closed([], Stream); 624 | {<<>>, Frames} -> 625 | MQueue = maps:get(mqueue, Stream), 626 | handle_remote_closed([], Stream#{recvbuff => <<>>, mqueue => MQueue ++ Frames}) 627 | end; 628 | 629 | stream_handle({gun_error, _GunPid, _StreamRef, {stream_error, no_error, 'Stream reset by server.'}}, 630 | Stream = #{st := {_LS, closed}, mqueue := MQueue}) when MQueue =/= [] -> 631 | {ok, Stream}; 632 | stream_handle({gun_error, _GunPid, _StreamRef, Reason}, Stream) -> 633 | {shutdown, Reason, Stream}; 634 | 635 | stream_handle(Info, Stream) -> 636 | ?LOG(error, "Unexecpted stream event: ~p, stream ~0p", [Info, Stream]). 637 | 638 | handle_remote_closed(Trailers, Stream = #{st := {closed, _}}) -> 639 | case clean_hangs(Stream#{st => {closed, closed}}) of 640 | NStream = #{hangs := [{From, _}|NHangs], mqueue := MQueue} -> 641 | Events1 = [{reply, From, {ok, MQueue ++ [{eos, Trailers}]}}], 642 | Events2 = lists:map(fun({F, _}) -> {reply, F, {error, closed}} end, NHangs), 643 | {shutdown, normal, Events1 ++ Events2, NStream#{hangs => [], mqueue => []}}; 644 | NStream = #{hangs := [], mqueue := MQueue} -> 645 | {ok, NStream#{mqueue => MQueue ++ [{eos, Trailers}], 646 | stopped => erlang:system_time(millisecond)}} 647 | end; 648 | 649 | handle_remote_closed(Trailers, Stream = #{st := {Ls, _}}) -> 650 | case clean_hangs(Stream#{st => {Ls, closed}}) of 651 | NStream = #{hangs := [{From, _}|NHangs], mqueue := MQueue} -> 652 | Events1 = [{reply, From, {ok, MQueue ++ [{eos, Trailers}]}}], 653 | Events2 = lists:map(fun({F, _}) -> {reply, F, {error, closed}} end, NHangs), 654 | {ok, Events1 ++ Events2, NStream#{hangs => [], mqueue => []}}; 655 | NStream = #{hangs := [], mqueue := MQueue} -> 656 | {ok, NStream#{mqueue => MQueue ++ [{eos, Trailers}]}} 657 | end. 658 | 659 | clean_hangs(Stream = #{hangs := []}) -> 660 | Stream; 661 | clean_hangs(Stream = #{hangs := Hangs}) -> 662 | Nowts = erlang:system_time(millisecond), 663 | Hangs1 = lists:filter(fun({_, T}) -> T >= Nowts end, Hangs), 664 | Stream#{hangs => Hangs1}. 665 | 666 | %%-------------------------------------------------------------------- 667 | %% Internal funcs 668 | %%-------------------------------------------------------------------- 669 | 670 | do_connect(State = #state{server = {_, Host, Port}, client_opts = ClientOpts}) -> 671 | GunOpts = maps:get(gun_opts, ClientOpts, #{}), 672 | case gun:open(Host, Port, GunOpts) of 673 | {ok, Pid} -> 674 | %% NOTE 675 | %% By default, `gun` retries failed connection attempts 5 times, with 676 | %% 5 seconds delay in-between. Give it a bit more spare time, just in 677 | %% case. 678 | MRef = monitor(process, Pid), 679 | case gun:await_up(Pid, 60_000, MRef) of 680 | {ok, _Protocol} -> 681 | State#state{mref = MRef, gun_pid = Pid, gun_state = up}; 682 | {error, {down, Reason}} -> 683 | {error, Reason}; 684 | {error, timeout} -> 685 | _ = gun:close(Pid), 686 | {error, timeout} 687 | end; 688 | {error, Reason} -> 689 | {error, Reason} 690 | end. 691 | 692 | %%-------------------------------------------------------------------- 693 | %% Helpers 694 | 695 | flush_streams(Nowts, State = #state{streams = Streams, 696 | gun_pid = GunPid, 697 | client_opts = ClientOpts}) -> 698 | Intv = maps:get(stream_batch_delay_ms, ClientOpts, ?DEFAULT_STREAMING_DELAY), 699 | NStreams = 700 | maps:map( 701 | fun(_, Stream = #{sendbuff_size := 0}) -> 702 | Stream; 703 | (_, Stream = #{sendbuff_last_flush_ts := Ts}) 704 | when Nowts < (Ts + Intv) -> 705 | Stream; 706 | (StreamRef, Stream = #{sendbuff := IolistData, 707 | sendbuff_last_flush_ts := Ts}) 708 | when Nowts >= (Ts + Intv) -> 709 | ok = gun:data(GunPid, StreamRef, nofin, lists:reverse(IolistData)), 710 | Stream#{sendbuff := [], sendbuff_size := 0, sendbuff_last_flush_ts := Nowts} 711 | end, Streams), 712 | State#state{streams = NStreams}. 713 | 714 | maybe_send_data(Bytes, IsFin, StreamRef, 715 | Stream = #{st := {_, _RS}, 716 | sendbuff := IolistData0, 717 | sendbuff_size := BufferSize0}, GunPid, BatchSize) -> 718 | IolistData = [Bytes | IolistData0], 719 | IolistSize = BufferSize0 + iolist_size(Bytes), 720 | case IsFin == fin orelse IolistSize >= BatchSize of 721 | true -> 722 | NData = lists:reverse(IolistData), 723 | ok = gun:data(GunPid, StreamRef, IsFin, NData), 724 | case IsFin of 725 | fin -> 726 | Stream#{st := {closed, _RS}, 727 | sendbuff := [], 728 | sendbuff_size := 0 729 | }; 730 | _ -> 731 | Stream#{sendbuff := [], 732 | sendbuff_size := 0 733 | } 734 | end; 735 | false -> 736 | Stream#{sendbuff := IolistData, sendbuff_size := IolistSize} 737 | end. 738 | 739 | trailers_to_error([]) -> 740 | stream_closed_without_any_response; 741 | trailers_to_error(Trailers) -> 742 | {grpc_utils:codename( 743 | proplists:get_value(<<"grpc-status">>, Trailers, ?GRPC_STATUS_OK) 744 | ), 745 | proplists:get_value(<<"grpc-message">>, Trailers, <<>>)}. 746 | 747 | ensure_clean_timer(State = #state{tref = undefined}) -> 748 | TRef = erlang:start_timer(?STREAM_RESERVED_TIMEOUT, 749 | self(), 750 | clean_stopped_stream), 751 | State#state{tref = TRef}. 752 | 753 | ensure_flush_timer(State = #state{streams = Streams, 754 | flush_timer_ref = undefined, 755 | client_opts = ClientOpts 756 | }) -> 757 | case have_buffered_bytes(Streams) of 758 | true -> 759 | Intv = maps:get(stream_batch_delay_ms, ClientOpts, ?DEFAULT_STREAMING_DELAY), 760 | TRef = erlang:start_timer(Intv, self(), flush_streams_sendbuff), 761 | State#state{flush_timer_ref = TRef}; 762 | _ -> 763 | State 764 | end; 765 | ensure_flush_timer(State) -> 766 | State. 767 | 768 | have_buffered_bytes(Streams) when is_map(Streams) -> 769 | have_buffered_bytes(maps:next(maps:iterator(Streams))); 770 | 771 | have_buffered_bytes({_StreamRef, #{sendbuff_size := 0}, I}) -> 772 | have_buffered_bytes(maps:next(I)); 773 | have_buffered_bytes({_StreamRef, #{sendbuff_size := S}, _I}) when S > 0 -> 774 | true; 775 | have_buffered_bytes({_StreamRef, #{sendbuff_size := S}, none}) -> 776 | S > 0; 777 | have_buffered_bytes(none) -> 778 | false. 779 | 780 | format_stream(#{st := St, recvbuff := Buff, mqueue := MQueue}) -> 781 | io_lib:format("#stream{st=~p, buff_size=~w, mqueue=~p}", 782 | [St, byte_size(Buff), MQueue]). 783 | 784 | %% copied from gen.erl and gen_server.erl 785 | call(Process, Request, Options) -> 786 | Timeout = timeout(Options), 787 | ExitOnCrash = exit_on_connection_crash(Options), 788 | Mref = erlang:monitor(process, Process, [{alias, reply_demonitor}]), 789 | 790 | %% OTP-21: 791 | %% Auto-connect is asynchronous. But we still use 'noconnect' to make sure 792 | %% we send on the monitored connection, and not trigger a new auto-connect. 793 | %% 794 | erlang:send(Process, {'$gen_call', {Mref, Mref}, Request}, [noconnect]), 795 | 796 | receive 797 | {Mref, Reply} -> 798 | erlang:demonitor(Mref, [flush]), 799 | Reply; 800 | {'DOWN', Mref, _, _, Reason} -> 801 | call_result(ExitOnCrash, Reason) 802 | after Timeout -> 803 | erlang:demonitor(Mref, [flush]), 804 | receive 805 | {Mref, Reply} -> Reply 806 | after 0 -> 807 | {error, {grpc_utils:codename(?GRPC_STATUS_DEADLINE_EXCEEDED), 808 | <<"Waiting for response timeout">>}} 809 | end 810 | end. 811 | 812 | call_result(_ExitOnCrash = true, Reason) -> 813 | exit(Reason); 814 | call_result(_ExitOnCrash = false, Reason) -> 815 | error({exit, Reason}). 816 | 817 | pick(ChannName, Key) -> 818 | gproc_pool:pick_worker(ChannName, Key). 819 | 820 | assemble_grpc_headers(Encoding, MessageType, Timeout, MD) -> 821 | [{<<"content-type">>, <<"application/grpc+proto">>}, 822 | {<<"user-agent">>, <<"grpc-erlang/0.1.0">>}, 823 | {<<"grpc-encoding">>, Encoding}, 824 | {<<"grpc-message-type">>, MessageType}, 825 | {<<"te">>, <<"trailers">>}] 826 | ++ assemble_grpc_timeout_header(Timeout) 827 | ++ assemble_grpc_metadata_header(MD). 828 | 829 | assemble_grpc_timeout_header(infinity) -> 830 | []; 831 | assemble_grpc_timeout_header(Timeout) -> 832 | [{<<"grpc-timeout">>, ms2timeout(Timeout)}]. 833 | 834 | assemble_grpc_metadata_header(MD) -> 835 | maps:to_list(MD). 836 | 837 | ms2timeout(Ms) when Ms > 1000 -> 838 | [integer_to_list(Ms div 1000), $S]; 839 | ms2timeout(Ms) -> 840 | [integer_to_list(Ms), $m]. 841 | -------------------------------------------------------------------------------- /src/client/grpc_client_sup.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% The client supervisor 18 | -module(grpc_client_sup). 19 | 20 | -behaviour(supervisor). 21 | 22 | -export([ create_channel_pool/3 23 | , spec/3 24 | , stop_channel_pool/1 25 | , workers/1 26 | ]). 27 | 28 | -export([ start_link/3 29 | , init/1 30 | ]). 31 | 32 | -define(APP_SUP, grpc_sup). 33 | 34 | -type name() :: term(). 35 | 36 | -type options() :: grpc_client:client_options() 37 | | #{pool_size => non_neg_integer()}. 38 | 39 | %%-------------------------------------------------------------------- 40 | %% APIs 41 | %%-------------------------------------------------------------------- 42 | 43 | -spec create_channel_pool(name(), uri_string:uri_string(), options()) 44 | -> supervisor:startchild_ret(). 45 | create_channel_pool(Name, URL, Opts) -> 46 | case spec(Name, URL, Opts) of 47 | {ok, Spec} -> supervisor:start_child(?APP_SUP, Spec); 48 | {error, Reason} -> {error, Reason} 49 | end. 50 | 51 | -spec spec(name(), uri_string:uri_string(), options()) 52 | -> {ok, supervisor:child_spec()} | {error, term()}. 53 | spec(Name, URL, Opts) -> 54 | case uri_string:parse(URL) of 55 | #{scheme := Scheme, host := Host, port := Port} -> 56 | Server = {Scheme, Host, Port}, 57 | Spec = #{id => Name, 58 | start => {?MODULE, start_link, [Name, Server, Opts]}, 59 | restart => transient, 60 | shutdown => infinity, 61 | type => supervisor, 62 | modules => [?MODULE]}, 63 | {ok, Spec}; 64 | {error, Reason, _} -> {error, Reason} 65 | end. 66 | 67 | -spec stop_channel_pool(name()) -> ok | {error, term()}. 68 | stop_channel_pool(Name) -> 69 | case supervisor:terminate_child(?APP_SUP, Name) of 70 | ok -> 71 | _ = gproc_pool:force_delete(Name), 72 | ok = supervisor:delete_child(?APP_SUP, Name); 73 | R -> R 74 | end. 75 | 76 | -spec workers(name()) -> list(). 77 | workers(Name) -> 78 | gproc_pool:active_workers(Name). 79 | 80 | %%-------------------------------------------------------------------- 81 | %% Callbacks 82 | %%-------------------------------------------------------------------- 83 | 84 | start_link(Name, Server, Opts) -> 85 | supervisor:start_link(?MODULE, [Name, Server, Opts]). 86 | 87 | init([Name, Server, Opts]) -> 88 | Size = pool_size(Opts), 89 | ok = ensure_pool(Name, hash, [{size, Size}]), 90 | {ok, {{one_for_one, 10, 3600}, [ 91 | begin 92 | ensure_pool_worker(Name, {Name, I}, I), 93 | #{id => {Name, I}, 94 | start => {grpc_client, start_link, [Name, I, Server, Opts]}, 95 | restart => transient, 96 | shutdown => 5000, 97 | type => worker, 98 | modules => [grpc_client]} 99 | end || I <- lists:seq(1, Size)]}}. 100 | 101 | %% @private 102 | ensure_pool(Name, Type, Opts) -> 103 | try gproc_pool:new(Name, Type, Opts) 104 | catch 105 | error:exists -> ok 106 | end. 107 | 108 | %% @private 109 | ensure_pool_worker(Name, WorkName, Slot) -> 110 | try gproc_pool:add_worker(Name, WorkName, Slot) 111 | catch 112 | error:exists -> ok 113 | end. 114 | 115 | pool_size(Opts) -> 116 | maps:get(pool_size, Opts, erlang:system_info(schedulers)). 117 | -------------------------------------------------------------------------------- /src/grpc.app.src: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {application, grpc, 3 | [{description, "An Erlang implementation of gRPC"}, 4 | {vsn, "git"}, 5 | {registered, []}, 6 | {mod, {grpc_app, []}}, 7 | {applications, [kernel,stdlib,ssl,public_key,gproc,cowboy,gun]}, 8 | {env,[]}, 9 | {modules, []}, 10 | {licenses, ["Apache 2.0"]}, 11 | {links, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/grpc.appup.src: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {"0.6.14", 3 | [ 4 | {"0.6.13", 5 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 6 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}]}, 7 | {"0.6.12", 8 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 9 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}]}, 10 | {"0.6.11", 11 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 12 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 13 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 14 | {"0.6.10", 15 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 16 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 17 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 18 | {"0.6.9", 19 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 20 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 21 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 22 | {"0.6.8", 23 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 24 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 25 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 26 | {"0.6.7", 27 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 28 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 29 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 30 | {"0.6.6", 31 | [{update, grpc_client, {advanced, ["0.6.6"]}}, 32 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 33 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 34 | {"0.6.5", 35 | [{update, grpc_client, {advanced, ["0.6.5"]}}, 36 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 37 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 38 | {"0.6.4", 39 | [{update, grpc_client, {advanced, ["0.6.4"]}}, 40 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 41 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 42 | {"0.6.3", 43 | [{update, grpc_client, {advanced, ["0.6.3"]}}, 44 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 45 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 46 | {"0.6.2", 47 | [{add_module, grpc_utils}, 48 | {update, grpc_client, {advanced, ["0.6.2"]}}, 49 | {load_module, grpc_stream, brutal_purge, soft_purge, []}, 50 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 51 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 52 | {<<".*">>, []}], 53 | [ 54 | {"0.6.13", 55 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 56 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}]}, 57 | {"0.6.12", 58 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 59 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}]}, 60 | {"0.6.11", 61 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 62 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 63 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 64 | {"0.6.10", 65 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 66 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 67 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 68 | {"0.6.9", 69 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 70 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 71 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 72 | {"0.6.8", 73 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 74 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 75 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 76 | {"0.6.7", 77 | [{load_module, grpc_client, brutal_purge, soft_purge, []}, 78 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 79 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 80 | {"0.6.6", 81 | [{update, grpc_client, {advanced, ["0.6.6"]}}, 82 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 83 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 84 | {"0.6.5", 85 | [{update, grpc_client, {advanced, ["0.6.5"]}}, 86 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 87 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 88 | {"0.6.4", 89 | [{update, grpc_client, {advanced, ["0.6.4"]}}, 90 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 91 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 92 | {"0.6.3", 93 | [{update, grpc_client, {advanced, ["0.6.3"]}}, 94 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 95 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 96 | {"0.6.2", 97 | [{delete_module, grpc_utils}, 98 | {update, grpc_client, {advanced, ["0.6.2"]}}, 99 | {load_module, grpc_stream, brutal_purge, soft_purge, []}, 100 | {load_module, grpc_client_sup, brutal_purge, soft_purge, []}, 101 | {load_module, grpc, brutal_purge, soft_purge, []}]}, 102 | {<<".*">>, []}]}. 103 | -------------------------------------------------------------------------------- /src/grpc.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc). 18 | 19 | %% APIs 20 | -export([ start_server/3 21 | , start_server/4 22 | , stop_server/1 23 | ]). 24 | 25 | -ifdef(TEST). 26 | -compile(export_all). 27 | -compile(nowarn_export_all). 28 | -endif. 29 | 30 | -type listen_on() :: {inet:ip_address(), inet:port_number()} | inet:port_number(). 31 | -type services() :: #{protos := [module()], 32 | services := #{ServiceName :: atom() => HandlerModule :: module()} 33 | }. 34 | 35 | -type option() :: {ssl_options, ssl:tls_server_option()} 36 | | {ranch_opts, ranch:opts()} 37 | | {cowboy_opts, cowboy_http2:opts()}. 38 | 39 | %% TODO: Figure out types. 40 | -type metadata_key() :: term(). 41 | -type metadata_value() :: term(). 42 | -type metadata() :: #{metadata_key() => metadata_value()}. 43 | 44 | %% NOTE: Expected by generated code. 45 | -type options() :: grpc_client:options(). 46 | 47 | -export_type([ metadata/0 48 | , metadata_key/0 49 | , metadata_value/0 50 | , options/0]). 51 | 52 | %%-------------------------------------------------------------------- 53 | %% APIs 54 | %%-------------------------------------------------------------------- 55 | 56 | -spec start_server(any(), listen_on(), services()) 57 | -> {ok, pid()} | {error, term()}. 58 | start_server(Name, ListenOn, Services) -> 59 | start_server(Name, ListenOn, Services, []). 60 | 61 | -spec start_server(any(), listen_on(), services(), [option()]) 62 | -> {ok, pid()} | {error, term()}. 63 | %% @doc Start a gRPC server 64 | start_server(Name, ListenOn, Services, Options) -> 65 | Services1 = enable_default_services( 66 | proplists:get_value(enable_default_services, Options, all), 67 | Services), 68 | 69 | UserOptions = #{services => compile_service_rules(Services1)}, 70 | start_http_server(Name, listen(ListenOn), Options, UserOptions). 71 | 72 | enable_default_services(all, Services) -> 73 | DefatServicesName = ['grpc.health.v1.Health', 74 | 'grpc.reflection.v1alpha.ServerReflection' 75 | ], 76 | enable_default_services(DefatServicesName, Services); 77 | 78 | enable_default_services([], Services) -> 79 | Services; 80 | enable_default_services(['grpc.health.v1.Health' | Ls], 81 | #{protos := Protos, services := Services}) -> 82 | enable_default_services( 83 | Ls, 84 | #{protos => ['grpc_health_pb' | Protos], 85 | services => Services#{'grpc.health.v1.Health' => grpc_health_svr}} 86 | ); 87 | enable_default_services(['grpc.reflection.v1alpha.ServerReflection' | Ls], 88 | #{protos := Protos, services := Services}) -> 89 | enable_default_services( 90 | Ls, 91 | #{protos => ['grpc_reflection_pb' | Protos], 92 | services => 93 | Services#{ 94 | 'grpc.reflection.v1alpha.ServerReflection' => grpc_reflection_svr}} 95 | ). 96 | 97 | %% @private 98 | %% {ServicesName, FunName} => ServiceDefs 99 | compile_service_rules(Services0) -> 100 | Protos = maps:get(protos, Services0, []), 101 | 102 | Defineds = lists:foldr(fun(Pb, Acc) -> 103 | [{S, Pb} || S <- Pb:get_service_names()] ++ Acc 104 | end, [], Protos), 105 | Services = maps:get(services, Services0, #{}), 106 | maps:fold(fun(SvrName, Handler, Acc) -> 107 | case lists:keyfind(SvrName, 1, Defineds) of 108 | false -> Acc; 109 | {_, Pb} -> 110 | lists:foldl(fun(RpcName, Acc2) -> 111 | RpcDef = Pb:find_rpc_def(SvrName, RpcName), 112 | Acc2#{{atom_to_binary(SvrName, utf8), 113 | atom_to_binary(RpcName, utf8) 114 | } => RpcDef#{pb => Pb, handler => Handler} 115 | } 116 | end, Acc, Pb:get_rpc_names(SvrName)) 117 | end 118 | end, #{}, Services). 119 | 120 | -spec stop_server(any()) -> ok | {error, not_found}. 121 | 122 | %% @doc Stop a gRPC server 123 | stop_server(Name) -> 124 | cowboy:stop_listener(Name). 125 | 126 | %% @private 127 | listen({IPAddr, Port}) 128 | when is_tuple(IPAddr), 129 | is_integer(Port) -> 130 | {IPAddr, Port}; 131 | listen(AddrStr) 132 | when is_list(AddrStr); 133 | is_binary(AddrStr) -> 134 | case re:split(AddrStr, ":", [{return, list}]) of 135 | [IPAddr, Port] -> 136 | {ok, IPAddr1} = inet:parse_address(IPAddr), 137 | {IPAddr1, list_to_integer(Port)}; 138 | [Port] -> 139 | {{0,0,0,0}, list_to_integer(Port)} 140 | end; 141 | listen(Port) when is_integer(Port) -> 142 | {{0,0,0,0}, Port}. 143 | 144 | 145 | %%-------------------------------------------------------------------- 146 | %% Internal funcs 147 | %%-------------------------------------------------------------------- 148 | 149 | start_http_server(Name, {Ip, Port}, Options, UserOptions) -> 150 | Dispatch = cowboy_router:compile( 151 | [{'_', [{"/:service/:method", grpc_stream, UserOptions}]}] 152 | ), 153 | ProtoOpts0 = #{env => #{dispatch => Dispatch}, 154 | protocols => [http2], 155 | max_received_frame_rate => {100000, 1000}, 156 | stream_handlers => [grpc_stream_h, cowboy_stream_h] 157 | }, 158 | 159 | Ssloptions = proplists:get_value(ssl_options, Options, []), 160 | TransOpts0 = #{max_connections => 1024000, 161 | socket_opts => [{ip, Ip}, {port, Port} | Ssloptions] 162 | }, 163 | TransOpts = maps:merge(proplists:get_value(ranch_opts, Options, #{}), TransOpts0), 164 | ProtoOpts = maps:merge(proplists:get_value(cowboy_opts, Options, #{}), ProtoOpts0), 165 | StartFun = case Ssloptions of 166 | [] -> start_clear; 167 | _ -> start_tls 168 | end, 169 | cowboy:StartFun(Name, TransOpts, ProtoOpts). 170 | -------------------------------------------------------------------------------- /src/grpc_app.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_app). 18 | 19 | -behaviour(application). 20 | 21 | -export([ start/2 22 | , stop/1 23 | ]). 24 | 25 | start(_StartType, _StartArgs) -> 26 | grpc_sup:start_link(). 27 | 28 | stop(_State) -> 29 | ok. 30 | -------------------------------------------------------------------------------- /src/grpc_frame.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_frame). 18 | 19 | -include("grpc.hrl"). 20 | 21 | -export([ encode/2 22 | , split/2 23 | ]). 24 | 25 | -export_type([encoding/0]). 26 | 27 | -type encoding() :: identity | gzip. 28 | 29 | -define(GRPC_ERROR(Status, Message), {grpc_error, {Status, Message}}). 30 | -define(THROW(Status, Message), throw(?GRPC_ERROR(Status, Message))). 31 | 32 | %%-------------------------------------------------------------------- 33 | %% APIs 34 | %%-------------------------------------------------------------------- 35 | 36 | -spec encode(encoding(), binary()) -> binary(). 37 | 38 | encode(Encoding, Bin) when is_binary(Encoding) -> 39 | encode(binary_to_existing_atom(Encoding, utf8), Bin); 40 | encode(gzip, Bin) -> 41 | CompressedBin = zlib:gzip(Bin), 42 | Length = byte_size(CompressedBin), 43 | <<1, Length:32, CompressedBin/binary>>; 44 | encode(identity, Bin) -> 45 | Length = byte_size(Bin), 46 | <<0, Length:32, Bin/binary>>; 47 | encode(Encoding, _) -> 48 | throw({error, {unknown_encoding, Encoding}}). 49 | 50 | -spec split(binary(), encoding()) -> 51 | {Remaining :: binary(), Frames :: [binary()]}. 52 | 53 | split(Frame, Encoding) when is_binary(Encoding) -> 54 | split(Frame, binary_to_existing_atom(Encoding, utf8)); 55 | split(Frame, Encoding) -> 56 | split(Frame, Encoding, []). 57 | 58 | split(<<>>, _Encoding, Acc) -> 59 | {<<>>, lists:reverse(Acc)}; 60 | split(<<0, Length:32, Encoded:Length/binary, Rest/binary>>, Encoding, Acc) -> 61 | split(Rest, Encoding, [Encoded | Acc]); 62 | split(<<1, Length:32, Compressed:Length/binary, Rest/binary>>, Encoding, Acc) -> 63 | Encoded = case Encoding of 64 | gzip -> 65 | try zlib:gunzip(Compressed) 66 | catch 67 | error:data_error -> 68 | ?THROW(?GRPC_STATUS_INTERNAL, 69 | <<"Could not decompress but compression algorithm ", 70 | (atom_to_binary(Encoding, utf8))/binary, " is supported">>) 71 | end; 72 | _ -> 73 | ?THROW(?GRPC_STATUS_UNIMPLEMENTED, 74 | <<"Compression mechanism ", (atom_to_binary(Encoding, utf8))/binary, 75 | " used for received frame not supported">>) 76 | end, 77 | split(Rest, Encoding, [Encoded | Acc]); 78 | split(Bin, _Encoding, Acc) -> 79 | {Bin, lists:reverse(Acc)}. 80 | -------------------------------------------------------------------------------- /src/grpc_health_svr.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_health_svr). 18 | 19 | -behavior(grpc_health_v_1_health_bhvr). 20 | 21 | -export([ check/2 22 | , watch/2 23 | ]). 24 | 25 | -dialyzer({nowarn_function, [check/2, watch/2]}). 26 | 27 | %%-------------------------------------------------------------------- 28 | %% Callbacks 29 | 30 | -spec check(grpc_health_pb:health_check_request(), grpc:metadata()) 31 | -> {ok, grpc_health_pb:health_check_response(), grpc:metadata()} 32 | | {error, grpc_stream:error_response()}. 33 | check(#{service := _Service}, _Md) -> 34 | %% TODO: How to get the Service running status? 35 | {ok, #{status => 'SERVING'}, _Md}. 36 | 37 | -spec watch(grpc_stream:stream(), grpc:metadata()) 38 | -> {ok, grpc_stream:stream()}. 39 | watch(Stream, _Md) -> 40 | %% TODO: How to get the Service running status? 41 | {eos, [#{service := _Service}], NStream} = grpc_stream:recv(Stream), 42 | RelpyLp = fun _Lp() -> 43 | grpc_stream:reply(NStream, [#{status => 'SERVING'}]), 44 | timer:sleep(15000), 45 | _Lp() 46 | end, 47 | RelpyLp(), 48 | {ok, NStream}. 49 | -------------------------------------------------------------------------------- /src/grpc_lib.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | 20 | -module(grpc_lib). 21 | 22 | -export([ auth_fun/1 23 | , decode_input/4 24 | , encode_output/4 25 | , maybe_encode_header/1 26 | , maybe_encode_headers/1 27 | , maybe_decode_header/1 28 | , keytake/3 29 | ]). 30 | 31 | -export([list_snake_case/1]). 32 | 33 | -type cert() :: term(). 34 | 35 | -spec auth_fun(Directory::string()) -> fun((cert()) -> {true, string()} | false). 36 | %% @doc returns a function that can be used to authenticate against the 37 | %% keys that are stored in a certain directory. 38 | %% The base name of the key file is used as the identity. 39 | auth_fun(Directory) -> 40 | Ids = issuer_ids_from_directory(Directory), 41 | fun(Cert) -> 42 | {ok, IssuerID} = public_key:pkix_issuer_id(Cert, self), 43 | case maps:find(IssuerID, Ids) of 44 | {ok, Identity} -> 45 | {true, Identity}; 46 | error -> 47 | false 48 | end 49 | end. 50 | 51 | -spec decode_input(ServiceName::atom(), RpcName::atom(), 52 | DecoderModule::module(), Message::binary() | eof) 53 | -> map() | eof. 54 | %% @doc Decode input protobuf message to map. 55 | decode_input(_, _, _, eof) -> 56 | eof; 57 | decode_input(ServiceName, RpcName, DecoderModule, Msg) -> 58 | #{input := MsgName} = DecoderModule:fetch_rpc_def(ServiceName, RpcName), 59 | DecoderModule:decode_msg(Msg, MsgName). 60 | 61 | -spec encode_output(ServiceName::atom(), RpcName::atom(), 62 | DecoderModule::module(), Message::map()) -> binary(). 63 | %% @doc Encode response message (map) to binary protobuf message. 64 | encode_output(ServiceName, RpcName, DecoderModule, Msg) -> 65 | #{output := MsgName} = DecoderModule:fetch_rpc_def(ServiceName, RpcName), 66 | DecoderModule:encode_msg(Msg, MsgName). 67 | 68 | -spec maybe_encode_header(Header::{grpc:metadata_key(), 69 | grpc:metadata_value()}) -> 70 | {grpc:metadata_key(), grpc:metadata_value()}. 71 | %% @doc Encode header using Base64 if the header name ends with "-bin". 72 | maybe_encode_header({Key, Value} = Header) -> 73 | case is_bin_header(Key) of 74 | true -> 75 | {Key, base64:encode(Value)}; 76 | false -> 77 | Header 78 | end. 79 | 80 | -spec maybe_decode_header(Header::{grpc:metadata_key(), 81 | grpc:metadata_value()}) -> 82 | {grpc:metadata_key(), grpc:metadata_value()}. 83 | %% @doc Decode header from Base64 if the header name ends with "-bin". 84 | maybe_decode_header({Key, Value} = Header) -> 85 | case is_bin_header(Key) of 86 | true -> 87 | {Key, decode(Value)}; 88 | false -> 89 | Header 90 | end. 91 | 92 | %% golang gRPC implementation does not add the padding that the Erlang 93 | %% decoder needs... 94 | decode(Base64) when byte_size(Base64) rem 4 == 3 -> 95 | base64:decode(< 105 | maps:map(fun(K, V) -> 106 | case is_bin_header(K) of 107 | true -> 108 | base64:encode(V); 109 | false -> 110 | V 111 | end 112 | end, Headers). 113 | 114 | -spec keytake(Key::term(), KVList::[{term(), term()}], Default::term()) -> 115 | {Value::term(), NewKVList::[{term(), term()}]}. 116 | %% @doc Get the value for a certain key from a list and remove it from the 117 | %% list. 118 | %% 119 | %% Returns the value (or the default, if it was not found) and the list with 120 | %% this key removed. 121 | keytake(Key, KVList, Default) -> 122 | case lists:keytake(Key, 1, KVList) of 123 | {value, {_, Value}, List2} -> 124 | {Value, List2}; 125 | false -> 126 | {Default, KVList} 127 | end. 128 | 129 | list_snake_case(Name) when is_binary(Name) -> 130 | list_snake_case(binary_to_list(Name)); 131 | list_snake_case(Name) when is_atom(Name) -> 132 | list_snake_case(atom_to_list(Name)); 133 | list_snake_case(NameString) -> 134 | Snaked = lists:foldl( 135 | fun(RE, Snaking) -> 136 | re:replace(Snaking, RE, "\\1_\\2", [{return, list}, global]) 137 | end, 138 | NameString, 139 | [%% uppercase followed by lowercase 140 | "(.)([A-Z][a-z]+)", 141 | %% any consecutive digits 142 | "(.)([0-9]+)", 143 | %% uppercase with lowercase 144 | %% or digit before it 145 | "([a-z0-9])([A-Z])"]), 146 | Snaked1 = string:replace(Snaked, ".", "_", all), 147 | Snaked2 = string:replace(Snaked1, "__", "_", all), 148 | string:to_lower(unicode:characters_to_list(Snaked2)). 149 | 150 | %%% --------------------------------------------------------------------------- 151 | %%% Internal functions 152 | %%% --------------------------------------------------------------------------- 153 | 154 | is_bin_header(Key) -> 155 | binary:longest_common_suffix([Key, <<"-bin">>]) == 4. 156 | 157 | issuer_ids_from_directory(Dir) -> 158 | {ok, Filenames} = file:list_dir(Dir), 159 | Keyfiles = lists:filter(fun(N) -> 160 | case filename:extension(N) of 161 | ".pem" -> true; 162 | ".crt" -> true; 163 | _ -> false 164 | end 165 | end, Filenames), 166 | maps:from_list([issuer_id_from_file(filename:join([Dir, F])) 167 | || F <- Keyfiles]). 168 | 169 | issuer_id_from_file(Filename) -> 170 | {certfile_to_issuer_id(Filename), 171 | filename:rootname(filename:basename(Filename))}. 172 | 173 | certfile_to_issuer_id(Filename) -> 174 | {ok, Data} = file:read_file(Filename), 175 | [{'Certificate', Cert, not_encrypted}] = public_key:pem_decode(Data), 176 | {ok, IssuerID} = public_key:pkix_issuer_id(Cert, self), 177 | IssuerID. 178 | -------------------------------------------------------------------------------- /src/grpc_reflection_svr.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_reflection_svr). 18 | 19 | -behavior(grpc_reflection_v_1alpha_server_reflection_bhvr). 20 | 21 | -export([server_reflection_info/2]). 22 | 23 | %%-------------------------------------------------------------------- 24 | %% Callbacks 25 | 26 | -spec server_reflection_info(grpc_stream:stream(), grpc:metadata()) 27 | -> {ok, grpc_stream:stream()}. 28 | 29 | server_reflection_info(Stream, _Md) -> 30 | LoopRecv = fun _Lp(St) -> 31 | case grpc_stream:recv(St) of 32 | {more, Reqs, NSt} -> 33 | io:format("reflection req: ~p~n", [Reqs]), 34 | _Lp(NSt); 35 | {eos, Reqs, NSt} -> 36 | io:format("reflection req: ~p~n", [Reqs]), 37 | NSt 38 | end 39 | end, 40 | NStream = LoopRecv(Stream), 41 | {ok, NStream}. 42 | -------------------------------------------------------------------------------- /src/grpc_stream.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | %% The gRPC stream 18 | -module(grpc_stream). 19 | 20 | -behavior(cowboy_handler). 21 | 22 | -include("grpc.hrl"). 23 | 24 | %% APIs 25 | -export([ recv/1 26 | , recv/2 27 | , reply/2 28 | ]). 29 | 30 | %% cowboy callbacks 31 | -export([init/2]). 32 | 33 | %% Internal callbacks 34 | -export([ handle_in/2 35 | , handle_out/3 36 | ]). 37 | 38 | -export_type([stream/0, error_response/0]). 39 | 40 | -type stream() :: #{ req := cowboy_req:req() 41 | , rest := binary() 42 | , metadata := map() 43 | , encoding := grpc_frame:encoding() 44 | , compression := grpc_frame:encoding() %% TODO: Figure out types 45 | , decoder := function() 46 | , encoder := function() 47 | , handler := {atom(), atom()} 48 | , is_unary := boolean() 49 | , input_stream := boolean() 50 | , output_stream := boolean() 51 | , client_info := map() 52 | }. 53 | 54 | %% TODO: Figure out types. 55 | -type error_response() :: term(). 56 | 57 | %%-------------------------------------------------------------------- 58 | %% APIs 59 | %%-------------------------------------------------------------------- 60 | 61 | recv(St) -> 62 | recv(St, 15000). 63 | 64 | -spec recv(stream(), timeout()) -> {more | eos, [map()], stream()}. 65 | recv(St = #{req := Req, 66 | rest := Rest, 67 | decoder := Decoder, 68 | compression := Compression}, Timeout) -> 69 | {More, Bytes, NReq} = cowboy_req:read_body(Req, #{length => 5, period => Timeout}), 70 | {NRest, Frames} = grpc_frame:split(<>, Compression), 71 | 72 | NSt = St#{rest => NRest, req => NReq}, 73 | Requests = lists:map(Decoder, Frames), 74 | case More of 75 | more -> {more, Requests, NSt}; 76 | _ -> {eos, Requests, NSt} 77 | end. 78 | 79 | -spec reply(stream(), map() | list(map())) -> ok. 80 | 81 | reply(St, Resp) when is_map(Resp) -> 82 | reply(St, [Resp]); 83 | 84 | reply(#{req := Req, 85 | encoder := Encoder, 86 | compression := Compression}, Resps) -> 87 | IoData = lists:map(fun(F) -> 88 | grpc_frame:encode(Compression, F) 89 | end, lists:map(Encoder, Resps)), 90 | ok = cowboy_req:stream_body(IoData, nofin, Req). 91 | 92 | %%-------------------------------------------------------------------- 93 | %% Cowboy handler callback 94 | 95 | init(Req, Options) -> 96 | St = do_init_state( 97 | #{req => Req, 98 | rest => <<>>, 99 | metadata => #{}, 100 | encoding => identity, 101 | compression => maps:get(compression, Options, identity), 102 | timeout => infinity}, Req), 103 | Services = maps:get(services, Options, #{}), 104 | RpcServicesAndName = {_, ReqRpc} = 105 | {cowboy_req:binding(service, Req), cowboy_req:binding(method, Req)}, 106 | case maps:get(RpcServicesAndName, Services, undefined) of 107 | undefined -> 108 | shutdown(?GRPC_STATUS_NOT_FOUND, <<"Service Not Found">>, send_headers_first(St)); 109 | Defs -> 110 | case authenticate(Req, Options) of 111 | {true, ClientInfo} -> 112 | ReqRpc1 = list_to_existing_atom(grpc_lib:list_snake_case(ReqRpc)), 113 | NSt = St#{ 114 | decoder => decoder_func(Defs), 115 | encoder => encoder_func(Defs), 116 | handler => {maps:get(handler, Defs), ReqRpc1}, 117 | is_unary => not maps:get(input_stream, Defs) 118 | andalso not maps:get(output_stream, Defs), 119 | input_stream => maps:get(input_stream, Defs), 120 | output_stream => maps:get(output_stream, Defs), 121 | client_info => ClientInfo 122 | }, 123 | try 124 | before_loop(send_headers_first(NSt)) 125 | catch T:R:Stk -> 126 | ?LOG(error, "Stream process crashed: ~p, ~p, stacktrace: ~p~n", 127 | [T, R, Stk]), 128 | shutdown(?GRPC_STATUS_INTERNAL, 129 | <<"Internal Error: unexpected crash">>, St) 130 | end; 131 | _ -> 132 | shutdown(?GRPC_STATUS_UNAUTHENTICATED, 133 | <<"Not Authenticated">>, St) 134 | end 135 | end. 136 | 137 | do_init_state(St, #{headers := Headers}) -> 138 | maps:fold(fun process_header/3, St, Headers). 139 | 140 | process_header(<<"grpc-timeout">>, Value, Acc) -> 141 | Acc#{timeout => Value}; 142 | process_header(<<"grpc-encoding">>, Value, Acc) -> 143 | Acc#{encoding => Value}; 144 | process_header(<<"grpc-message-type">>, Value, Acc) -> 145 | Acc#{message_type => Value}; 146 | process_header(<<"user-agent">>, Value, Acc) -> 147 | Acc#{user_agent => Value}; 148 | process_header(<<"content-type">>, Value, Acc) -> 149 | Acc#{content_type => Value}; 150 | process_header(K, _Value, Acc) 151 | when K == <<"te">>; 152 | K == <<"content-length">> -> 153 | %% XXX: not clear what should be done with this header 154 | Acc; 155 | process_header(Key, Value, #{metadata := Metadata} = Acc) -> 156 | {_, DecodedValue} = grpc_lib:maybe_decode_header({Key, Value}), 157 | Acc#{metadata => Metadata#{Key => DecodedValue}}. 158 | 159 | authenticate(Req, #{auth_fun := AuthFun}) when is_function(AuthFun) -> 160 | case cowboy_req:cert(Req) of 161 | undefined -> 162 | false; 163 | Cert when is_binary(Cert) -> 164 | AuthFun(Cert) 165 | end; 166 | authenticate(_Req, _Options) -> 167 | {true, undefined}. 168 | 169 | decoder_func(#{pb := Pb, input := InputName}) -> 170 | fun(Frame) -> 171 | Pb:decode_msg(Frame, InputName) 172 | end. 173 | encoder_func(#{pb := Pb, output := OutputName}) -> 174 | fun(Msg) -> 175 | Pb:encode_msg(Msg, OutputName) 176 | end. 177 | 178 | before_loop(St = #{is_unary := true}) -> 179 | Req = maps:get(req, St), 180 | case cowboy_recv(Req) of 181 | {ok, Bytes, NReq} -> 182 | Rest = maps:get(rest, St), 183 | Compression = maps:get(compression, St), 184 | {NRest, Frames} = grpc_frame:split(<>, Compression), 185 | InEvnts = [{handle_in, [Frame]} || Frame <- Frames], 186 | case events(InEvnts, St#{rest => NRest, req => NReq}) of 187 | {shutdown, Code, Message, NSt} -> 188 | shutdown(Code, Message, NSt); 189 | NSt -> 190 | shutdown(?GRPC_STATUS_OK, <<"">>, NSt) 191 | end; 192 | {error, _Reason} -> 193 | shutdown(?GRPC_STATUS_INTERNAL, 194 | <<"Internal Error: failed to receive body bytes">>, 195 | St) 196 | end; 197 | 198 | before_loop(St = #{is_unary := false}) -> 199 | try 200 | Metadata = maps:get(metadata, St), 201 | {Mod, Fun} = maps:get(handler, St), 202 | case apply(Mod, Fun, [St, Metadata]) of 203 | {ok, NSt} -> 204 | shutdown(?GRPC_STATUS_OK, <<>>, NSt); 205 | {Code, Reason, NSt} -> 206 | shutdown(Code, Reason, NSt) 207 | end 208 | catch T:R:Stk -> 209 | ?LOG(error, "Handle frame crashed: {~p, ~p} stacktrace: ~0p~n", 210 | [T, R, Stk]), 211 | shutdown(?GRPC_STATUS_INTERNAL, 212 | <<"Internal Error: crashed to execute callback function">>, 213 | St) 214 | end. 215 | 216 | events([], St) -> 217 | St; 218 | events([{F, Args} | Events], St) -> 219 | case apply(?MODULE, F, Args ++ [St]) of 220 | {shutdown, Code, Message} -> 221 | {shutdown, Code, Message, St}; 222 | {ok, NEvents, NSt} -> 223 | events(NEvents ++ Events, NSt); 224 | {ok, NSt} -> 225 | events(Events, NSt) 226 | end. 227 | 228 | shutdown(Status, Message, St) -> 229 | Req = maps:get(req, St), 230 | Trailers0 = #{<<"grpc-status">> => Status}, 231 | Trailers1 = case Message of 232 | <<>> -> Trailers0; 233 | _ -> Trailers0#{<<"grpc-message">> => Message} 234 | end, 235 | cowboy_send_trailers(Trailers1, Req), 236 | {ok, Req, []}. 237 | 238 | headers(St) -> 239 | Meta = maps:get(metadata, St), 240 | Headers = case maps:get(compression, St) of 241 | gzip -> 242 | Meta#{<<"grpc-encoding">> => <<"gzip">>}; 243 | _ -> Meta 244 | end, 245 | NHeaders = Headers#{ 246 | <<"content-type">> => <<"application/grpc">> 247 | }, 248 | grpc_lib:maybe_encode_headers(NHeaders). 249 | 250 | %% Ret :: {shutdown, Code, Message} 251 | %% | {ok, NEvents, NSt} 252 | %% 253 | %% event :: {handle_out, [headers, Hs]} 254 | %% | {handle_rpc, [x]} 255 | handle_in(Frame, St) -> 256 | Decoder = maps:get(decoder, St), 257 | try 258 | Request = Decoder(Frame), 259 | Metadata = maps:get(metadata, St), 260 | {Mod, Fun} = maps:get(handler, St), 261 | case apply(Mod, Fun, [Request, Metadata]) of 262 | {ok, Resp, NMetadata} -> 263 | {ok, [{handle_out, [reply, Resp]}], St#{metadata => NMetadata}}; 264 | {error, Code} -> 265 | %% FIXME: Streaming: shutdown / reply_error ?? 266 | {shutdown, Code, <<"">>} 267 | end 268 | catch T:R:Stk -> 269 | ?LOG(error, "Handle frame crashed: {~p, ~p} stacktrace: ~0p~n", 270 | [T, R, Stk]), 271 | {shutdown, ?GRPC_STATUS_INTERNAL, <<"RPC Execution Crashed">>} 272 | end. 273 | 274 | handle_out(reply, Resp, St) -> 275 | Encoder = maps:get(encoder, St), 276 | Compression = maps:get(compression, St), 277 | Bytes = grpc_frame:encode(Compression, Encoder(Resp)), 278 | 279 | #{req := Req} = St, 280 | _ = cowboy_send_body(Bytes, Req), 281 | {ok, St}. 282 | 283 | send_headers_first(St) -> 284 | Req = maps:get(req, St), 285 | NReq = cowboy_send_header(headers(St), Req), 286 | St#{req => NReq}. 287 | 288 | %%-------------------------------------------------------------------- 289 | %% Cowboy layer funcs 290 | %%-------------------------------------------------------------------- 291 | 292 | cowboy_recv(Req) -> 293 | cowboy_recv(Req, <<>>). 294 | 295 | cowboy_recv(Req, Acc) -> 296 | %% FIXME: Streaming?? 297 | %% Read at least 5 bytes 298 | case catch cowboy_req:read_body(Req, #{length => 5}) of 299 | {ok, Bytes, NReq} -> 300 | {ok, <>, NReq}; 301 | {more, Bytes, NReq} -> 302 | cowboy_recv(NReq, <>); 303 | {'EXIT', {Reason, _Stk}} -> 304 | ?LOG(error, "Read body occuring an error: ~p, stacktrace: ~p~n", [Reason, _Stk]), 305 | {error, Reason} 306 | end. 307 | 308 | cowboy_send_header(Headers, Req) -> 309 | cowboy_req:stream_reply(200, Headers, Req). 310 | 311 | cowboy_send_body(Bytes, Req) -> 312 | ok = cowboy_req:stream_body(Bytes, nofin, Req). 313 | 314 | cowboy_send_trailers(Trailers, Req) -> 315 | ok = cowboy_req:stream_trailers(Trailers, Req). 316 | -------------------------------------------------------------------------------- /src/grpc_stream_h.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | 20 | %% @doc This cowboy stream handler takes care of flow control. 21 | %% 22 | %% It keeps track of the number of bytes received on a stream 23 | %% and sends a {flow, ...} command when this reaches a certain threshold, so 24 | %% that the stream and connection windows are adjusted (Cowboy will send 25 | %% two WINDOW_UPDATE frames to the client, one for the stream and one for the 26 | %% connection). 27 | %% 28 | %% The goal is to send the updates in such a way that the traffic will 29 | %% never be stalled, but at the same time limiting the number of WINDOW_UPDATE 30 | %% frames. Since this implementation has no knowledge of the specifics of the 31 | %% stream, it must take a very general approach. The chosen approach is to 32 | %% "top up" the window to its original size (65535 bytes) as soon as it is 33 | %% 50% depleted. 34 | 35 | -module(grpc_stream_h). 36 | 37 | -export([init/3]). 38 | -export([data/4]). 39 | -export([info/3]). 40 | -export([terminate/3]). 41 | -export([early_error/5]). 42 | 43 | -record(state, 44 | { next :: any() 45 | , bytes_received :: integer() 46 | }). 47 | 48 | -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) 49 | -> {cowboy_stream:commands(), #state{}}. 50 | init(StreamID, Req, Opts) -> 51 | {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts), 52 | {Commands0, #state{bytes_received=0, next=Next}}. 53 | 54 | -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) 55 | -> {cowboy_stream:commands(), State} when State::#state{}. 56 | data(StreamID, IsFin, Data, State0=#state{bytes_received = Received, 57 | next=Next0}) -> 58 | {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), 59 | Size = size(Data), 60 | TotalReceived = Size + Received, 61 | %% io:format("received ~p bytes, total now: ~p~n", [Size, TotalReceived]), 62 | case TotalReceived > 32767 of 63 | false -> 64 | {Commands0, State0#state{next=Next, 65 | bytes_received = TotalReceived}}; 66 | true -> 67 | Increment = TotalReceived, %% top up 68 | %% io:format("sending WINDOW_UPDATEs (~p bytes)~n", [Increment]), 69 | {[{flow, Increment} | Commands0], 70 | State0#state{next=Next, bytes_received = 0}} 71 | end. 72 | 73 | -spec info(cowboy_stream:streamid(), any(), State) 74 | -> {cowboy_stream:commands(), State} when State::#state{}. 75 | info(StreamID, Info, State0=#state{next=Next0}) -> 76 | {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), 77 | Commands = remove_date_and_server(Commands0), 78 | {Commands, State0#state{next=Next}}. 79 | 80 | -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). 81 | terminate(StreamID, Reason, #state{next=Next}) -> 82 | cowboy_stream:terminate(StreamID, Reason, Next). 83 | 84 | -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), 85 | cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp 86 | when Resp::cowboy_stream:resp_command(). 87 | early_error(StreamID, Reason, PartialReq, Resp, Opts) -> 88 | cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). 89 | 90 | %%----------------------------------------------------------------------------- 91 | %% Internal functions 92 | %%----------------------------------------------------------------------------- 93 | 94 | %% cowboy adds headers for "date" and "server", these must be removed. 95 | remove_date_and_server(Commands) -> 96 | F = fun({headers, Status, Headers}) -> 97 | {headers, Status, maps:without([<<"date">>, <<"server">>], Headers)}; 98 | (Other) -> 99 | Other 100 | end, 101 | [F(C) || C <- Commands]. 102 | -------------------------------------------------------------------------------- /src/grpc_sup.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_sup). 18 | 19 | -behaviour(supervisor). 20 | 21 | -export([start_link/0]). 22 | 23 | -export([init/1]). 24 | 25 | -define(SERVER, ?MODULE). 26 | 27 | start_link() -> 28 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 29 | 30 | init([]) -> 31 | {ok, {{one_for_all, 5, 60}, []}}. 32 | -------------------------------------------------------------------------------- /src/grpc_utils.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_utils). 18 | 19 | -include("grpc.hrl"). 20 | 21 | -export([ codename/1 22 | ]). 23 | 24 | -spec codename(grpc_status()) -> grpc_status_name(). 25 | codename(?GRPC_STATUS_OK) -> ok; 26 | codename(?GRPC_STATUS_CANCELLED) -> cancelled; 27 | codename(?GRPC_STATUS_UNKNOWN) -> unknown; 28 | codename(?GRPC_STATUS_INVALID_ARGUMENT) -> invalid_argument; 29 | codename(?GRPC_STATUS_DEADLINE_EXCEEDED) -> deadline_exceeded; 30 | codename(?GRPC_STATUS_NOT_FOUND) -> not_found; 31 | codename(?GRPC_STATUS_ALREADY_EXISTS) -> already_exists; 32 | codename(?GRPC_STATUS_PERMISSION_DENIED) -> permission_denied; 33 | codename(?GRPC_STATUS_RESOURCE_EXHAUSTED) -> resource_exhausted; 34 | codename(?GRPC_STATUS_FAILED_PRECONDITION) -> failed_precondition; 35 | codename(?GRPC_STATUS_ABORTED) -> aborted; 36 | codename(?GRPC_STATUS_OUT_OF_RANGE) -> out_of_range; 37 | codename(?GRPC_STATUS_UNIMPLEMENTED) -> unimplemented; 38 | codename(?GRPC_STATUS_INTERNAL) -> internal; 39 | codename(?GRPC_STATUS_UNAVAILABLE) -> unavailable; 40 | codename(?GRPC_STATUS_DATA_LOSS) -> data_loss; 41 | codename(?GRPC_STATUS_UNAUTHENTICATED) -> unauthenticated. 42 | -------------------------------------------------------------------------------- /test/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUD6sUqo048vpmkcfYuT3v3MJ/uzowDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDcwMjQwMTFaFw0zMDEx 5 | MDUwMjQwMTFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDNr5eU5aQgf7oFh1+LZiCfG+ULDOGrelpmXYhlvvho 8 | NLzg6gr2NOa6XerE4Yd/hegK2DPoatWj2kVzW7ZY13gPtqoqL8b8cH3teQhWPT4B 9 | gnczqDV7b3cyTnHyc/1wsTffSJUsEwo/nrPSMID4w2WaVfOLgRcUNos/szJdmOuY 10 | AZgJF+Y87JSdv/UaSPystTmkNhVZmRf0XIKtmqX9iVGwyLkv7Yr/Em3IJD9AWNQt 11 | c5qj6DxxZDTqZhQNdx/5jNW1TyNkQd43PmwQiZAXHplQqR4Amb9TfIUfF2ulbCuh 12 | IsUWKlBN3dNgXmUYWe8IeofQ4P1o2+NmThuJJnQon/ItAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBSpESzlo9SX2CnOP2hP2SOR5+7qQTAfBgNVHSMEGDAWgBSpESzlo9SX2CnO 14 | P2hP2SOR5+7qQTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBB 15 | 2JXqsgnT+eX0m0rcuDzKxS3eRZIGuQxr9wja6hN44nhOg4Jmzv2JD9rRISVtTM1z 16 | KPeofWNDdKlzEm8k50STmJED8AXLDnN2WFUlFmWVWmlGKkYIK5h027sIvWb+VzU9 17 | +bVWwMC7XCO8P0DF56k4XpBZqi2OJjHneG86QkxUQS0h4GEwrRypRmM/l1EJl3Ll 18 | Jr3wPHRON37vuE2bRct5fE0ELZbzx5oGF5N0LtiqwntocrNhqsPKQDD5rxjsvc4a 19 | Ne+Vh2v7z4ULzbGSZZZtsS76jyihg6V4UjvsVC1IHEMc4VhiEnm7LQXVymkiFwIR 20 | zDlD5E9ZWusGi1PuJbd+ 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/jCCAeYCAQEwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV 3 | BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 4 | ZDAeFw0yMDExMDcwMjQwNDBaFw0zMDExMDUwMjQwNDBaMEUxCzAJBgNVBAYTAkFV 5 | MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz 6 | IFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJqrHDPSiA 7 | MQMOeBE0/Du0RKoPk90zjxC28G/5gRdsxrPsonvOeRvxpp+AfN8TrmeZ+c7wErbF 8 | GOOeWQLpiY9XqVdci4LdYZmt/UpFqq03r1Duk8HwDWk5yN0N+5UQ9Cr3WQq0OnDG 9 | 4A2FPxzQfZUccNPbZqed5dtMSumFvuMY7ScqvH2JVSIYiklvNnlKVCCPPUQNcBVX 10 | rL1ksVg3nQNLxKUknSIQEZ18Wa/CbKC4VAOQSBdV7lHS7z2gLZLnTGURzJkgoPQE 11 | wN1mxmiX5c2pXWi2MsH5fNztd61WV1BxZfnhYx7RsDGJICpH6TczUpBNwtlwQcVH 12 | 0tg4NyTH71SFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAM32AXKXhmQnWAJ8hc6 13 | 1I4/0JGvNyKF/sT3LU27sitk/NQwAlsm1JUz728ulJGqCdbOy1KlfuDTpXjYInr5 14 | /JFhVb2AJgaRDSJ7k7+z3673KGxRw32XQvR6xUNWrntUMbLE1wkcmiekvGFLXZ3M 15 | e22FjvOT8qg3Lw1gk6zplKkY2STMi9aCvkJv4zMKtNGpioh+NJLl3qhP1LUlVwF4 16 | MVzIs+dJa/2eSfVpVaJhlVyNjQQbI++Dzu6+P3C0YZOtd8yXIaqiMRdlu+XMcRFx 17 | moFNGr2TYAfzGizNIZkxPxMNg4/stK3Cz2BxGMwrAntaiZnNV+PjwoEkin+GvpNz 18 | Tug= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAyaqxwz0ogDEDDngRNPw7tESqD5PdM48QtvBv+YEXbMaz7KJ7 3 | znkb8aafgHzfE65nmfnO8BK2xRjjnlkC6YmPV6lXXIuC3WGZrf1KRaqtN69Q7pPB 4 | 8A1pOcjdDfuVEPQq91kKtDpwxuANhT8c0H2VHHDT22anneXbTErphb7jGO0nKrx9 5 | iVUiGIpJbzZ5SlQgjz1EDXAVV6y9ZLFYN50DS8SlJJ0iEBGdfFmvwmyguFQDkEgX 6 | Ve5R0u89oC2S50xlEcyZIKD0BMDdZsZol+XNqV1otjLB+Xzc7XetVldQcWX54WMe 7 | 0bAxiSAqR+k3M1KQTcLZcEHFR9LYODckx+9UhQIDAQABAoIBAG/xg6mH2mKGUlcG 8 | yS5rUUz4zJnHD4PeR331K4MJaJJmHlwdubHcQmm6f1GYf0/ygOnU5E0ZZkE0Vq2A 9 | ZYBzbpFPnAHwI112HxOYxVC0tGBzve4hMQdqul1sJWDspUt67hrNX4a55f5PQtIT 10 | PZByFYJrEv74FdUtUbUg5E3sry0XA0wohScae+X9EypCll3hT8fFy0edk3zt6Wrq 11 | IYRs1/DXWQsGP4a7Z0sg9jAYW+Bx+rVXnuCFSZYaRY/i734BuiH3BJKuMhW3GMoG 12 | uQ3XlWbgWOu8l9p4XC7wZuS6hH5oqRL8uIaA7IY+Y1szfTXDALUSP09jxo95cMR6 13 | B6FTQ2UCgYEA6ZSv7bx8U/Oq6hRvqdPQJhJ/Wg4xkhZyK7clbicB9oe8ZCM6dEhg 14 | 7O0QSTOBLZ8oALhkVEkX8C2PVnATEY/jEB9/fciu8FMu4sr8UBUM3Hd+dfu1tfi5 15 | 5b9YVg7eWVXPlk9Tl8ftY0q1At5WVQyzC6cOw+fSzxE9vs8xgElWQhMCgYEA3QXY 16 | /DOBbvqfV2voDlJIvj/uTN8WA46uuhCL7rD6Gnuuusy2FbeRoMk2tw+8Vj92HfDO 17 | NLmsNTC62E2w09na81+My49SRL6Z/ZWX3zYwSLYI+YaQz0tKCzD9Sjl+fN47BEx5 18 | En8QpicXaLukAjW1gW1+JIKiY4Kzsc3/bZifIgcCgYAytrKfxj2kKJJiMj+wOqnF 19 | qlx2HADNPAxby27YBKYbdYsEntVxK8nHhwzzJ7iTRCv2RBKcbiZBYlLtrHWnaXse 20 | JAiVMb4xtY3HddTkOj3JnDQbv6PLN459AFdYj+/cq5Hfi6eVm7XByhWU4tsRqikx 21 | jXraM/oENTUXuXqA5OtSewKBgQDHv8orm+zlPJGXM4lksA7YCfU2+gLuMhxNQjkE 22 | /mL6Xj86yVniZKWzV0YgyZLfq7j3NDNYSVmONL0YUZZ20BPmEbuwGJY0VMHdAT5h 23 | V5rpi8KuqGPlRFjlpl+zniRne3yA1RAynC2SzA+G8tY1vQ6Nq0PugTV7k5sNHWw0 24 | Q1PGLwKBgQCeIY1YeN8u1yJ1A4U5wjQYLXyzLfGGABvOwOJjUsDVjM1GKAgIBiW6 25 | e6c+tAfT3zCit9KJmY4/RvmjQwPFQrnjRf+C6fcWq/2Z3C2oYpArCkPqGfCGnKmj 26 | jy6gjrKloE4JFtq3/012EFaDGJM/B7vVjvH//cJJyN2hYbxr1+7qhQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/greeter.proto: -------------------------------------------------------------------------------- 1 | // copy from: https://grpc.io/docs/what-is-grpc/introduction/ 2 | 3 | syntax = "proto3"; 4 | 5 | // The greeter service definition. 6 | service Greeter { 7 | // Sends a greeting 8 | rpc SayHello (HelloRequest) returns (HelloReply) {} 9 | } 10 | 11 | // The request message containing the user's name. 12 | message HelloRequest { 13 | string name = 1; 14 | } 15 | 16 | // The response message containing the greetings 17 | message HelloReply { 18 | string message = 1; 19 | } 20 | -------------------------------------------------------------------------------- /test/greeter_svr.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(greeter_svr). 18 | 19 | -behavior(greeter_bhvr). 20 | 21 | -compile(export_all). 22 | -compile(nowarn_export_all). 23 | 24 | %%-------------------------------------------------------------------- 25 | %% Callbacks 26 | 27 | say_hello(_Req = #{name := Name}, _Md) -> 28 | {ok, #{message => <<"Hi, ", Name/binary, "~">>}, _Md}. 29 | -------------------------------------------------------------------------------- /test/grpc_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | -define(SERVER_NAME, server). 26 | -define(CHANN_NAME, channel). 27 | 28 | -define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)). 29 | 30 | %%-------------------------------------------------------------------- 31 | %% Setups 32 | %%-------------------------------------------------------------------- 33 | 34 | all() -> 35 | [{group, http}, {group, https}]. 36 | 37 | groups() -> 38 | Tests = [t_say_hello, t_get_feature, t_list_features, t_record_route, t_record_chat], 39 | [{http, Tests}, {https,Tests}]. 40 | 41 | init_per_group(GrpName, Cfg) -> 42 | _ = application:ensure_all_started(grpc), 43 | DataDir = proplists:get_value(data_dir, Cfg), 44 | TestDir = re:replace(DataDir, "/grpc_SUITE_data", "", [{return, list}]), 45 | CA = filename:join([TestDir, "certs", "ca.pem"]), 46 | Cert = filename:join([TestDir, "certs", "cert.pem"]), 47 | Key = filename:join([TestDir, "certs", "key.pem"]), 48 | 49 | Services = #{protos => [grpc_greeter_pb, grpc_route_guide_pb], 50 | services => #{'Greeter' => greeter_svr, 51 | 'routeguide.RouteGuide' => route_guide_svr} 52 | }, 53 | Options = case GrpName of 54 | https -> 55 | [{ssl_options, [{cacertfile, CA}, 56 | {certfile, Cert}, 57 | {keyfile, Key}]}]; 58 | _ -> [] 59 | end, 60 | ClientOps = case GrpName of 61 | https -> 62 | #{gun_opts => 63 | #{transport => ssl, 64 | tls_opts => [{cacertfile, CA}, {verify, verify_none}]}}; 65 | _ -> #{} 66 | end, 67 | SvrAddr = case GrpName of 68 | https -> "https://127.0.0.1:10000"; 69 | _ -> "http://127.0.0.1:10000" 70 | end, 71 | 72 | {ok, _} = grpc:start_server(?SERVER_NAME, 10000, Services, Options), 73 | {ok, _} = grpc_client_sup:create_channel_pool(?CHANN_NAME, SvrAddr, ClientOps), 74 | Cfg. 75 | 76 | end_per_group(_GrpName, _Cfg) -> 77 | _ = grpc_client_sup:stop_channel_pool(?CHANN_NAME), 78 | _ = grpc:stop_server(?SERVER_NAME), 79 | _ = application:stop(grpc). 80 | 81 | %%-------------------------------------------------------------------- 82 | %% Tests 83 | %%-------------------------------------------------------------------- 84 | 85 | t_say_hello(_) -> 86 | ?assertMatch({ok, _, _}, 87 | greeter_client:say_hello(#{name => <<"Xiao Ming">>}, #{channel => ?CHANN_NAME})). 88 | 89 | t_get_feature(_) -> 90 | Point = #{latitude => 1, 91 | longitude => 1 92 | }, 93 | ?assertMatch({ok, _, _}, 94 | routeguide_route_guide_client:get_feature(Point, #{channel => ?CHANN_NAME})). 95 | 96 | t_list_features(_) -> 97 | {ok, Stream} = routeguide_route_guide_client:list_features(#{}, #{channel => ?CHANN_NAME}), 98 | grpc_client:send(Stream, #{}, fin), 99 | ?assertMatch([#{name := <<"City1">>}, 100 | #{name := <<"City2">>}, 101 | #{name := <<"City3">>}, 102 | {eos,[{<<"grpc-status">>,<<"0">>}]}], recv_n(Stream, 4)). 103 | 104 | t_record_route(_) -> 105 | {ok, Stream} = routeguide_route_guide_client:record_route(#{}, #{channel => ?CHANN_NAME}), 106 | grpc_client:send(Stream, #{latitude => 1, longitude => 1}), 107 | grpc_client:send(Stream, #{latitude => 2, longitude => 2}), 108 | timer:sleep(100), 109 | grpc_client:send(Stream, #{latitude => 3, longitude => 3}, fin), 110 | ?assertMatch([#{point_count := 3}, 111 | {eos,[{<<"grpc-status">>,<<"0">>}]}], recv_n(Stream, 2)). 112 | 113 | t_record_chat(_) -> 114 | {ok, Stream} = routeguide_route_guide_client:route_chat(#{}, #{channel => ?CHANN_NAME}), 115 | timer:sleep(100), 116 | ?assertMatch( 117 | [ 118 | #{name := <<"City1">>}, 119 | #{name := <<"City2">>}, 120 | #{name := <<"City3">>}, 121 | {eos, [{<<"grpc-status">>, <<"0">>}]} 122 | ], 123 | recv_n(Stream, 4) 124 | ). 125 | 126 | %%-------------------------------------------------------------------- 127 | %% Helper functions 128 | %%-------------------------------------------------------------------- 129 | 130 | recv_n(Stream, N) -> 131 | recv_n(Stream, N, []). 132 | 133 | recv_n(_Stream, 0, Acc) -> 134 | Acc; 135 | recv_n(Stream, N, Acc) -> 136 | {ok, Fs} = grpc_client:recv(Stream), 137 | recv_n(Stream, N - length(Fs), Acc ++ Fs). 138 | -------------------------------------------------------------------------------- /test/grpc_performance_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_performance_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | -define(LOG(Fmt), io:format(standard_error, Fmt, [])). 26 | -define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)). 27 | 28 | -define(SERVER_NAME, server). 29 | -define(CHANN_NAME, channel). 30 | 31 | %%-------------------------------------------------------------------- 32 | %% Setups 33 | %%-------------------------------------------------------------------- 34 | 35 | all() -> 36 | [t_performance]. 37 | 38 | init_per_suite(Cfg) -> 39 | _ = application:ensure_all_started(grpc), 40 | Services = #{protos => [grpc_greeter_pb], 41 | services => #{'Greeter' => greeter_svr} 42 | }, 43 | {ok, _} = grpc:start_server(?SERVER_NAME, 10000, Services), 44 | {ok, _} = grpc_client_sup:create_channel_pool(?CHANN_NAME, "http://127.0.0.1:10000", #{}), 45 | Cfg. 46 | 47 | end_per_suite(_Cfg) -> 48 | _ = grpc_client_sup:stop_channel_pool(?CHANN_NAME), 49 | _ = grpc:stop_server(?SERVER_NAME), 50 | _ = application:stop(grpc). 51 | 52 | %%-------------------------------------------------------------------- 53 | %% Test cases 54 | %%-------------------------------------------------------------------- 55 | 56 | matrix() -> 57 | %% Procs count, Req/procs, Req size 58 | [ {1, 2, 10} 59 | , {100, 100, 1024} 60 | , {1000, 100, 1024} 61 | % , {100, 10000, 1024} %% 1000MB 62 | % 63 | % , {10000, 1, 32} %% 312KB 64 | % , {1, 1000000, 32} %% 312KB 65 | % , {100, 100, 32} %% 312KB 66 | % , {100, 100, 64} %% 624KB 67 | % , {100, 100, 128} %% 1.24MB 68 | % , {100, 100, 1024} %% 100MB 69 | % , {100, 100, 8192} %% 800MB 70 | % , {100, 100, 65536} %% 2500MB 71 | ]. 72 | 73 | t_performance(_) -> 74 | Res = [{M, shot_one_case(M)} || M <- matrix()], 75 | ?LOG("\n\n"), 76 | ?LOG("Statistics: \n"), 77 | format_result(Res). 78 | 79 | %%-------------------------------------------------------------------- 80 | %% Internal funcs 81 | 82 | shot_once_func(Size) -> 83 | Bin = chaos_bin(Size), 84 | HelloReq = #{name => Bin}, 85 | fun() -> 86 | try greeter_client:say_hello(HelloReq, #{channel => ?CHANN_NAME}) of 87 | {ok, _, _} -> ok; 88 | Err -> 89 | ?LOG("1Send request failed: ~p~n", [Err]), 90 | error 91 | catch Type:Name:_Stk -> 92 | ?LOG("Send request failed: ~p:~p:~p~n", [Type, element(1,Name), _Stk]), 93 | error 94 | end 95 | end. 96 | 97 | shot_one_case({Pcnt, Rcnt, Rsize}) -> 98 | Throughput = Pcnt * Rcnt * Rsize, 99 | RequestCnt = Pcnt * Rcnt, 100 | 101 | ShotFun = shot_once_func(Rsize), 102 | 103 | P = self(), 104 | ?LOG("\n"), 105 | ?LOG("===============================================\n"), 106 | ?LOG("-- Request: ~s, size: ~s Total: ~s\n", [format_cnt(RequestCnt), format_byte(Rsize), format_byte(Throughput)]), 107 | ?LOG("--\n"), 108 | statistics(runtime), 109 | statistics(wall_clock), 110 | [spawn(fun() -> 111 | [begin 112 | P ! {ShotFun(), I, J} 113 | end || J <- lists:seq(1, Rcnt)] 114 | end) || I <- lists:seq(1, Pcnt)], 115 | 116 | Clt = fun _F(0) -> ok; 117 | _F(X) -> 118 | receive 119 | {_, _I, _J} -> _F(X-1) 120 | after 1000 -> ok 121 | end 122 | end, 123 | Clt(Pcnt*Rcnt), 124 | Time1 = case statistics(runtime) of 125 | {_, 0} -> 1; 126 | {_, T1} -> T1 127 | end, 128 | Time2 = case statistics(wall_clock) of 129 | {_, 0} -> 1; 130 | {_, T2} -> T2 131 | end, 132 | ?LOG("-- Run time: ~s, Wall Clock time: ~s\n", [format_ts(Time1), format_ts(Time2)]), 133 | ?LOG("-- TPS: ~s/s (~s/s) \n", [format_cnt(1000*RequestCnt/Time1), format_cnt(1000*RequestCnt/Time2)]), 134 | ?LOG("-- Throughput: ~s/s (~s/s) \n", [format_byte(1000*Throughput/Time1), format_byte(1000*Throughput/Time2)]), 135 | ?LOG("===============================================\n"), 136 | {1000*RequestCnt/Time2, 1000*Throughput/Time2}. 137 | 138 | %%-------------------------------------------------------------------- 139 | %% Utils 140 | 141 | chaos_bin(S) -> 142 | iolist_to_binary([$a || _ <- lists:seq(1, S)]). 143 | 144 | format_ts(Ms) -> 145 | case Ms > 1000 of 146 | true -> 147 | lists:flatten(io_lib:format("~.2fs", [Ms/1000])); 148 | _ -> 149 | lists:flatten(io_lib:format("~wms", [Ms])) 150 | end. 151 | 152 | format_byte(Byte) -> 153 | if 154 | Byte > 1024*124 -> 155 | lists:flatten(io_lib:format("~.2fMB", [Byte/1024/1024])); 156 | Byte > 1024 -> 157 | lists:flatten(io_lib:format("~.2fKB", [Byte/1024])); 158 | true -> 159 | lists:flatten(io_lib:format("~wB", [Byte])) 160 | end. 161 | 162 | format_cnt(Cnt) -> 163 | case Cnt > 1000 of 164 | true -> 165 | lists:flatten(io_lib:format("~.2fk", [Cnt / 1000])); 166 | _ -> 167 | lists:flatten(io_lib:format("~w", [Cnt])) 168 | end. 169 | 170 | format_result([]) -> 171 | ok; 172 | format_result([{{Pcnt, Rcnt, Rsize}, {Tps, Throughput}}|Rs]) -> 173 | ?LOG("\t~w, ~w, ~w, ~w\n", [Pcnt*Rcnt, Rsize, Tps, Throughput]), 174 | format_result(Rs). 175 | -------------------------------------------------------------------------------- /test/grpc_test2_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(grpc_test2_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | -define(SERVER_NAME, server). 26 | -define(SERVER_ADDR, "http://127.0.0.1:10000"). 27 | -define(CHANN_NAME, channel). 28 | 29 | -define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)). 30 | 31 | %%-------------------------------------------------------------------- 32 | %% Setups 33 | %%-------------------------------------------------------------------- 34 | 35 | all() -> 36 | [t_deadline, t_health_check]. 37 | 38 | init_per_suite(Cfg) -> 39 | _ = application:ensure_all_started(grpc), 40 | Services = #{protos => [grpc_test_pb], services => #{'Test' => test_svr}}, 41 | [{services, Services} | Cfg]. 42 | 43 | end_per_suite(_Cfg) -> 44 | _ = application:stop(grpc). 45 | 46 | init_per_testcase(t_deadline, Cfg) -> 47 | {ok, _} = grpc:start_server(?SERVER_NAME, 10000, ?config(services, Cfg), []), 48 | {ok, _} = grpc_client_sup:create_channel_pool(?CHANN_NAME, ?SERVER_ADDR, #{}), 49 | Cfg; 50 | init_per_testcase(t_health_check, Cfg) -> 51 | %% The case will handle the channel pool creation and server start by itself 52 | Cfg. 53 | 54 | end_per_testcase(t_deadline, _Cfg) -> 55 | _ = grpc_client_sup:stop_channel_pool(?CHANN_NAME), 56 | _ = grpc:stop_server(?SERVER_NAME), 57 | ok; 58 | end_per_testcase(t_health_check, _Cfg) -> 59 | ok. 60 | 61 | %%-------------------------------------------------------------------- 62 | %% Tests 63 | %%-------------------------------------------------------------------- 64 | 65 | t_deadline(_) -> 66 | ?assertMatch({error, {deadline_exceeded, _}}, 67 | test_client:test_deadline(#{ms => 3000}, 68 | #{channel => ?CHANN_NAME, 69 | timeout => 2000} 70 | )), 71 | receive 72 | Msg -> 73 | ?assert({should_not_receive_a_garbage_msg, Msg}) 74 | after 3000 -> 75 | ok 76 | end. 77 | 78 | t_health_check(Cfg) -> 79 | Services = ?config(services, Cfg), 80 | {ok, _} = grpc:start_server(?SERVER_NAME, 10000, Services, []), 81 | {ok, _} = grpc_client_sup:create_channel_pool(?CHANN_NAME, ?SERVER_ADDR, #{}), 82 | 83 | WorkersHealthCheck = 84 | fun(Worker) -> 85 | case grpc_client:health_check(Worker, #{channel => ?CHANN_NAME}) of 86 | ok -> true; 87 | _ -> false 88 | end 89 | end, 90 | 91 | WorkersPid = [WorkerPid || {_, WorkerPid} <- grpc_client_sup:workers(?CHANN_NAME)], 92 | 93 | ?assert(lists:all(WorkersHealthCheck, WorkersPid)), 94 | 95 | 96 | grpc:stop_server(?SERVER_NAME), 97 | ?assertNot(lists:all(WorkersHealthCheck, WorkersPid)), 98 | 99 | _ = grpc_client_sup:stop_channel_pool(?CHANN_NAME), 100 | 101 | ok. 102 | -------------------------------------------------------------------------------- /test/route_guide.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015, Google Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | syntax = "proto3"; 31 | 32 | option java_multiple_files = true; 33 | option java_package = "io.grpc.examples.routeguide"; 34 | option java_outer_classname = "RouteGuideProto"; 35 | 36 | package routeguide; 37 | 38 | // Interface exported by the server. 39 | service RouteGuide { 40 | // A simple RPC. 41 | // 42 | // Obtains the feature at a given position. 43 | // 44 | // A feature with an empty name is returned if there's no feature at the given 45 | // position. 46 | rpc GetFeature(Point) returns (Feature) {} 47 | 48 | // A server-to-client streaming RPC. 49 | // 50 | // Obtains the Features available within the given Rectangle. Results are 51 | // streamed rather than returned at once (e.g. in a response message with a 52 | // repeated field), as the rectangle may cover a large area and contain a 53 | // huge number of features. 54 | rpc ListFeatures(Rectangle) returns (stream Feature) {} 55 | 56 | // A client-to-server streaming RPC. 57 | // 58 | // Accepts a stream of Points on a route being traversed, returning a 59 | // RouteSummary when traversal is completed. 60 | rpc RecordRoute(stream Point) returns (RouteSummary) {} 61 | 62 | // A Bidirectional streaming RPC. 63 | // 64 | // Accepts a stream of RouteNotes sent while a route is being traversed, 65 | // while receiving other RouteNotes (e.g. from other users). 66 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 67 | } 68 | 69 | // Points are represented as latitude-longitude pairs in the E7 representation 70 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 71 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 72 | // the range +/- 180 degrees (inclusive). 73 | message Point { 74 | int32 latitude = 1; 75 | int32 longitude = 2; 76 | } 77 | 78 | // A latitude-longitude rectangle, represented as two diagonally opposite 79 | // points "lo" and "hi". 80 | message Rectangle { 81 | // One corner of the rectangle. 82 | Point lo = 1; 83 | 84 | // The other corner of the rectangle. 85 | Point hi = 2; 86 | } 87 | 88 | // A feature names something at a given point. 89 | // 90 | // If a feature could not be named, the name is empty. 91 | message Feature { 92 | // The name of the feature. 93 | string name = 1; 94 | 95 | // The point where the feature is detected. 96 | Point location = 2; 97 | } 98 | 99 | // A RouteNote is a message sent while at a given point. 100 | message RouteNote { 101 | string name = 1; 102 | 103 | // The location from which the message is sent. 104 | Point location = 2; 105 | 106 | // The message to be sent. 107 | string message = 3; 108 | } 109 | 110 | // A RouteSummary is received in response to a RecordRoute rpc. 111 | // 112 | // It contains the number of individual points received, the number of 113 | // detected features, and the total distance covered as the cumulative sum of 114 | // the distance between each point. 115 | message RouteSummary { 116 | // The number of points received. 117 | int32 point_count = 1; 118 | 119 | // The number of known features passed while traversing the route. 120 | int32 feature_count = 2; 121 | 122 | // The distance covered in metres. 123 | int32 distance = 3; 124 | 125 | // The duration of the traversal in seconds. 126 | int32 elapsed_time = 4; 127 | } 128 | -------------------------------------------------------------------------------- /test/route_guide_svr.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(route_guide_svr). 18 | 19 | -behavior(route_guide_bhvr). 20 | 21 | -compile(export_all). 22 | -compile(nowarn_export_all). 23 | 24 | -define(LOG(Fmt, Args), io:format(standard_error, Fmt, Args)). 25 | 26 | %%-------------------------------------------------------------------- 27 | %% Callbacks 28 | 29 | get_feature(Request, _Md) -> 30 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Request]), 31 | {ok, #{}, _Md}. 32 | 33 | list_features(Stream, _Md) -> 34 | {eos, [Request], NStream} = grpc_stream:recv(Stream), 35 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Request]), 36 | 37 | grpc_stream:reply(Stream, [#{name => "City1", location => #{latitude => 1, longitude => 1}}]), 38 | grpc_stream:reply(Stream, [#{name => "City2", location => #{latitude => 2, longitude => 2}}]), 39 | grpc_stream:reply(Stream, [#{name => "City3", location => #{latitude => 3, longitude => 3}}]), 40 | {ok, NStream}. 41 | 42 | record_route(Stream, _Md) -> 43 | LoopRecv = fun _Lp(St, Acc) -> 44 | case grpc_stream:recv(St) of 45 | {more, Reqs, NSt} -> 46 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 47 | 48 | _Lp(NSt, Acc ++ Reqs); 49 | {eos, Reqs, NSt} -> 50 | ?LOG("~p: ~0p~n", [?FUNCTION_NAME, Reqs]), 51 | {NSt, Acc ++ Reqs} 52 | end 53 | end, 54 | {NStream, Points} = LoopRecv(Stream, []), 55 | grpc_stream:reply(NStream, #{point_count => length(Points)}), 56 | {ok, NStream}. 57 | 58 | route_chat(Stream, _Md) -> 59 | grpc_stream:reply(Stream, [#{name => "City1", location => #{latitude => 1, longitude => 1}}]), 60 | grpc_stream:reply(Stream, [#{name => "City2", location => #{latitude => 2, longitude => 2}}]), 61 | grpc_stream:reply(Stream, [#{name => "City3", location => #{latitude => 3, longitude => 3}}]), 62 | {ok, Stream}. 63 | -------------------------------------------------------------------------------- /test/test.proto: -------------------------------------------------------------------------------- 1 | // copy from: https://grpc.io/docs/what-is-grpc/introduction/ 2 | 3 | syntax = "proto3"; 4 | 5 | // The greeter service definition. 6 | service Test { 7 | // Sends a greeting 8 | rpc TestDeadline(DeadlineRequest) returns (TestReply) {} 9 | } 10 | 11 | // The request message containing the user's name. 12 | message DeadlineRequest { 13 | int32 ms = 1; 14 | } 15 | 16 | // The response message containing the greetings 17 | message TestReply { 18 | string message = 1; 19 | } 20 | -------------------------------------------------------------------------------- /test/test_svr.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 3 | %% 4 | %% Licensed under the Apache License, Version 2.0 (the "License"); 5 | %% you may not use this file except in compliance with the License. 6 | %% You may obtain a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, software 11 | %% distributed under the License is distributed on an "AS IS" BASIS, 12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | %% See the License for the specific language governing permissions and 14 | %% limitations under the License. 15 | %%-------------------------------------------------------------------- 16 | 17 | -module(test_svr). 18 | 19 | -behavior(test_bhvr). 20 | 21 | -compile(export_all). 22 | -compile(nowarn_export_all). 23 | 24 | %%-------------------------------------------------------------------- 25 | %% Callbacks 26 | 27 | test_deadline(_Req = #{ms := Ms}, _Md) -> 28 | timer:sleep(Ms), 29 | {ok, #{message => <<>>}, _Md}. 30 | --------------------------------------------------------------------------------