packages feed

quic-simple 0.1.1.0 → 0.1.2.0

raw patch · 6 files changed

+370/−47 lines, 6 filesdep +ramdep +time-hourglassdep −hourglassdep −memorydep ~quicPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: ram, time-hourglass

Dependencies removed: hourglass, memory

Dependency ranges changed: quic

API changes (from Hackage documentation)

- Network.QUIC.Simple.Stream: streamCodec :: (sendMsg -> ByteString) -> (ByteString -> IO (ByteString, Maybe recvMsg)) -> Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
- Network.QUIC.Simple.Stream: streamSerialise :: (Serialise sendMsg, Serialise recvMsg) => Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
+ Network.QUIC.Simple.Stream: acceptLoop :: Serialise header => Connection -> (header -> ByteString -> Stream -> IO ()) -> IO ()
+ Network.QUIC.Simple.Stream: codec :: (sendMsg -> ByteString) -> (ByteString -> IO (ByteString, Maybe recvMsg)) -> Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
+ Network.QUIC.Simple.Stream: codecFrom :: (sendMsg -> ByteString) -> (ByteString -> IO (ByteString, Maybe recvMsg)) -> ByteString -> Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
+ Network.QUIC.Simple.Stream: consume :: ByteString -> Stream -> (ByteString -> IO ()) -> IO ()
+ Network.QUIC.Simple.Stream: open :: Serialise header => Connection -> header -> IO Stream
+ Network.QUIC.Simple.Stream: recvMessage :: Serialise msg => Stream -> IO (msg, ByteString)
+ Network.QUIC.Simple.Stream: sendMessage :: Serialise msg => Stream -> msg -> IO ()
+ Network.QUIC.Simple.Stream: serialise :: (Serialise sendMsg, Serialise recvMsg) => Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
+ Network.QUIC.Simple.Stream: serialiseFrom :: (Serialise sendMsg, Serialise recvMsg) => ByteString -> Stream -> IO (Async (), MessageQueues sendMsg recvMsg)
- Network.QUIC.Simple: runServerStateful :: (Serialise q, Serialise r) => IP -> PortNumber -> (Connection -> TBQueue r -> IO s) -> (Connection -> s -> IO ()) -> (s -> q -> IO (s, Maybe r)) -> IO ()
+ Network.QUIC.Simple: runServerStateful :: (Serialise q, Serialise r) => IP -> PortNumber -> (Connection -> TBQueue q -> TBQueue r -> IO s) -> (Connection -> s -> IO ()) -> (s -> q -> IO (s, Maybe r)) -> IO ()

Files

