http2-client-grpc 0.3.0.0 → 0.4.0.0
raw patch · 3 files changed
+190/−47 lines, 3 filesdep +tlsPVP ok
version bump matches the API change (PVP)
Dependencies added: tls
API changes (from Hackage documentation)
+ Network.GRPC.Client: Compressed :: CompressMode
+ Network.GRPC.Client: Uncompressed :: CompressMode
+ Network.GRPC.Client: data CompressMode
+ Network.GRPC.Client.Helpers: GrpcClient :: Http2Client -> Authority -> [(ByteString, ByteString)] -> Timeout -> Compression -> GrpcClient
+ Network.GRPC.Client.Helpers: GrpcClientConfig :: !HostName -> !PortNumber -> ![(ByteString, ByteString)] -> !Timeout -> !Compression -> !(Maybe ClientParams) -> GoAwayHandler -> FallBackFrameHandler -> GrpcClientConfig
+ Network.GRPC.Client.Helpers: [_grpcClientAuthority] :: GrpcClient -> Authority
+ Network.GRPC.Client.Helpers: [_grpcClientCompression] :: GrpcClient -> Compression
+ Network.GRPC.Client.Helpers: [_grpcClientConfigCompression] :: GrpcClientConfig -> !Compression
+ Network.GRPC.Client.Helpers: [_grpcClientConfigFallbackHandler] :: GrpcClientConfig -> FallBackFrameHandler
+ Network.GRPC.Client.Helpers: [_grpcClientConfigGoAwayHandler] :: GrpcClientConfig -> GoAwayHandler
+ Network.GRPC.Client.Helpers: [_grpcClientConfigHeaders] :: GrpcClientConfig -> ![(ByteString, ByteString)]
+ Network.GRPC.Client.Helpers: [_grpcClientConfigHost] :: GrpcClientConfig -> !HostName
+ Network.GRPC.Client.Helpers: [_grpcClientConfigPort] :: GrpcClientConfig -> !PortNumber
+ Network.GRPC.Client.Helpers: [_grpcClientConfigTLS] :: GrpcClientConfig -> !(Maybe ClientParams)
+ Network.GRPC.Client.Helpers: [_grpcClientConfigTimeout] :: GrpcClientConfig -> !Timeout
+ Network.GRPC.Client.Helpers: [_grpcClientHeaders] :: GrpcClient -> [(ByteString, ByteString)]
+ Network.GRPC.Client.Helpers: [_grpcClientHttp2Client] :: GrpcClient -> Http2Client
+ Network.GRPC.Client.Helpers: [_grpcClientTimeout] :: GrpcClient -> Timeout
+ Network.GRPC.Client.Helpers: data GrpcClient
+ Network.GRPC.Client.Helpers: data GrpcClientConfig
+ Network.GRPC.Client.Helpers: grpcClientConfigSimple :: HostName -> PortNumber -> UseTlsOrNot -> GrpcClientConfig
+ Network.GRPC.Client.Helpers: rawStreamClient :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ClientStreaming) => RPC s m -> GrpcClient -> a -> (a -> IO (a, Either StreamDone (CompressMode, MethodInput s m))) -> IO (Either TooMuchConcurrency (a, (RawReply (MethodOutput s m))))
+ Network.GRPC.Client.Helpers: rawStreamServer :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ServerStreaming) => RPC s m -> GrpcClient -> a -> MethodInput s m -> (a -> HeaderList -> MethodOutput s m -> IO a) -> IO (Either TooMuchConcurrency (a, HeaderList, HeaderList))
+ Network.GRPC.Client.Helpers: rawUnary :: (Service s, HasMethod s m) => RPC s m -> GrpcClient -> MethodInput s m -> IO (Either TooMuchConcurrency (RawReply (MethodOutput s m)))
+ Network.GRPC.Client.Helpers: setupGrpcClient :: GrpcClientConfig -> IO GrpcClient
+ Network.GRPC.Client.Helpers: tlsSettings :: UseTlsOrNot -> HostName -> PortNumber -> Maybe ClientParams
+ Network.GRPC.Client.Helpers: type UseTlsOrNot = Bool
- Network.GRPC.Client: open :: (Service s, HasMethod s m) => RPC s m -> Http2Client -> Authority -> HeaderList -> Timeout -> Compression -> RPCCall a -> IO (Either TooMuchConcurrency a)
+ Network.GRPC.Client: open :: (Service s, HasMethod s m) => Http2Client -> Authority -> HeaderList -> Timeout -> Encoding -> Decoding -> RPCCall s m a -> IO (Either TooMuchConcurrency a)
- Network.GRPC.Client: singleRequest :: (Service s, HasMethod s m) => RPC s m -> Compression -> MethodInput s m -> RPCCall (RawReply (MethodOutput s m))
+ Network.GRPC.Client: singleRequest :: (Service s, HasMethod s m) => RPC s m -> MethodInput s m -> RPCCall s m (RawReply (MethodOutput s m))
- Network.GRPC.Client: streamReply :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ServerStreaming) => RPC s m -> Compression -> a -> MethodInput s m -> (a -> HeaderList -> MethodOutput s m -> IO a) -> RPCCall (a, HeaderList, HeaderList)
+ Network.GRPC.Client: streamReply :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ServerStreaming) => RPC s m -> a -> MethodInput s m -> (a -> HeaderList -> MethodOutput s m -> IO a) -> RPCCall s m (a, HeaderList, HeaderList)
- Network.GRPC.Client: streamRequest :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ClientStreaming) => RPC s m -> a -> (a -> IO (Compression, a, Either StreamDone (MethodInput s m))) -> RPCCall (a, RawReply (MethodOutput s m))
+ Network.GRPC.Client: streamRequest :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ClientStreaming) => RPC s m -> a -> (a -> IO (a, Either StreamDone (CompressMode, MethodInput s m))) -> RPCCall s m (a, RawReply (MethodOutput s m))
Files
- http2-client-grpc.cabal +3/−1
- src/Network/GRPC/Client.hs +58/−46
- src/Network/GRPC/Client/Helpers.hs +129/−0
http2-client-grpc.cabal view
@@ -1,5 +1,5 @@ name: http2-client-grpc-version: 0.3.0.0+version: 0.4.0.0 synopsis: Implement gRPC-over-HTTP2 clients. description: Uses http2-client and proto-lens to generate client code. homepage: https://github.com/lucasdicioccio/http2-client-grpc#readme@@ -16,6 +16,7 @@ library hs-source-dirs: src exposed-modules: Network.GRPC.Client+ , Network.GRPC.Client.Helpers build-depends: base >= 4.7 && < 5 , binary , bytestring@@ -26,6 +27,7 @@ , proto-lens , proto-lens-protoc , text+ , tls , zlib default-language: Haskell2010
src/Network/GRPC/Client.hs view
@@ -18,6 +18,7 @@ , singleRequest , streamReply , streamRequest+ , CompressMode(..) , StreamDone(..) -- * Errors. , InvalidState(..)@@ -37,6 +38,7 @@ import Data.Binary.Get (Decoder(..), pushChunk, pushEndOfInput) import qualified Data.ByteString.Char8 as ByteString import Data.ProtoLens.Service.Types (Service(..), HasMethod, HasMethodImpl(..), StreamingType(..))+import GHC.TypeLits (Symbol) import Network.GRPC.HTTP2.Types import Network.GRPC.HTTP2.Encoding@@ -68,15 +70,16 @@ throwOnPushPromise _ _ _ _ _ = throwIO UnallowedPushPromiseReceived -- | Wait for an RPC reply.-waitReply :: (Service s, HasMethod s m) => RPC s m -> Compression -> Http2Stream -> IncomingFlowControl -> IO (RawReply (MethodOutput s m))-waitReply rpc compress stream flowControl = do+waitReply :: (Service s, HasMethod s m) => RPC s m -> Decoding -> Http2Stream -> IncomingFlowControl -> IO (RawReply (MethodOutput s m))+waitReply rpc decoding stream flowControl = do format . fromStreamResult <$> waitStream stream flowControl throwOnPushPromise where+ decompress = _getDecodingCompression decoding format rsp = do (hdrs, dat, trls) <- rsp let res = case lookup grpcMessageH hdrs of- Nothing -> fromDecoder $ pushEndOfInput $ flip pushChunk dat $ decodeOutput rpc compress+ Nothing -> fromDecoder $ pushEndOfInput $ flip pushChunk dat $ decodeOutput rpc decompress Just errMsg -> Left $ unpack errMsg return (hdrs, trls, res)@@ -93,15 +96,17 @@ -- | Newtype helper used to uniformize all type of streaming modes when -- passing arguments to the 'open' call.-newtype RPCCall a = RPCCall {- runRPC :: Http2Client -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> IO a+newtype RPCCall s (m ::Symbol) a = RPCCall {+ runRPC :: Http2Client -> Http2Stream -> IncomingFlowControl -> OutgoingFlowControl -> Encoding -> Decoding -> IO a } +-- | Helper to get the proxy object from an RPCCall.+rpcFromCall :: RPCCall s m a -> RPC s m+rpcFromCall _ = RPC+ -- | Main handler to perform gRPC calls to a service. open :: (Service s, HasMethod s m)- => RPC s m- -- ^ A token carrying information specifying the RPC to call.- -> Http2Client+ => Http2Client -- ^ A connected HTTP2 client. -> Authority -- ^ The HTTP2-Authority portion of the URL (e.g., "dicioccio.fr:7777").@@ -109,23 +114,24 @@ -- ^ A set of HTTP2 headers (e.g., for adding authentication headers). -> Timeout -- ^ Timeout in seconds.- -> Compression- -- ^ An indication of the compression that you will be using and accepting. Compression- -- should be per message, however a bug in gRPC-Go (to be confirmed) seems- -- to turn message compression mandatory if advertised in the HTTP2- -- headers, even though the specification states that compression per- -- message is optional irrespectively of headers.- -> RPCCall a+ -> Encoding+ -- ^ Compression used for encoding.+ -> Decoding+ -- ^ Compression allowed for decoding+ -> RPCCall s m a -- ^ The actual RPC handler. -> IO (Either TooMuchConcurrency a)-open rpc conn authority extraheaders timeout compression doStuff = do+open conn authority extraheaders timeout encoding decoding call = do+ let rpc = rpcFromCall call+ let compress = _getEncodingCompression encoding+ let decompress = _getDecodingCompression decoding let request = [ (":method", "POST") , (":scheme", "http") , (":authority", authority) , (":path", path rpc) , (grpcTimeoutH, showTimeout timeout)- , (grpcEncodingH, grpcCompressionHV compression)- , (grpcAcceptEncodingH, mconcat [grpcAcceptEncodingHVdefault, ",", grpcCompressionHV compression])+ , (grpcEncodingH, grpcCompressionHV compress)+ , (grpcAcceptEncodingH, mconcat [grpcAcceptEncodingHVdefault, ",", grpcCompressionHV decompress]) , ("content-type", grpcContentTypeHV) , ("te", "trailers") ] <> extraheaders@@ -133,25 +139,22 @@ let initStream = headers stream request (setEndHeader) handler isfc osfc = do- (runRPC doStuff) conn stream isfc osfc+ (runRPC call) conn stream isfc osfc encoding decoding in StreamDefinition initStream handler -- | gRPC call for Server Streaming. streamReply :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ServerStreaming) => RPC s m- -- ^ The RPC to call.- -> Compression- -- ^ The compression to use.- -- TODO: revisit compressions.+ -- ^ RPC to call. -> a -- ^ An initial state. -> MethodInput s m -- ^ The input. -> (a -> HeaderList -> MethodOutput s m -> IO a) -- ^ A state-passing handler that is called with the message read.- -> RPCCall (a, HeaderList, HeaderList)-streamReply rpc compress v0 req handler = RPCCall $ \conn stream isfc osfc -> do+ -> RPCCall s m (a, HeaderList, HeaderList)+streamReply rpc v0 req handler = RPCCall $ \conn stream isfc osfc encoding decoding -> do let { loop v1 decode hdrs = _waitEvent stream >>= \case (StreamPushPromiseEvent _ _ _) ->@@ -164,21 +167,23 @@ _addCredit isfc (ByteString.length dat) _ <- _consumeCredit isfc (ByteString.length dat) _ <- _updateWindow isfc- handleAllChunks v1 hdrs decode dat loop+ handleAllChunks decoding v1 hdrs decode dat loop } in do let ocfc = _outgoingFlowControl conn- sendSingleMessage rpc req compress setEndStream conn ocfc stream osfc+ let decompress = _getDecodingCompression decoding+ sendSingleMessage rpc req encoding setEndStream conn ocfc stream osfc _waitEvent stream >>= \case StreamHeadersEvent _ hdrs ->- loop v0 (decodeOutput rpc compress) hdrs+ loop v0 (decodeOutput rpc decompress) hdrs _ -> throwIO (InvalidState "no headers") where- handleAllChunks v1 hdrs decode dat exitLoop =+ handleAllChunks decoding v1 hdrs decode dat exitLoop = case pushChunk decode dat of (Done unusedDat _ (Right val)) -> do v2 <- handler v1 hdrs val- handleAllChunks v2 hdrs (decodeOutput rpc compress) unusedDat exitLoop+ let decompress = _getDecodingCompression decoding+ handleAllChunks decoding v2 hdrs (decodeOutput rpc decompress) unusedDat exitLoop (Done unusedDat _ (Left err)) -> do throwIO (StreamReplyDecodingError $ "done-error: " ++ err) (Fail _ _ err) -> do@@ -188,6 +193,8 @@ data StreamDone = StreamDone +data CompressMode = Compressed | Uncompressed+ -- | gRPC call for Client Streaming. streamRequest :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ClientStreaming)@@ -195,37 +202,41 @@ -- ^ RPC to call. -> a -- ^ An initial state.- -> (a -> IO (Compression, a, Either StreamDone (MethodInput s m)))+ -> (a -> IO (a, Either StreamDone (CompressMode, MethodInput s m))) -- ^ A state-passing action to retrieve the next message to send to the server.- -> RPCCall (a, RawReply (MethodOutput s m))-streamRequest rpc v0 handler = RPCCall $ \conn stream isfc streamFlowControl ->+ -> RPCCall s m (a, RawReply (MethodOutput s m))+streamRequest rpc v0 handler = RPCCall $ \conn stream isfc streamFlowControl encoding decoding ->+ let decompress = _getDecodingCompression decoding in let ocfc = _outgoingFlowControl conn go v1 = do- (compress, v2, nextEvent) <- handler v1+ (v2, nextEvent) <- handler v1 case nextEvent of- Right msg -> do- sendSingleMessage rpc msg compress id conn ocfc stream streamFlowControl+ Right (doCompress, msg) -> do+ let compress = case doCompress of+ Compressed -> _getEncodingCompression encoding+ Uncompressed -> uncompressed+ sendSingleMessage rpc msg encoding id conn ocfc stream streamFlowControl go v2 Left _ -> do sendData conn stream setEndStream ""- reply <- waitReply rpc compress stream isfc+ reply <- waitReply rpc decoding stream isfc pure (v2, reply) in go v0 - -- | Serialize and send a single message. sendSingleMessage :: (Service s, HasMethod s m) => RPC s m -> MethodInput s m- -> Compression+ -> Encoding -> FlagSetter -> Http2Client -> OutgoingFlowControl -> Http2Stream -> OutgoingFlowControl -> IO ()-sendSingleMessage rpc msg compression flagMod conn connectionFlowControl stream streamFlowControl = do+sendSingleMessage rpc msg encoding flagMod conn connectionFlowControl stream streamFlowControl = do+ let compress = _getEncodingCompression encoding let goUpload dat = do let !wanted = ByteString.length dat gotStream <- _withdrawCredit streamFlowControl wanted@@ -237,16 +248,17 @@ else do sendData conn stream id (ByteString.take got dat) goUpload (ByteString.drop got dat)- goUpload . toStrict . toLazyByteString . encodeInput rpc compression $ msg+ goUpload . toStrict . toLazyByteString . encodeInput rpc compress $ msg -- | gRPC call for an unary request. singleRequest :: (Service s, HasMethod s m) => RPC s m- -> Compression+ -- ^ RPC to call. -> MethodInput s m- -> RPCCall (RawReply (MethodOutput s m))-singleRequest rpc compress msg = RPCCall $ \conn stream isfc osfc -> do+ -- ^ RPC's input.+ -> RPCCall s m (RawReply (MethodOutput s m))+singleRequest rpc msg = RPCCall $ \conn stream isfc osfc encoding decoding -> do let ocfc = _outgoingFlowControl conn- sendSingleMessage rpc msg compress setEndStream conn ocfc stream osfc- waitReply rpc compress stream isfc+ sendSingleMessage rpc msg encoding setEndStream conn ocfc stream osfc+ waitReply rpc decoding stream isfc
+ src/Network/GRPC/Client/Helpers.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GADTs #-}++-- | Set of helpers helping with writing gRPC clients with not much exposure of+-- the http2-client complexity.+--+-- Th+module Network.GRPC.Client.Helpers where++import Control.Exception (throwIO)+import qualified Data.ByteString.Char8 as ByteString+import Data.ByteString.Char8 (ByteString)+import Data.Default.Class (def)+import Data.ProtoLens.Service.Types (Service(..), HasMethod, HasMethodImpl(..), StreamingType(..))+import qualified Network.TLS as TLS+import qualified Network.TLS.Extra.Cipher as TLS+import Network.HPACK (HeaderList)++import Network.HTTP2+import Network.HTTP2.Client+import Network.GRPC.Client+import Network.GRPC.HTTP2.Types+import Network.GRPC.HTTP2.Encoding++-- | A simplified gRPC Client connected via an HTTP2Client to a given server.+-- Each call from one client will share similar headers, timeout, compression.+data GrpcClient = GrpcClient {+ _grpcClientHttp2Client :: Http2Client+ -- ^ Underlying HTTP2 client.+ , _grpcClientAuthority :: Authority+ -- ^ Authority header of the server the client is connected to.+ , _grpcClientHeaders :: [(ByteString, ByteString)]+ -- ^ Extra HTTP2 headers to pass to every call (e.g., authentication tokens).+ , _grpcClientTimeout :: Timeout+ -- ^ Timeout for RPCs.+ , _grpcClientCompression :: Compression+ -- ^ Compression shared for every call and expected for every answer.+ }++-- | Configuration to setup a GrpcClient.+data GrpcClientConfig = GrpcClientConfig {+ _grpcClientConfigHost :: !HostName+ -- ^ Hostname of the server.+ , _grpcClientConfigPort :: !PortNumber+ -- ^ Port of the server.+ , _grpcClientConfigHeaders :: ![(ByteString, ByteString)]+ -- ^ Extra HTTP2 headers to pass to every call (e.g., authentication tokens).+ , _grpcClientConfigTimeout :: !Timeout+ -- ^ Timeout for RPCs.+ , _grpcClientConfigCompression :: !Compression+ -- ^ Compression shared for every call and expected for every answer.+ , _grpcClientConfigTLS :: !(Maybe ClientParams)+ -- ^ TLS parameters for the session.+ , _grpcClientConfigGoAwayHandler :: GoAwayHandler+ -- ^ HTTP2 handler for GoAways.+ , _grpcClientConfigFallbackHandler :: FallBackFrameHandler+ -- ^ HTTP2 handler for unhandled frames.+ }++grpcClientConfigSimple :: HostName -> PortNumber -> UseTlsOrNot -> GrpcClientConfig+grpcClientConfigSimple host port tls =+ GrpcClientConfig host port [] (Timeout 3000) gzip (tlsSettings tls host port) throwIO ignoreFallbackHandler++type UseTlsOrNot = Bool++tlsSettings :: UseTlsOrNot -> HostName -> PortNumber -> Maybe ClientParams+tlsSettings False _ _ = Nothing+tlsSettings True host port = Just $ TLS.ClientParams {+ TLS.clientWantSessionResume = Nothing+ , TLS.clientUseMaxFragmentLength = Nothing+ , TLS.clientServerIdentification = (host, ByteString.pack $ show port)+ , TLS.clientUseServerNameIndication = True+ , TLS.clientShared = def+ , TLS.clientHooks = def { TLS.onServerCertificate = \_ _ _ _ -> return []+ }+ , TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default }+ , TLS.clientDebug = def+ }+++setupGrpcClient :: GrpcClientConfig -> IO GrpcClient+setupGrpcClient config = do+ let host = _grpcClientConfigHost config+ let port = _grpcClientConfigPort config+ let tls = _grpcClientConfigTLS config+ let compression = _grpcClientConfigCompression config+ let onGoAway = _grpcClientConfigGoAwayHandler config+ let onFallback = _grpcClientConfigFallbackHandler config+ let timeout = _grpcClientConfigTimeout config+ let headers = _grpcClientConfigHeaders config+ let authority = ByteString.pack $ host <> ":" <> show port++ conn <- newHttp2FrameConnection host port tls+ cli <- newHttp2Client conn 8192 8192 [] onGoAway onFallback+ return $ GrpcClient cli authority headers timeout compression ++rawUnary+ :: (Service s, HasMethod s m)+ => RPC s m+ -> GrpcClient+ -> MethodInput s m+ -> IO (Either TooMuchConcurrency (RawReply (MethodOutput s m)))+rawUnary rpc (GrpcClient client authority headers timeout compression) input =+ let call = singleRequest rpc input+ in open client authority headers timeout (Encoding compression) (Decoding compression) call++rawStreamServer + :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ServerStreaming)+ => RPC s m+ -> GrpcClient+ -> a+ -> MethodInput s m+ -> (a -> HeaderList -> MethodOutput s m -> IO a)+ -> IO (Either TooMuchConcurrency (a, HeaderList, HeaderList))+rawStreamServer rpc (GrpcClient client authority headers timeout compression) v0 input handler =+ let call = streamReply rpc v0 input handler+ in open client authority headers timeout (Encoding compression) (Decoding compression) call++rawStreamClient+ :: (Service s, HasMethod s m, MethodStreamingType s m ~ 'ClientStreaming)+ => RPC s m+ -> GrpcClient+ -> a+ -> (a -> IO (a, Either StreamDone (CompressMode, MethodInput s m)))+ -> IO (Either TooMuchConcurrency (a, (RawReply (MethodOutput s m))))+rawStreamClient rpc (GrpcClient client authority headers timeout compression) v0 getNext =+ let call = streamRequest rpc v0 getNext+ in open client authority headers timeout (Encoding compression) (Decoding compression) call