mirror of
https://github.com/unclechu/gRPC-haskell.git
synced 2024-12-24 18:59:44 +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
|
||||
------------
|
||||
|
||||
**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
|
||||
`compile-proto-file` executable:
|
||||
|
||||
|
@ -17,6 +19,8 @@ $ nix-env -iA grpc-haskell -f release.nix
|
|||
Usage
|
||||
-----
|
||||
|
||||
There is a tutorial [here](examples/tutorial/TUTORIAL.md)
|
||||
|
||||
```bash
|
||||
$ compile-proto-file --help
|
||||
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
|
||||
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
|
||||
if flag(with-examples)
|
||||
build-depends:
|
||||
|
|
|
@ -28,10 +28,17 @@ module Network.GRPC.HighLevel.Generated (
|
|||
|
||||
-- * Server Auth
|
||||
, ServerSSLConfig(..)
|
||||
|
||||
-- * Client
|
||||
, withGRPCClient
|
||||
, ClientConfig(..)
|
||||
, ClientRequest(..)
|
||||
, ClientResult(..)
|
||||
)
|
||||
where
|
||||
|
||||
import Network.GRPC.HighLevel.Server
|
||||
import Network.GRPC.HighLevel.Client
|
||||
import Network.GRPC.LowLevel
|
||||
import Network.GRPC.LowLevel.Call
|
||||
import System.IO (hPutStrLn, stderr)
|
||||
|
@ -84,3 +91,6 @@ defaultServiceOptions = ServiceOptions
|
|||
, Network.GRPC.HighLevel.Generated.sslConfig = Nothing
|
||||
, 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.
|
||||
|
||||
data ClientConfig = ClientConfig {serverHost :: Host,
|
||||
serverPort :: Port,
|
||||
data ClientConfig = ClientConfig {clientServerHost :: Host,
|
||||
clientServerPort :: Port,
|
||||
clientArgs :: [C.Arg],
|
||||
-- ^ Optional arguments for setting up the
|
||||
-- channel on the client. Supplying an empty
|
||||
|
@ -69,7 +69,7 @@ data ClientConfig = ClientConfig {serverHost :: Host,
|
|||
}
|
||||
|
||||
clientEndpoint :: ClientConfig -> Endpoint
|
||||
clientEndpoint ClientConfig{..} = endpoint serverHost serverPort
|
||||
clientEndpoint ClientConfig{..} = endpoint clientServerHost clientServerPort
|
||||
|
||||
addMetadataCreds :: C.ChannelCredentials
|
||||
-> Maybe C.ClientMetadataCreate
|
||||
|
|
Loading…
Reference in a new issue