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/

AlexV
7,3532 gold badges24 silver badges47 bronze badges
asked Nov 29, 2019 at 14:57
\$\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.

answered Nov 30, 2019 at 20:03
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.