mirror of
https://github.com/unclechu/gRPC-haskell.git
synced 2024-11-26 21:19:43 +01:00
First draft of a tutorial (#15)
* add basic tutorial, re-export client stuff in HighLevel.Generated * add a relative link in README.md * forgot to document the language extensions * cabal file fixup * more context to compile-proto-file * haskell syntax highlighting in markdown * link to gRPC official tutorials for basic concepts * add a note on how to build the examples * prominent notice of required gRPC version * fix typo * do some error handling, show how to run the example executables * use mapM
This commit is contained in:
parent
b550607f60
commit
169dbb7fff
9 changed files with 513 additions and 3 deletions
|
@ -7,6 +7,8 @@ have extended and released under the same [`LICENSE`](./LICENSE)
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
**The current version of this library requires gRPC version 1.2.0. Newer versions may work but have not been tested.**
|
||||||
|
|
||||||
Run the following command from the root of this repository to install the
|
Run the following command from the root of this repository to install the
|
||||||
`compile-proto-file` executable:
|
`compile-proto-file` executable:
|
||||||
|
|
||||||
|
@ -17,6 +19,8 @@ $ nix-env -iA grpc-haskell -f release.nix
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
There is a tutorial [here](examples/tutorial/TUTORIAL.md)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ compile-proto-file --help
|
$ compile-proto-file --help
|
||||||
Dumps a compiled .proto file to stdout
|
Dumps a compiled .proto file to stdout
|
||||||
|
|
129
examples/tutorial/Arithmetic.hs
Normal file
129
examples/tutorial/Arithmetic.hs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
|
||||||
|
-- | Generated by Haskell protocol buffer compiler. DO NOT EDIT!
|
||||||
|
module Arithmetic where
|
||||||
|
import qualified Prelude as Hs
|
||||||
|
import qualified Proto3.Suite.DotProto as HsProtobuf
|
||||||
|
import qualified Proto3.Suite.Types as HsProtobuf
|
||||||
|
import qualified Proto3.Suite.Class as HsProtobuf
|
||||||
|
import qualified Proto3.Wire as HsProtobuf
|
||||||
|
import Control.Applicative ((<*>), (<|>))
|
||||||
|
import qualified Data.Text as Hs (Text)
|
||||||
|
import qualified Data.ByteString as Hs
|
||||||
|
import qualified Data.String as Hs (fromString)
|
||||||
|
import qualified Data.Vector as Hs (Vector)
|
||||||
|
import qualified Data.Int as Hs (Int16, Int32, Int64)
|
||||||
|
import qualified Data.Word as Hs (Word16, Word32, Word64)
|
||||||
|
import GHC.Generics as Hs
|
||||||
|
import GHC.Enum as Hs
|
||||||
|
import Network.GRPC.HighLevel.Generated as HsGRPC
|
||||||
|
import Network.GRPC.HighLevel.Client as HsGRPC
|
||||||
|
import Network.GRPC.HighLevel.Server as HsGRPC hiding (serverLoop)
|
||||||
|
import Network.GRPC.HighLevel.Server.Unregistered as HsGRPC
|
||||||
|
(serverLoop)
|
||||||
|
import Network.GRPC.LowLevel.Call as HsGRPC
|
||||||
|
|
||||||
|
data Arithmetic request response = Arithmetic{arithmeticAdd ::
|
||||||
|
request 'HsGRPC.Normal TwoInts OneInt ->
|
||||||
|
Hs.IO (response 'HsGRPC.Normal OneInt),
|
||||||
|
arithmeticRunningSum ::
|
||||||
|
request 'HsGRPC.ClientStreaming OneInt OneInt ->
|
||||||
|
Hs.IO (response 'HsGRPC.ClientStreaming OneInt)}
|
||||||
|
deriving Hs.Generic
|
||||||
|
|
||||||
|
arithmeticServer ::
|
||||||
|
Arithmetic HsGRPC.ServerRequest HsGRPC.ServerResponse ->
|
||||||
|
HsGRPC.ServiceOptions -> Hs.IO ()
|
||||||
|
arithmeticServer
|
||||||
|
Arithmetic{arithmeticAdd = arithmeticAdd,
|
||||||
|
arithmeticRunningSum = arithmeticRunningSum}
|
||||||
|
(ServiceOptions serverHost serverPort useCompression
|
||||||
|
userAgentPrefix userAgentSuffix initialMetadata sslConfig logger)
|
||||||
|
= (HsGRPC.serverLoop
|
||||||
|
HsGRPC.defaultOptions{HsGRPC.optNormalHandlers =
|
||||||
|
[(HsGRPC.UnaryHandler
|
||||||
|
(HsGRPC.MethodName "/arithmetic.Arithmetic/Add")
|
||||||
|
(HsGRPC.convertGeneratedServerHandler arithmeticAdd))],
|
||||||
|
HsGRPC.optClientStreamHandlers =
|
||||||
|
[(HsGRPC.ClientStreamHandler
|
||||||
|
(HsGRPC.MethodName "/arithmetic.Arithmetic/RunningSum")
|
||||||
|
(HsGRPC.convertGeneratedServerReaderHandler
|
||||||
|
arithmeticRunningSum))],
|
||||||
|
HsGRPC.optServerStreamHandlers = [],
|
||||||
|
HsGRPC.optBiDiStreamHandlers = [], optServerHost = serverHost,
|
||||||
|
optServerPort = serverPort, optUseCompression = useCompression,
|
||||||
|
optUserAgentPrefix = userAgentPrefix,
|
||||||
|
optUserAgentSuffix = userAgentSuffix,
|
||||||
|
optInitialMetadata = initialMetadata, optSSLConfig = sslConfig,
|
||||||
|
optLogger = logger})
|
||||||
|
|
||||||
|
arithmeticClient ::
|
||||||
|
HsGRPC.Client ->
|
||||||
|
Hs.IO (Arithmetic HsGRPC.ClientRequest HsGRPC.ClientResult)
|
||||||
|
arithmeticClient client
|
||||||
|
= (Hs.pure Arithmetic) <*>
|
||||||
|
((Hs.pure (HsGRPC.clientRequest client)) <*>
|
||||||
|
(HsGRPC.clientRegisterMethod client
|
||||||
|
(HsGRPC.MethodName "/arithmetic.Arithmetic/Add")))
|
||||||
|
<*>
|
||||||
|
((Hs.pure (HsGRPC.clientRequest client)) <*>
|
||||||
|
(HsGRPC.clientRegisterMethod client
|
||||||
|
(HsGRPC.MethodName "/arithmetic.Arithmetic/RunningSum")))
|
||||||
|
|
||||||
|
data TwoInts = TwoInts{twoIntsX :: Hs.Int32, twoIntsY :: Hs.Int32}
|
||||||
|
deriving (Hs.Show, Hs.Eq, Hs.Ord, Hs.Generic)
|
||||||
|
|
||||||
|
instance HsProtobuf.Named TwoInts where
|
||||||
|
nameOf _ = (Hs.fromString "TwoInts")
|
||||||
|
|
||||||
|
instance HsProtobuf.Message TwoInts where
|
||||||
|
encodeMessage _ TwoInts{twoIntsX = twoIntsX, twoIntsY = twoIntsY}
|
||||||
|
= (Hs.mconcat
|
||||||
|
[(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 1)
|
||||||
|
twoIntsX),
|
||||||
|
(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 2)
|
||||||
|
twoIntsY)])
|
||||||
|
decodeMessage _
|
||||||
|
= (Hs.pure TwoInts) <*>
|
||||||
|
(HsProtobuf.at HsProtobuf.decodeMessageField
|
||||||
|
(HsProtobuf.FieldNumber 1))
|
||||||
|
<*>
|
||||||
|
(HsProtobuf.at HsProtobuf.decodeMessageField
|
||||||
|
(HsProtobuf.FieldNumber 2))
|
||||||
|
dotProto _
|
||||||
|
= [(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 1)
|
||||||
|
(HsProtobuf.Prim HsProtobuf.Int32)
|
||||||
|
(HsProtobuf.Single "x")
|
||||||
|
[]
|
||||||
|
Hs.Nothing),
|
||||||
|
(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 2)
|
||||||
|
(HsProtobuf.Prim HsProtobuf.Int32)
|
||||||
|
(HsProtobuf.Single "y")
|
||||||
|
[]
|
||||||
|
Hs.Nothing)]
|
||||||
|
|
||||||
|
data OneInt = OneInt{oneIntResult :: Hs.Int32}
|
||||||
|
deriving (Hs.Show, Hs.Eq, Hs.Ord, Hs.Generic)
|
||||||
|
|
||||||
|
instance HsProtobuf.Named OneInt where
|
||||||
|
nameOf _ = (Hs.fromString "OneInt")
|
||||||
|
|
||||||
|
instance HsProtobuf.Message OneInt where
|
||||||
|
encodeMessage _ OneInt{oneIntResult = oneIntResult}
|
||||||
|
= (Hs.mconcat
|
||||||
|
[(HsProtobuf.encodeMessageField (HsProtobuf.FieldNumber 1)
|
||||||
|
oneIntResult)])
|
||||||
|
decodeMessage _
|
||||||
|
= (Hs.pure OneInt) <*>
|
||||||
|
(HsProtobuf.at HsProtobuf.decodeMessageField
|
||||||
|
(HsProtobuf.FieldNumber 1))
|
||||||
|
dotProto _
|
||||||
|
= [(HsProtobuf.DotProtoField (HsProtobuf.FieldNumber 1)
|
||||||
|
(HsProtobuf.Prim HsProtobuf.Int32)
|
||||||
|
(HsProtobuf.Single "result")
|
||||||
|
[]
|
||||||
|
Hs.Nothing)]
|
39
examples/tutorial/ArithmeticClient.hs
Normal file
39
examples/tutorial/ArithmeticClient.hs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE OverloadedLists #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
|
||||||
|
import Arithmetic
|
||||||
|
import Network.GRPC.HighLevel.Generated
|
||||||
|
|
||||||
|
clientConfig :: ClientConfig
|
||||||
|
clientConfig = ClientConfig { clientServerHost = "localhost"
|
||||||
|
, clientServerPort = 50051
|
||||||
|
, clientArgs = []
|
||||||
|
, clientSSLConfig = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = withGRPCClient clientConfig $ \client -> do
|
||||||
|
(Arithmetic arithmeticAdd arithmeticRunningSum) <- arithmeticClient client
|
||||||
|
|
||||||
|
-- Request for the Add RPC
|
||||||
|
ClientNormalResponse (OneInt x) _meta1 _meta2 _status _details
|
||||||
|
<- arithmeticAdd (ClientNormalRequest (TwoInts 2 2) 1 [])
|
||||||
|
putStrLn ("2 + 2 = " ++ show x)
|
||||||
|
|
||||||
|
-- Request for the RunningSum RPC
|
||||||
|
ClientWriterResponse reply _streamMeta1 _streamMeta2 streamStatus streamDtls
|
||||||
|
<- arithmeticRunningSum $ ClientWriterRequest 1 [] $ \send -> do
|
||||||
|
eithers <- mapM send [OneInt 1, OneInt 2, OneInt 3]
|
||||||
|
:: IO [Either GRPCIOError ()]
|
||||||
|
case sequence eithers of
|
||||||
|
Left err -> error ("Error while streaming: " ++ show err)
|
||||||
|
Right _ -> return ()
|
||||||
|
|
||||||
|
case reply of
|
||||||
|
Just (OneInt y) -> print ("1 + 2 + 3 = " ++ show y)
|
||||||
|
Nothing -> putStrLn ("Client stream failed with status "
|
||||||
|
++ show streamStatus
|
||||||
|
++ " and details "
|
||||||
|
++ show streamDtls)
|
||||||
|
return ()
|
50
examples/tutorial/ArithmeticServer.hs
Normal file
50
examples/tutorial/ArithmeticServer.hs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE OverloadedLists #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
|
||||||
|
import Arithmetic
|
||||||
|
import Network.GRPC.HighLevel.Generated
|
||||||
|
|
||||||
|
import Data.String (fromString)
|
||||||
|
|
||||||
|
handlers :: Arithmetic ServerRequest ServerResponse
|
||||||
|
handlers = Arithmetic { arithmeticAdd = addHandler
|
||||||
|
, arithmeticRunningSum = runningSumHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
addHandler :: ServerRequest 'Normal TwoInts OneInt
|
||||||
|
-> IO (ServerResponse 'Normal OneInt)
|
||||||
|
addHandler (ServerNormalRequest _metadata (TwoInts x y)) = do
|
||||||
|
let answer = OneInt (x + y)
|
||||||
|
return (ServerNormalResponse answer
|
||||||
|
[("metadata_key_one", "metadata_value")]
|
||||||
|
StatusOk
|
||||||
|
"addition is easy!")
|
||||||
|
|
||||||
|
|
||||||
|
runningSumHandler :: ServerRequest 'ClientStreaming OneInt OneInt
|
||||||
|
-> IO (ServerResponse 'ClientStreaming OneInt)
|
||||||
|
runningSumHandler req@(ServerReaderRequest metadata recv) =
|
||||||
|
loop 0
|
||||||
|
where loop !i =
|
||||||
|
do msg <- recv
|
||||||
|
case msg of
|
||||||
|
Left err -> return (ServerReaderResponse
|
||||||
|
Nothing
|
||||||
|
[]
|
||||||
|
StatusUnknown
|
||||||
|
(fromString (show err)))
|
||||||
|
Right (Just (OneInt x)) -> loop (i + x)
|
||||||
|
Right Nothing -> return (ServerReaderResponse
|
||||||
|
(Just (OneInt i))
|
||||||
|
[]
|
||||||
|
StatusOk
|
||||||
|
"")
|
||||||
|
|
||||||
|
options :: ServiceOptions
|
||||||
|
options = defaultServiceOptions
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = arithmeticServer handlers options
|
216
examples/tutorial/TUTORIAL.md
Normal file
216
examples/tutorial/TUTORIAL.md
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
## Introduction to gRPC-Haskell
|
||||||
|
|
||||||
|
*This tutorial assumes that you already have a basic understanding of gRPC as well as Haskell.* For an intoduction to the concepts of gRPC, see the [official tutorials](http://www.grpc.io/docs/tutorials/).
|
||||||
|
|
||||||
|
This will go through a basic example of using the library, with the `arithmetic` example in the `examples/arithmetic` directory. After cloning this repository, it would be a good idea to run `stack haddock` from within the repository directory to generate the documentation so you can read more about the functions and types we're using as we go. Also remember that [typed holes](https://wiki.haskell.org/GHC/Typed_holes) can be very handy.
|
||||||
|
|
||||||
|
To build the examples, you can run
|
||||||
|
|
||||||
|
```
|
||||||
|
$ stack build --flag grpc-haskell:with-examples
|
||||||
|
```
|
||||||
|
|
||||||
|
The gRPC service we will be implementing provides two amazing functions:
|
||||||
|
|
||||||
|
1. `Add`, which adds two integers.
|
||||||
|
2. `RunningSum`, which receives a stream of integers from the client and finally returns a single integer that is the sum of all the integers it has received.
|
||||||
|
|
||||||
|
You can run the examples by running `stack exec arithmetic-server` and `stack exec arithmetic-client`.
|
||||||
|
|
||||||
|
### Library Organization
|
||||||
|
|
||||||
|
**tl;dr: you probably only need to import `Network.GRPC.HighLevel.Generated`.** Other modules are exposed for advanced users only.
|
||||||
|
|
||||||
|
This library exposes quite a few modules, but you won't need to worry about most of them. They are currently organized based on the level of abstraction they afford over using the C [gRPC Core library](http://www.grpc.io/grpc/core/) directly:
|
||||||
|
|
||||||
|
* *`Unsafe`* modules directly wrap functions in the gRPC Core library. Using them directly is like using C: you need to think about memory management, pointers, and so on. The rest of the library is built on top of these functions and users of gRPC-haskell should never need to deal with the `Unsafe` modules directly.
|
||||||
|
* *`LowLevel`* modules still require an understanding of the gRPC Core library, but guarantee memory and thread safety. Only advanced users with special requirements would use `LowLevel` modules directly.
|
||||||
|
* *`HighLevel`* modules give you an opinionated Haskell interface to gRPC that should cover most use cases while (hopefully) being easy to use. You should only need to import the `Network.GRPC.HighLevel.Generated` module to start using the library. If you need to import other modules, we probably forgot to re-export something and you should open an issue or PR.
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
To start out, we need to generate code for our protocol buffers and RPCs. The `compile-proto-file` command is provided as part of `grpc-haskell`. You can either use `stack install` to install the command globally, or use `stack exec` within the `grpc-haskell` directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ stack exec -- compile-proto-file --proto examples/echo/echo.proto > examples/echo/echo-hs/Echo.hs
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.proto` file compiler always names the generated module the same as the `.proto` file, capitalizing the first letter if it is not already. Since our proto file is `arithmetic.proto`, the generated code should be placed in `Arithmetic.hs`.
|
||||||
|
|
||||||
|
The important things to notice in this generated file are:
|
||||||
|
|
||||||
|
1. For each proto message type, an equivalent Haskell type with the same name has been generated.
|
||||||
|
2. The `arithmeticServer` function takes a a record containing handlers for each RPC endpoint and some options, and starts a server. So, you just need to call this function to get a server running.
|
||||||
|
3, The `arithmeticClient` function takes a `Client` (which is just a proof that the gRPC core has been started) and gives you a record of functions that can be used to run RPCs.
|
||||||
|
|
||||||
|
### The server
|
||||||
|
|
||||||
|
First, we need to turn on some language extensions:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE GADTs #-}
|
||||||
|
{-# LANGUAGE OverloadedLists #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
```
|
||||||
|
|
||||||
|
All we need to do to run a server is call the `arithmeticServer` function:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
main :: IO ()
|
||||||
|
main = arithmeticServer handlers options
|
||||||
|
```
|
||||||
|
|
||||||
|
So we just need to define `handlers` and `options`.
|
||||||
|
|
||||||
|
`options` is easy-- it's just some basic options for the server. We can just use the default options for now, which will start the server listening on `localhost:50051`:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
options :: ServiceOptions
|
||||||
|
options = defaultServiceOptions
|
||||||
|
```
|
||||||
|
|
||||||
|
`handlers` is a bit more involved. Its type is `Arithmetic ServerRequest ServerResponse`. Values of this type contain a record field for each RPC defined in your `.proto` file.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
handlers :: Arithmetic ServerRequest ServerResponse
|
||||||
|
handlers = Arithmetic { arithmeticAdd = addHandler
|
||||||
|
, arithmeticRunningSum = runningSumHandler
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can think of the handlers as being of type `ServerRequest -> ServerResponse`, though there are a few more type parameters in there. The most important one is the first parameter, which specifies whether the RPC is streaming (`ClientStreaming`, `ServerStreaming`, or `BidiStreaming`) or not (`Normal`).
|
||||||
|
|
||||||
|
The `ServerRequest` passed to your handler contains all the tools you will need to handle the request, including:
|
||||||
|
|
||||||
|
1. The metadata the client sent with the request.
|
||||||
|
2. The protocol buffer message sent with the request, which has already been parsed into a Haskell type for you.
|
||||||
|
3. If it's a streaming request, you will also be given functions for sending or receiving messages in the stream.
|
||||||
|
|
||||||
|
#### The unary RPC handler for `Add`
|
||||||
|
|
||||||
|
So, let's pattern match on the `ServerRequest` for the `addHandler` function:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
addHandler (ServerNormalRequest metadata (TwoInts x y)) = -- to be continued!
|
||||||
|
```
|
||||||
|
|
||||||
|
The body of the `addHandler` function just needs to add `x` and `y` and then bundle the answer up in a `ServerResponse`:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
addHandler (ServerNormalRequest _metadata (TwoInts x y)) = do
|
||||||
|
let answer = OneInt (x + y)
|
||||||
|
return (ServerNormalResponse answer
|
||||||
|
[("metadata_key_one", "metadata_value")]
|
||||||
|
StatusOk
|
||||||
|
"addition is easy!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Since this is a non-streaming "Normal" RPC, we use the the `ServerNormalResponse` constructor. Its parameters are the response message, some (optional) metadata key-value pairs, a status code, and a string with additional details about the status, which would normally be used to explain any errors in handling the request.
|
||||||
|
|
||||||
|
#### The client streaming handler for `RunningSum`
|
||||||
|
|
||||||
|
Now let's make our `runningSumHandler`. Since this is an RPC where the server reads from a stream of numbers, we pattern match on the `ServerReaderRequest` constructor:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
runningSumHandler req@(ServerReaderRequest metadata recv) = -- to be continued!
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike the unary "Normal" request handler, we don't get a message from the client in this pattern match. Instead, we get an IO action `recv`, which we can run to wait for the client to send us another message.
|
||||||
|
|
||||||
|
There are three possibilities when we try to receive another message from the client:
|
||||||
|
|
||||||
|
1. The RPC breaks with some gRPC error, such as losing the connection with the client.
|
||||||
|
2. We receive another message from the client.
|
||||||
|
3. The client has sent its last message and is waiting for a response.
|
||||||
|
|
||||||
|
We write a simple loop that keeps track of the running sum and finally sends off a `ServerReaderResponse` when the client finishes streaming or an error occurs:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
runningSumHandler req@(ServerReaderRequest metadata recv) =
|
||||||
|
loop 0
|
||||||
|
where loop !i =
|
||||||
|
do msg <- recv
|
||||||
|
case msg of
|
||||||
|
Left err -> return (ServerReaderResponse
|
||||||
|
Nothing
|
||||||
|
[]
|
||||||
|
StatusUnknown
|
||||||
|
(fromString (show err)))
|
||||||
|
Right (Just (OneInt x)) -> loop (i + x)
|
||||||
|
Right Nothing -> return (ServerReaderResponse
|
||||||
|
(Just (OneInt i))
|
||||||
|
[]
|
||||||
|
StatusOk
|
||||||
|
"")
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ServerReaderResponse` type is almost the same as `ServerNormalResponse`, except that the first argument, the message to send back to the client, is optional. Otherwise, it takes metadata (which we leave empty), a status code, and a string containing more information about the status code.
|
||||||
|
|
||||||
|
### The client
|
||||||
|
|
||||||
|
The client-side code generated for us is `arithmeticClient`, which takes a `Client` as input and gives us a record containing actions that execute RPCs. To start up the C gRPC library and get a `Client`, we use `withGRPCClient`, which takes a `ClientConfig`:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
clientConfig :: ClientConfig
|
||||||
|
clientConfig = ClientConfig { clientServerHost = "localhost"
|
||||||
|
, clientServerPort = 50051
|
||||||
|
, clientArgs = []
|
||||||
|
, clientSSLConfig = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = withGRPCClient clientConfig $ \client -> do
|
||||||
|
(Arithmetic arithmeticAdd arithmeticRunningSum) <- arithmeticClient client
|
||||||
|
-- to be continued!
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we are on the client side, the `Arithmetic` record contains functions that make RPC requests. You can think of these functions as roughly having the type `ClientRequest -> ClientResult`. Like before, the particular constructors will vary depending on whether the RPC is streaming or not.
|
||||||
|
|
||||||
|
#### Requesting unary RPC
|
||||||
|
|
||||||
|
Here we construct a `ClientNormalRequest`, which takes as input a message, a timeout in seconds, and metadata. The result is a `ClientNormalResponse`, containing the server's response, the initial and trailing metadata for the call, and the status and status details string.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
-- Request for the Add RPC
|
||||||
|
ClientNormalResponse (OneInt x) _meta1 _meta2 _status _details
|
||||||
|
<- arithmeticAdd (ClientNormalRequest (TwoInts 2 2) 1 [])
|
||||||
|
print ("2 + 2 = " ++ (show x))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Executing a client streaming RPC
|
||||||
|
|
||||||
|
Doing a streaming request is slightly trickier. As input to the streaming RPC action, we pass in another IO action that tells `grpc-haskell` what to send. It takes a `send` action as input. This is a bit convoluted, but it guarantees that you can't send streaming messages outside of the context of a streaming call!
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
-- Request for the RunningSum RPC
|
||||||
|
ClientWriterResponse reply _streamMeta1 _streamMeta2 streamStatus streamDtls
|
||||||
|
<- arithmeticRunningSum $ ClientWriterRequest 1 [] $ \send -> do
|
||||||
|
eithers <- mapM send [OneInt 1, OneInt 2, OneInt 3]
|
||||||
|
:: IO [Either GRPCIOError ()]
|
||||||
|
case sequence eithers of
|
||||||
|
Left err -> error ("Error while streaming: " ++ show err)
|
||||||
|
Right _ -> return ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `send` potentially returns an error message, with the type `Either GRPCIOError ()`. We use `sequence` to run all the `send` actions, and then use `sequence` again to collapse all the `Either`s. If an error is encountered while streaming, there's nothing we can do to salvage the RPC, so a more serious program would need to do some application-specific error-handling. Since this is just a tutorial, we print an error message and exit. Otherwise, we `return ()` to finish sending.
|
||||||
|
|
||||||
|
We can now inspect the `reply` to get our answer to the RPC.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
case reply of
|
||||||
|
Just (OneInt y) -> print ("1 + 2 + 3 = " ++ show y)
|
||||||
|
Nothing -> putStrLn ("Client stream failed with status "
|
||||||
|
++ show streamStatus
|
||||||
|
++ " and details "
|
||||||
|
++ show streamDtls)
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the examples and see the requests, start the `arithmetic-server` process in the background, and then run the `arithmetic-client` process:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ stack exec -- arithmetic-server &
|
||||||
|
$ stack exec -- arithmetic-client
|
||||||
|
```
|
17
examples/tutorial/arithmetic.proto
Normal file
17
examples/tutorial/arithmetic.proto
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package arithmetic;
|
||||||
|
|
||||||
|
service Arithmetic {
|
||||||
|
rpc Add (TwoInts) returns (OneInt) {}
|
||||||
|
|
||||||
|
rpc RunningSum (stream OneInt) returns (OneInt) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message TwoInts {
|
||||||
|
int32 x = 1;
|
||||||
|
int32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OneInt {
|
||||||
|
int32 result = 1;
|
||||||
|
}
|
|
@ -170,6 +170,51 @@ executable echo-server
|
||||||
hs-source-dirs: examples/echo/echo-hs
|
hs-source-dirs: examples/echo/echo-hs
|
||||||
main-is: EchoServer.hs
|
main-is: EchoServer.hs
|
||||||
|
|
||||||
|
|
||||||
|
executable arithmetic-server
|
||||||
|
if flag(with-examples)
|
||||||
|
build-depends:
|
||||||
|
base >=4.8 && <5.0
|
||||||
|
, async
|
||||||
|
, bytestring == 0.10.*
|
||||||
|
, containers ==0.5.*
|
||||||
|
, grpc-haskell
|
||||||
|
, optparse-generic
|
||||||
|
, proto3-suite
|
||||||
|
, proto3-wire
|
||||||
|
, text
|
||||||
|
, vector
|
||||||
|
other-modules:
|
||||||
|
Arithmetic
|
||||||
|
else
|
||||||
|
buildable: False
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -g -threaded -rtsopts -with-rtsopts=-N -O2
|
||||||
|
hs-source-dirs: examples/tutorial/
|
||||||
|
main-is: ArithmeticServer.hs
|
||||||
|
|
||||||
|
executable arithmetic-client
|
||||||
|
if flag(with-examples)
|
||||||
|
build-depends:
|
||||||
|
base >=4.8 && <5.0
|
||||||
|
, async
|
||||||
|
, bytestring == 0.10.*
|
||||||
|
, containers ==0.5.*
|
||||||
|
, grpc-haskell
|
||||||
|
, optparse-generic
|
||||||
|
, proto3-suite
|
||||||
|
, proto3-wire
|
||||||
|
, text
|
||||||
|
, vector
|
||||||
|
other-modules:
|
||||||
|
Arithmetic
|
||||||
|
else
|
||||||
|
buildable: False
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -g -threaded -rtsopts -with-rtsopts=-N -O2
|
||||||
|
hs-source-dirs: examples/tutorial/
|
||||||
|
main-is: ArithmeticClient.hs
|
||||||
|
|
||||||
executable echo-client
|
executable echo-client
|
||||||
if flag(with-examples)
|
if flag(with-examples)
|
||||||
build-depends:
|
build-depends:
|
||||||
|
|
|
@ -28,10 +28,17 @@ module Network.GRPC.HighLevel.Generated (
|
||||||
|
|
||||||
-- * Server Auth
|
-- * Server Auth
|
||||||
, ServerSSLConfig(..)
|
, ServerSSLConfig(..)
|
||||||
|
|
||||||
|
-- * Client
|
||||||
|
, withGRPCClient
|
||||||
|
, ClientConfig(..)
|
||||||
|
, ClientRequest(..)
|
||||||
|
, ClientResult(..)
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Network.GRPC.HighLevel.Server
|
import Network.GRPC.HighLevel.Server
|
||||||
|
import Network.GRPC.HighLevel.Client
|
||||||
import Network.GRPC.LowLevel
|
import Network.GRPC.LowLevel
|
||||||
import Network.GRPC.LowLevel.Call
|
import Network.GRPC.LowLevel.Call
|
||||||
import System.IO (hPutStrLn, stderr)
|
import System.IO (hPutStrLn, stderr)
|
||||||
|
@ -84,3 +91,6 @@ defaultServiceOptions = ServiceOptions
|
||||||
, Network.GRPC.HighLevel.Generated.sslConfig = Nothing
|
, Network.GRPC.HighLevel.Generated.sslConfig = Nothing
|
||||||
, Network.GRPC.HighLevel.Generated.logger = hPutStrLn stderr
|
, Network.GRPC.HighLevel.Generated.logger = hPutStrLn stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withGRPCClient :: ClientConfig -> (Client -> IO a) -> IO a
|
||||||
|
withGRPCClient c f = withGRPC $ \grpc -> withClient grpc c $ \client -> f client
|
||||||
|
|
|
@ -54,8 +54,8 @@ data ClientSSLConfig = ClientSSLConfig
|
||||||
|
|
||||||
-- | Configuration necessary to set up a client.
|
-- | Configuration necessary to set up a client.
|
||||||
|
|
||||||
data ClientConfig = ClientConfig {serverHost :: Host,
|
data ClientConfig = ClientConfig {clientServerHost :: Host,
|
||||||
serverPort :: Port,
|
clientServerPort :: Port,
|
||||||
clientArgs :: [C.Arg],
|
clientArgs :: [C.Arg],
|
||||||
-- ^ Optional arguments for setting up the
|
-- ^ Optional arguments for setting up the
|
||||||
-- channel on the client. Supplying an empty
|
-- channel on the client. Supplying an empty
|
||||||
|
@ -69,7 +69,7 @@ data ClientConfig = ClientConfig {serverHost :: Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientEndpoint :: ClientConfig -> Endpoint
|
clientEndpoint :: ClientConfig -> Endpoint
|
||||||
clientEndpoint ClientConfig{..} = endpoint serverHost serverPort
|
clientEndpoint ClientConfig{..} = endpoint clientServerHost clientServerPort
|
||||||
|
|
||||||
addMetadataCreds :: C.ChannelCredentials
|
addMetadataCreds :: C.ChannelCredentials
|
||||||
-> Maybe C.ClientMetadataCreate
|
-> Maybe C.ClientMetadataCreate
|
||||||
|
|
Loading…
Reference in a new issue