CHANGELOG.md view
@@ -6,6 +6,19 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## 0.1.2.0 - 2026-09-03++- Servers and clients are datagram-enabled by default.+- Added server input queue to runServerStateful setup callback.+  * Can be used to inject datagrams as server queries.+- startClientAsync no longer inherits the caller's exception mask, so stopping a client started inside a bracket cannot deadlock on the quic watchdog thread.++- `Network.QUIC.Simple.Stream` is now meant for qualified import: `streamSerialise` and `streamCodec` became `Stream.serialise` and `Stream.codec`.+- Added `Stream.open` and `Stream.acceptLoop` for running multiple protocols over one connection, each stream announced by a header message.+- Added `Stream.sendMessage` and `Stream.recvMessage` for one-shot CBOR exchanges on bare streams, e.g. handshakes and stream headers.+- Added `Stream.serialiseFrom` and `Stream.codecFrom` to pick up a stream after a header, and `Stream.consume` for raw byte payloads.+- Stream workers now finish when the peer closes the stream instead of spinning on the end of stream.+ ## 0.1.1.0 - 2025-12-02  - Stream wrappers now use async and will close their streams on exit.
README.md view
@@ -4,8 +4,15 @@  - `QUIC.runServer [("127.0.0.1", 14443)] \conn stream ->` -- start a generic server with random TLS credentials and auto-accept the first stream. - `QUIC.runClient "127.0.0.1" "14443" \conn stream ->` -- start a generic client and request an initial stream.-- `(writeQ, readQ) <- QUIC.streamCodec encode decodeIncremental stream` -- convert a stream to a pair of queues.-- `(writeQ, readQ) <- QUIC.streamSerialise stream` -- run a CBOR codec over a stream.+- `(writeQ, readQ) <- Stream.codec encode decodeIncremental stream` -- convert a stream to a pair of queues.+- `(writeQ, readQ) <- Stream.serialise stream` -- run a CBOR codec over a stream.++Handshakes and multiple streams per connection, with `import Network.QUIC.Simple.Stream qualified as Stream`:++- `Stream.sendMessage stream hello` / `(reply, leftovers) <- Stream.recvMessage stream` -- exchange one-shot CBOR messages on a bare stream.+- `stream <- Stream.open conn header` -- request a stream and announce its protocol with a header message.+- `Stream.acceptLoop conn \header leftovers stream ->` -- accept the peer's streams and dispatch them by header, each in its own thread.+- `Stream.serialiseFrom leftovers stream` / `Stream.consume leftovers stream sink` -- continue a stream after its header with CBOR messages or raw bytes.  An extra-simple pair of wrappers for QUIC and dirty RPC: 
quic-simple.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.38.1.+-- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack  name:           quic-simple-version:        0.1.1.0+version:        0.1.2.0 synopsis:       Quick-start wrappers for QUIC description:    A few layers over QUIC, to get the first bytes out faster.                 The top level is RPC-like, using Serialise as a codec.@@ -50,13 +50,13 @@     , bytestring     , crypton     , crypton-x509-    , hourglass     , iproute-    , memory     , network-    , quic >=0.2.7+    , quic >=0.3.2+    , ram     , serialise     , stm+    , time-hourglass     , tls   default-language: GHC2021 
src/Network/QUIC/Simple.hs view
@@ -19,11 +19,10 @@  import Control.Concurrent.STM import Network.QUIC-import Network.QUIC.Simple.Stream  import Codec.Serialise (Serialise) import Control.Concurrent (threadDelay)-import Control.Concurrent.Async (Async, async, cancel, link, link2)+import Control.Concurrent.Async (Async, asyncWithUnmask, cancel, link, link2) import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar) import Control.Exception (SomeException, handle, onException) import Control.Monad (forever)@@ -33,6 +32,7 @@ import Network.QUIC.Server (ServerConfig(..), defaultServerConfig) import Network.QUIC.Server qualified as Server import Network.QUIC.Simple.Credentials (genCredentials)+import Network.QUIC.Simple.Stream qualified as Stream import Network.Socket (HostName, PortNumber, ServiceName)  {- $intro@@ -60,6 +60,9 @@     sc = defaultServerConfig       { scCredentials       , scAddresses+#if MIN_VERSION_quic(0,3,2)+      , scMaxDatagramFrameSize = 0xFFFF -- advertise support+#endif       }   Server.run sc \conn -> do     defaultStream <- acceptStream conn@@ -79,7 +82,7 @@ runServerSimple host port action =   runServerStateful host port setup teardown handler   where-    setup _conn _wq = pure ()+    setup _conn _rq _wq = pure ()     teardown _conn _s = pure ()     handler s q = do       r <- action q@@ -95,13 +98,13 @@   :: (Serialise q, Serialise r)   => IP   -> PortNumber-  -> (Connection -> TBQueue r -> IO s)+  -> (Connection -> TBQueue q -> TBQueue r -> IO s)   -> (Connection -> s -> IO ())   -> (s -> q -> IO (s, Maybe r))   -> IO () runServerStateful host port setup teardown action =   runServer [(host, port)] \conn stream0 -> do-    (codec, (writeQ, readQ)) <- streamSerialise stream0+    (codec, (writeQ, readQ)) <- Stream.serialise stream0     link codec     let       loop !s = handle (\(_ :: SomeException) -> teardown conn s) do@@ -109,7 +112,7 @@         (s', reply_) <- action s query         mapM_ (atomically . writeTBQueue writeQ) reply_         loop s'-    setup conn writeQ >>= loop+    setup conn readQ writeQ >>= loop  {- | Run a client connecting to the provided host/port and auto-request a stream. @@ -132,6 +135,9 @@       , ccSockConnected = True       , ccWatchDog = True #endif+#if MIN_VERSION_quic(0,3,2)+      , ccMaxDatagramFrameSize = 1200 -- a conservative request as datagrams must not be fragmented+#endif       }  {- | Start a client wrapper that will wait for a connection.@@ -166,11 +172,11 @@   :: (Serialise q, Serialise r)   => HostName   -> ServiceName-  -> IO (Async (), Connection, MessageQueues q r)+  -> IO (Async (), Connection, Stream.MessageQueues q r) startClientAsync host port = do   client <- newEmptyMVar-  tid <- async $ runClient host port \conn stream0 -> do-    queues <- streamSerialise stream0+  tid <- asyncWithUnmask \unmask -> unmask $ runClient host port \conn stream0 -> do+    queues <- Stream.serialise stream0     putMVar client (conn, queues)     forever (threadDelay maxBound)   (conn, (codec, queues)) <- takeMVar client `onException` cancel tid@@ -180,3 +186,4 @@     , conn     , queues     )+
src/Network/QUIC/Simple/Stream.hs view
@@ -1,20 +1,45 @@ module Network.QUIC.Simple.Stream-  ( MessageQueues-  , streamSerialise-  , streamCodec+  ( -- $intro++    -- * Message queues+    MessageQueues+  , serialise+  , serialiseFrom+  , codec+  , codecFrom+    -- * One-shot messages+  , sendMessage+  , recvMessage+    -- * Raw bytes+  , consume+    -- * Multiple streams+  , open+  , acceptLoop   ) where -import Codec.Serialise (Serialise, serialise, deserialiseIncremental)+import Codec.Serialise (Serialise, deserialiseIncremental)+import Codec.Serialise qualified as CBOR import Codec.Serialise qualified as IDecode (IDecode(..))-import Control.Concurrent.Async (Async, async, race_)+import Control.Concurrent.Async (Async, async, cancel, link, poll, race_) import Control.Concurrent.STM import Control.Exception (finally, throwIO)+import Control.Monad (filterM, forever, unless) import Control.Monad.ST (stToIO) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BSL import Data.IORef+import Data.Maybe (isNothing) import Network.QUIC qualified as QUIC +{- $intro+This module is meant to be imported qualified:++> import Network.QUIC.Simple.Stream qualified as Stream++The connection wrappers in "Network.QUIC.Simple" hand out the first stream.+The functions here run protocols over it, or open and accept more streams.+-}+ {- | A pair of bounded queues wrapping a stream. -} type MessageQueues sendMsg recvMsg = (TBQueue sendMsg, TBQueue recvMsg)@@ -25,12 +50,23 @@  No extra framing is required since CBOR is self-delimiting. -}-streamSerialise+serialise+  :: (Serialise sendMsg, Serialise recvMsg)+  => QUIC.Stream+  -> IO (Async (), MessageQueues sendMsg recvMsg)+serialise = serialiseFrom ""++{- | Same as 'serialise', but starts decoding from the bytes already received.++Use with the leftovers from 'recvMessage' when a stream starts with a header.+-}+serialiseFrom   :: forall sendMsg recvMsg   . (Serialise sendMsg, Serialise recvMsg)-  => QUIC.Stream+  => BS.ByteString+  -> QUIC.Stream   -> IO (Async (), MessageQueues sendMsg recvMsg)-streamSerialise stream = do+serialiseFrom leftovers stream = do   initial <- stToIO $ deserialiseIncremental @recvMsg   state <- newIORef initial   let@@ -39,49 +75,146 @@       case decoder of         IDecode.Fail _leftovers _offset err ->           throwIO err -- crash writer (thus the stream, and the reader/writer etc)-        IDecode.Done leftovers _consumed msg -> do+        IDecode.Done leftovers' _consumed msg -> do           stToIO deserialiseIncremental >>= writeIORef state -- restart decoder-          pure (leftovers, Just msg)-        IDecode.Partial consume -> do+          pure (leftovers', Just msg)+        IDecode.Partial consume_ -> do           -- want more data (initial state?)-          stToIO (consume $ Just chunk) >>= writeIORef state -- step decoder+          stToIO (consume_ $ Just chunk) >>= writeIORef state -- step decoder           if starting then             -- re-check if done             decode False ""           else             -- suspend and wait for next chunk             pure ("", Nothing)-  streamCodec serialise (decode True) stream+  codecFrom CBOR.serialise (decode True) leftovers stream  {- | Wrap the stream with a codec to provide a TBQueue interface to it.  The decoder loop is stateless. But it runs in IO so you can use external state and terminate the stream by erroring out.++The worker finishes when the peer closes the stream, or when either the codec or the stream fails.+Cancel it to close the stream from this side.+Messages still queued for sending at that moment are dropped. -}-streamCodec+codec   :: (sendMsg -> BSL.ByteString) -- ^ Encoder for outgoing messages   -> (BS.ByteString -> IO (BS.ByteString, Maybe recvMsg)) -- ^ Decoder for incomming chunks   -> QUIC.Stream   -> IO (Async (), MessageQueues sendMsg recvMsg)-streamCodec encode decode stream = do+codec encode decode = codecFrom encode decode ""++{- | Same as 'codec', but starts decoding from the bytes already received.+-}+codecFrom+  :: (sendMsg -> BSL.ByteString) -- ^ Encoder for outgoing messages+  -> (BS.ByteString -> IO (BS.ByteString, Maybe recvMsg)) -- ^ Decoder for incomming chunks+  -> BS.ByteString -- ^ Bytes received before the codec took over+  -> QUIC.Stream+  -> IO (Async (), MessageQueues sendMsg recvMsg)+codecFrom encode decode leftovers stream = do   readQ <- newTBQueueIO 1024   writeQ <- newTBQueueIO 1024   worker <- async $-    race_ (reader "" readQ) (writer writeQ) `finally` QUIC.closeStream stream+    race_ (reader leftovers readQ) (writer writeQ) `finally` QUIC.closeStream stream   pure (worker, (writeQ, readQ))   where-    reader leftovers readQ = do+    reader buffered readQ = do       chunk <--        if BS.null leftovers then+        if BS.null buffered then           QUIC.recvStream stream 4096         else-          pure leftovers-      (leftovers', message_) <- decode chunk-      mapM_ (atomically . writeTBQueue readQ) message_-      reader leftovers' readQ+          pure buffered+      unless (BS.null chunk) do+        (buffered', message_) <- decode chunk+        mapM_ (atomically . writeTBQueue readQ) message_+        reader buffered' readQ      writer writeQ = do       message <- atomically $ readTBQueue writeQ       let chunks = BSL.toChunks $ encode message       QUIC.sendStreamMany stream chunks       writer writeQ++{- | Send a single CBOR message to a stream.+-}+sendMessage :: Serialise msg => QUIC.Stream -> msg -> IO ()+sendMessage stream = QUIC.sendStreamMany stream . BSL.toChunks . CBOR.serialise++{- | Receive a single CBOR message from a stream.++Bytes received past the end of the message are returned along with it.+They belong to whatever comes next on the stream,+so feed them to 'serialiseFrom' or 'consume'.+In a lockstep exchange there will be none.++Throws a decoding error if the stream ends or gets garbled before the message is complete.+-}+recvMessage :: forall msg. Serialise msg => QUIC.Stream -> IO (msg, BS.ByteString)+recvMessage stream = stToIO (deserialiseIncremental @msg) >>= step+  where+    step = \case+      IDecode.Done leftovers _offset msg ->+        pure (msg, leftovers)+      IDecode.Fail _leftovers _offset err ->+        throwIO err+      IDecode.Partial consume_ -> do+        chunk <- QUIC.recvStream stream 4096+        stToIO (consume_ $ if BS.null chunk then Nothing else Just chunk) >>= step++{- | Feed the incoming bytes to a sink until the peer closes the stream.++The bytes received before, e.g. the leftovers from 'recvMessage', go first.+-}+consume :: BS.ByteString -> QUIC.Stream -> (BS.ByteString -> IO ()) -> IO ()+consume leftovers stream sink = do+  unless (BS.null leftovers) $ sink leftovers+  drain+  where+    drain = do+      chunk <- QUIC.recvStream stream 65536+      unless (BS.null chunk) do+        sink chunk+        drain++{- | Request a new bidirectional stream and announce its purpose with a header message.++The peer is expected to run 'acceptLoop' with a matching header type.+Anything sent right after the header will arrive as leftovers on the other side.+-}+open :: Serialise header => QUIC.Connection -> header -> IO QUIC.Stream+open conn header = do+  new <- QUIC.stream conn+  sendMessage new header+  pure new++{- | Accept streams opened by the peer and dispatch them by their header message.++Each stream gets a thread of its own that closes the stream when the handler returns.+The handler receives the bytes that arrived along with the header,+to be passed along to 'serialiseFrom' or 'consume'.++A crashing handler takes down the accept loop, and thus the connection.+Leaving the loop, by cancelling it or by connection failure, cancels the handlers still running.+-}+acceptLoop+  :: Serialise header+  => QUIC.Connection+  -> (header -> BS.ByteString -> QUIC.Stream -> IO ())+  -> IO ()+acceptLoop conn handler = do+  running <- newTVarIO []+  let+    cancelAll = readTVarIO running >>= mapM_ cancel+    prune = readTVarIO running >>= filterM (fmap isNothing . poll) >>= atomically . writeTVar running+    handleStream new = do+      (header, leftovers) <- recvMessage new+      handler header leftovers new+    accept = forever do+      new <- QUIC.acceptStream conn+      worker <- async $ handleStream new `finally` QUIC.closeStream new+      link worker+      prune+      atomically $ modifyTVar' running (worker :)+  accept `finally` cancelAll
test/Spec.hs view
@@ -4,17 +4,17 @@ import Control.Concurrent.STM import Control.Monad -import Control.Concurrent.Async (async, cancel, link, race_, replicateConcurrently_)-import Control.Exception (bracket)+import Control.Concurrent.Async (async, cancel, concurrently_, link, race_, replicateConcurrently_, wait)+import Control.Exception (bracket, throwIO) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BSL-import Data.IORef (newIORef, atomicModifyIORef')+import Data.IORef (newIORef, atomicModifyIORef', readIORef) import Data.IntMap.Strict qualified as IntMap import Data.Text (Text) import Data.Text.Encoding (encodeUtf8, decodeUtf8) import GHC.Generics (Generic) import Network.QUIC.Simple qualified as QUIC-import Network.QUIC.Simple.Stream (MessageQueues, streamCodec, streamSerialise)+import Network.QUIC.Simple.Stream qualified as Stream import System.Timeout (timeout)  main :: IO ()@@ -27,6 +27,10 @@   race_ serverBox clientBox   putStrLn "" +  putStrLn "Datagram"+  race_ serverDatagram clientDatagram+  putStrLn ""+   putStrLn "Serialise"   race_ serverSerialise clientSerialise   putStrLn ""@@ -39,6 +43,10 @@   race_ serverStateful clientAsync   putStrLn "" +  putStrLn "Multistream"+  race_ serverMulti clientMulti+  putStrLn ""+ -- * Raw  clientRaw :: IO ()@@ -78,10 +86,10 @@ This allows sending empty messages, but may break if the Text has a NUL in there. What kind of "text" is that anyway?! -}-cstringCodec :: QUIC.Stream -> IO (MessageQueues Text Text)+cstringCodec :: QUIC.Stream -> IO (Stream.MessageQueues Text Text) cstringCodec stream = do   previous <- newIORef BSL.empty -- gotta store partial messages somewhere-  snd <$> streamCodec encode (decode previous) stream+  snd <$> Stream.codec encode (decode previous) stream   where     -- add framing     encode msg = BSL.fromChunks [encodeUtf8 msg, "\NUL"]@@ -133,6 +141,47 @@     putStrLn $ "Server got query: " <> show query     atomically $ writeTBQueue writeQ $ "got yer bytes: " <> query +-- * Unreliable datagrams++clientDatagram :: IO ()+clientDatagram = QUIC.runClient "127.0.0.1" "14443" \conn stream -> do+  -- the initial stream comes pre-requested and ready to go+  putStrLn "Client connected:"+  QUIC.getConnectionInfo conn >>= print++  -- streams have no framing on their own+  -- but that's fine, for now...+  QUIC.sendStream stream "hi there"+  QUIC.sendDatagram conn "yo"+  yo <- QUIC.recvDatagram conn+  putStrLn $ "Client got datagram: " <> show yo+  reply <- QUIC.recvStream stream 4096+  putStrLn $ "Client got reply: " <> show reply++  QUIC.closeStream stream+  putStrLn "Client quits"++serverDatagram :: IO ()+serverDatagram = QUIC.runServer [("127.0.0.1", 14443)] \conn stream -> do+  -- the initial stream comes pre-accepted and ready to go+  putStrLn "Server accepted connection:"+  QUIC.getConnectionInfo conn >>= print++  -- wait until *something* arrives and take it all in+  query <- QUIC.recvStream stream 4096+  putStrLn $ "Server got query: " <> show query++  yo <- QUIC.recvDatagram conn+  putStrLn $ "Server got datagram: " <> show yo+  QUIC.sendDatagram conn yo++  QUIC.sendStream stream $ "got yer bytes: " <> query++  -- the final bytes will be "", signalling the connection getting closed+  finalBytes <- QUIC.recvStream stream 4096+  putStrLn $ "Server quits after " <> show finalBytes++ -- * Serialised messages  data ClientMessage@@ -159,7 +208,7 @@ clientSerialise :: IO () clientSerialise = do   QUIC.runClient "127.0.0.1" "14443" \_conn stream -> do-    (writeQ, readQ) <- snd <$> streamSerialise stream+    (writeQ, readQ) <- snd <$> Stream.serialise stream     replicateM_ 5 do       -- send messages one by one       atomically $ writeTBQueue writeQ Hello@@ -174,7 +223,7 @@ serverSerialise :: IO () serverSerialise = QUIC.runServer [("127.0.0.1", 14443)] \_conn stream -> do   putStrLn "Server accepted connection:"-  (writeQ, readQ) <- snd <$> streamSerialise stream+  (writeQ, readQ) <- snd <$> Stream.serialise stream   let     -- simple state-passing loop     loop counter = do@@ -308,7 +357,7 @@   QUIC.runServerStateful "127.0.0.1" 14443 (setup conns connIds) (teardown conns) handler   where     -- every connection has a local state too-    setup conns counter _conn writeQ = do+    setup conns counter _conn _readQ writeQ = do       -- generate an explicit key       connId <- atomicModifyIORef' counter \old -> (old + 1, old)       -- don't store ThreadIDs directly!@@ -338,3 +387,117 @@         _ ->           -- ignore casted Hellos and any form of Bye           pure (connState, Nothing)++-- * Handshake and multiple streams++{- | The first stream carries only the handshake.++Nothing else can be said before the session is established+and nothing about the handshake can be said after.+-}+data ClientHello = ClientHello { version :: Int, token :: Text }+  deriving (Show, Generic, QUIC.Serialise)++data ServerHello = Welcome Int | Rejected Text+  deriving (Show, Generic, QUIC.Serialise)++-- | Proof of a completed handshake, carrying whatever it produced.+newtype Session = Session { sessionId :: Int }++{- | Every stream opened afterwards starts with a header naming its protocol.++The rest of the stream is whatever that protocol says: CBOR messages, raw bytes, anything.+-}+data StreamHeader+  = Rpc+  | Upload Text Int+  deriving (Show, Generic, QUIC.Serialise)++data Received = Received Int+  deriving (Show, Generic, QUIC.Serialise)++clientMulti :: IO ()+clientMulti = QUIC.runClient "127.0.0.1" "14443" \conn hello -> do+  Stream.sendMessage hello ClientHello{version = 1, token = "letmein"}+  -- the handshake is lockstep, so there are no leftovers to worry about+  (reply, _) <- Stream.recvMessage hello+  session <- case reply of+    Welcome sid -> pure (Session sid)+    Rejected why -> throwIO (userError (show why))+  QUIC.closeStream hello+  putStrLn $ "Client got session " <> show (sessionId session)++  -- the streams run their protocols concurrently over one connection+  concurrently_ (rpc conn) (upload conn)+  putStrLn "Client quits"+  where+    rpc conn = do+      stream <- Stream.open conn Rpc+      -- the RPC stream is CBOR messages after the header+      (codec, (writeQ, readQ)) <- Stream.serialise stream+      link codec+      replicateM_ 3 do+        atomically $ writeTBQueue writeQ Hello+        Ok n <- atomically $ readTBQueue readQ+        putStrLn $ "Client got RPC reply " <> show n+      -- the replies are in, so the codec has nothing left to send+      cancel codec++    upload conn = do+      let+        pattern = BS.pack [0 .. 255]+        block = BS.concat (replicate 256 pattern)+        blocks = replicate 64 block+        size = sum (map BS.length blocks)+      -- the upload stream is raw bytes after the header, no framing needed+      stream <- Stream.open conn (Upload "blob" size)+      QUIC.sendStreamMany stream blocks+      -- finish sending, but keep the stream open for the reply+      QUIC.shutdownStream stream+      (Received got, _) <- Stream.recvMessage stream+      QUIC.closeStream stream+      unless (got == size) $+        throwIO (userError $ "Upload size mismatch: " <> show (got, size))+      putStrLn $ "Client uploaded " <> show got <> " bytes"++serverMulti :: IO ()+serverMulti = QUIC.runServer [("127.0.0.1", 14443)] \conn hello -> do+  (ClientHello{version, token}, _) <- Stream.recvMessage hello+  session <-+    if version == 1 && token == "letmein" then do+      Stream.sendMessage hello (Welcome 42)+      pure (Session 42)+    else do+      Stream.sendMessage hello (Rejected "bad credentials")+      throwIO (userError "handshake failed")+  QUIC.closeStream hello+  putStrLn $ "Server established session " <> show (sessionId session)++  -- from now on the peer announces what each stream is for+  Stream.acceptLoop conn \header leftovers stream -> case header of+    Rpc ->+      rpc session leftovers stream+    Upload name size ->+      upload name size leftovers stream+  where+    rpc Session{sessionId} leftovers stream = do+      -- the first message may have arrived together with the header+      (codec, (writeQ, readQ)) <- Stream.serialiseFrom leftovers stream+      counter <- newIORef sessionId+      let+        serve = forever do+          Hello <- atomically $ readTBQueue readQ+          n <- atomicModifyIORef' counter \old -> (old + 1, old)+          atomically $ writeTBQueue writeQ (Ok n)+      -- the codec finishes when the client closes the stream+      race_ (wait codec) serve+      putStrLn "Server RPC stream closed"++    upload name size leftovers stream = do+      received <- newIORef 0+      -- the bytes after the header are the payload+      Stream.consume leftovers stream \chunk ->+        atomicModifyIORef' received \old -> (old + BS.length chunk, ())+      total <- readIORef received+      putStrLn $ "Server received " <> show name <> ": " <> show total <> " of " <> show size+      Stream.sendMessage stream (Received total)