{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_HADDOCK prune not-home #-} {- | Module : Server Copyright : (c) 2022 Tim Emiola Maintainer : Tim Emiola SPDX-License-Identifier: BSD3 A demo server that responds to the demo client via a framed protocol. * once started, it waits for client connections * it loops forever, eventually it needs to be shutdown using CTRL-C * when a client connects, it responds to valid client requests, on invalid requests the connection is closed * valid requests are @'Header's@ that indicate the number of frames to send and their maximum size * it sends a series of responses that match the request specification * if the requested response size is 0, it closes the connection -} module Main (main) where import Attoparsec.ToyFrame (Header (..), asBytes, genAscFullFrames, parseHeader) import Data.Attoparsec.Framer ( Framer, Progression (..), mkFramer', runFramer, setOnBadParse, setOnClosed, ) import qualified Data.ByteString as BS import Data.Text (Text) import qualified Data.Text.IO as Text import Network.Run.TCP (runTCPServer) import Network.Socket (Socket) import Network.Socket.ByteString (recv, sendAll) main :: IO () main = runTCPServer Nothing "3927" $ \s -> do Text.putStrLn "a toy client connected" runFramer $ mkServerFramer s type ByteSink = BS.ByteString -> IO () mkServerFramer :: Socket -> Framer IO Header mkServerFramer s = let onHeader' = onHeader $ sendAll s in setOnClosed onClosed $ setOnBadParse onFailedParse $ mkFramer' parseHeader onHeader' (recv s . fromIntegral) onHeader :: ByteSink -> Header -> IO Progression onHeader sink Header {hResponseSize, hMaxPayloadSize} = do if (hResponseSize == 0) then -- hResponseSize is 0; the client means 'bye', stop waiting for input do Text.putStrLn "a toy client sent bye" pure Stop else do -- hResponseSize > 0; starting from 1, send a frame with a body whose max size is hMaxPayloadSize -- generate a list of frames counting up to the index provided in the header toSend <- genAscFullFrames hResponseSize hMaxPayloadSize mapM_ sink $ map asBytes toSend pure Continue onFailedParse :: Text -> IO () onFailedParse cause = do -- if does not parse as a frame header terminate the connection -- no explicit exception is raised here, so runFramer throws Text.putStrLn $ "parse error ended a connection from a toy client: " <> cause onClosed :: IO () onClosed = Text.putStrLn "a toy client closed a connection"