I'm growing a web server in Haskell that interfaces with TCP sockets using network. To read the HTTP header of the client's message, I use the following function:
import Network.Socket hiding ( recv )
import Network.Socket.ByteString ( recv )
import qualified Data.ByteString as S
import Data.ByteString.UTF8 as BSU ( fromString )
readHeader :: Socket -> IO S.ByteString
readHeader sock = go sock mempty
where
go sock prevContent = do
newContent <- recv sock maxNumberOfBytesToReceive
let content = prevContent <> newContent
-- Read the data from the socket unless it's empty which means
-- that the client has closed its socket or we see an empty line
-- which marks the end of the HTTP header
if S.null newContent || emptyLine `S.isSuffixOf` content
then return content
else go sock content
where
-- Only read one byte at a time to avoid reading further than the
-- empty line separating the HTTP header from the HTTP body
maxNumberOfBytesToReceive = 1
emptyLine = BSU.fromString "\r\n\r\n"
The function only reads a byte at a time since I want to be able to read the rest of the client's message from the socket afterwards. But I'm curious whether the function could be more efficient.
My first idea was to read more bytes (say 1024) from the socket, check if those bytes contain an empty line, save that part (the HTTP header) in a ByteString and put the bytes after the empty line back into the socket's buffer. But I'm not sure whether it's possible or wise to put data back into the socket's buffer.
I'm also interested in any other code improvements that come to your mind.
The whole project is here.
Start it with stack ghci
and then run main
. Send it an HTTP request with curl http://localhost:8080/
1 Answer 1
Perhaps you could wrap bare Socket
s in an auxiliary datatype that enabled buffering. Something like:
data BufferedSocket = BufferedSocket [ByteString] Socket
Then you could define your own recv
function like
recv :: BufferedSocket -> Int -> IO (BufferedSocket,ByteString)
which looked at the buffer before actually trying to read data from the socket. Note that this version of recv
returns a modified copy of the BufferedSocket
, because now we carry some state that isn't captured in the mutable Socket
reference.
(Perhaps this extra buffer state should be put in a separate mutable reference, an IORef
for example. We are already in mutable-land after all.)
We also need a function
putBack :: ByteString -> BufferedSocket -> BufferedSocket
for prepending the data.
Another option could consist in using a streaming library like streaming or streaming-bytestring and build a Stream
of ByteString
s out of the Socket
. Prepending would consist simply in concatenating a pure Stream
that yields the ByteString
to the effectful stream that reads from the socket, using >>
or *>
.
let socketStream' = S.yield someByteStringValue *> socketStream
Note that the old socketStream
value should not be reused!
This might have the disadvantage that you lose some control about how many bytes to "physically" read at each step, because typical Stream
s don't take "feedback" from downstream about the number of bytes to receive next.