6
\$\begingroup\$

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/

\$\endgroup\$
0

1 Answer 1

4
\$\begingroup\$

Perhaps you could wrap bare Sockets 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 ByteStrings 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 Streams don't take "feedback" from downstream about the number of bytes to receive next.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.