packages feed

splice-0.2: src/Network/Socket/Splice.hsc

{- |
  This library implements efficient socket to socket
  data transfer loops for proxy servers.
               
   On Linux, it uses the zero-copy splice() system call:
   <http://kerneltrap.org/node/6505>.

   On all other operating systems, it currently falls back
   to a portable Haskell implementation that allocates a
   constant-sized memory buffer before it enters an inner
   loop which then uses hGetBufSome and hPutBuf; this avoids
   lots of tiny allocations as would otherwise be caused by
   recv and sendAll functions from Network.Socket.ByteString.
-}
--  
-- Module      : Network.Socket.Splice
-- Copyright   : (c) Cetin Sert 2012
-- License     : BSD-style
-- Maintainer  : fusion@corsis.eu
-- Stability   : stable
-- Portability : GHC-only, works on all OSes

#ifdef LINUX_SPLICE
#include <fcntl.h>
{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
#endif

module Network.Socket.Splice (

  -- * Cross-platform API for Socket to Socket Data Transfer Loops
  {- | 'splice' is the cross-platform API for continous, uni-directional
       data transfer between two network sockets.

       It is an /infinite loop/ that is intended to be used with
       'Control.Concurrent.forkIO':

       > void . forkIO . try_ $ splice 1024 sourceSocket targetSocket
       > void . forkIO . try_ $ splice 1024 targetSocket sourceSocket
  -}

    splice
  , ChunkSize
  , zeroCopy

  -- * Combinators for Exception Handling
  , try_


  -- * Linux splice() Components
  {- | These are available only on Linux and
       will be moved to a different namespace
       in later releases. Their names will stay
       the same.
  -}

#ifdef LINUX_SPLICE
  , c_splice
  , sPLICE_F_MOVE
  , sPLICE_F_MORE
  , sPLICE_F_NONBLOCK
#endif

  ) where

import Data.Word
import Foreign.Ptr

import Network.Socket
import Control.Monad
import Control.Exception
import System.IO
import System.Posix.Types
import System.Posix.Internals
import GHC.IO.Handle.FD

#ifdef LINUX_SPLICE
import Data.Int
import Data.Bits
import Unsafe.Coerce
import Foreign.C.Types
import Foreign.C.Error
import System.Posix.IO
#else
import Foreign.Marshal.Alloc
#endif


-- | Indicates whether 'splice' uses zero-copy system calls
--   or the portable user mode Haskell substitue implementation.
zeroCopy :: Bool -- ^ True: uses zero-copy system calls; otherwise: portable.
zeroCopy =
#ifdef LINUX_SPLICE
  True
#else
  False
#endif


--------------------------------------------------------------------------------

-- | The numeric type used to recommend chunk sizes for moving
--   data between sockets used by both the Linux 'splice' and
--   the portable implementation of 'splice'.
type ChunkSize =
#ifdef LINUX_SPLICE
  (#type size_t)
#else
  Int
#endif


-- | Pipes data from one socket to another in an
--   **infinite loop**.
--
--   On Linux this happens in kernel space with
--   zero copying between kernel and user spaces.
--
--   On other operating systems, a portable
--   implementation utilizes a user space buffer
--   and works on handles instead of file descriptors.
splice
  :: ChunkSize -- ^ Chunk size.
  -> Socket    -- ^ Source socket.
  -> Socket    -- ^ Target socket.
  -> IO ()     -- ^ Infinite loop.
splice len sIn sOut = do

  let throwRecv0 = error "Network.Socket.Splice.splice ended"

  let fdIn  = fdSocket sIn
  let fdOut = fdSocket sOut 

#ifdef LINUX_SPLICE

  print "LINUX-SPLICE"

  (r,w) <- createPipe     -- r: read  end of pipe
  print ('+',r,w)         -- w: write end of pipe
  let s = Fd fdIn         -- s: source socket
  let t = Fd fdOut        -- t: target socket
  let n = nullPtr  
  let u = unsafeCoerce :: (#type ssize_t) -> (#type size_t)
  let check = throwErrnoIfMinus1 "Network.Socket.Splice.splice"
  let flags = sPLICE_F_MOVE .|. sPLICE_F_MORE
  let setNonBlockingMode v = do setNonBlockingFD fdIn  v
                                setNonBlockingFD fdOut v

  setNonBlockingMode False
  finally
    (forever $ do 
       bytes <- check $ c_splice s n w n    len    flags
       if bytes > 0
         then           c_splice r n t n (u bytes) flags
         else           throwRecv0)
    (do closeFd r
        closeFd w
        try_ $ setNonBlockingMode True
        print ('-',r,w))

#else

  s <- fdToHandle fdIn
  t <- fdToHandle fdOut
  hSetBuffering s NoBuffering
  hSetBuffering t NoBuffering
  a <- mallocBytes len :: IO (Ptr Word8)

  print "PORTABLE-SPLICE"

  finally
    (forever $ do
       bytes <-   hGetBufSome s a len
       if bytes > 0
         then     hPutBuf     t a bytes
         else     throwRecv0)
    (do free a
        try_ $ hClose s
        try_ $ hClose t)

#endif


-- | Similar to 'Control.Exception.Base.try' but used when an
--   obvious exception is expected whose type can be safely
--   ignored.
try_
  :: IO () -- ^ The action to run which can throw any exception.
  -> IO () -- ^ The new action where exceptions are silenced.
try_ a = (try a :: IO (Either SomeException ())) >> return ()


--------------------------------------------------------------------------------


#ifdef LINUX_SPLICE
-- SPLICE

-- fcntl.h
-- ssize_t splice(
--   int          fd_in,
--   loff_t*      off_in,
--   int          fd_out,
--   loff_t*      off_out,
--   size_t       len,
--   unsigned int flags
-- );

-- | Moves data between two file descriptors without
--   copying between kernel address space and user 
--   address space. It transfers up to 'len' bytes of
--   data from the file descriptor 'fd_in' to the file
--   file descriptor 'fd_out', where one of the
--   descriptors must refer to a pipe.
--
--   'c_splice' is NOT a loop and needs to called repeatedly.
--   For an example, see the source code of 'splice'.
foreign import ccall "splice"
  c_splice
  :: Fd                  -- ^ fd_in
  -> Ptr (#type loff_t)  -- ^ off_in
  -> Fd                  -- ^ fd_out
  -> Ptr (#type loff_t)  -- ^ off_out
  -> (#type size_t)      -- ^ len
  -> Word                -- ^ flags
  -> IO (#type ssize_t)  -- ^ number of bytes moved or -1 on error


-- | Attempt to move pages instead of copying. This is
--   only a hint to the kernel: pages may stil be copied
--   if the kernel cannot move the pages from the pipe,
--   or if the pipe buffers don't refer to full pages.
sPLICE_F_MOVE :: Word
sPLICE_F_MOVE = (#const "SPLICE_F_MOVE")


-- | More data will be coming in a subsequent splice.
--   This is a helpful hint when 'fd_out' refers to a
--   socket.
sPLICE_F_MORE :: Word
sPLICE_F_MORE = (#const "SPLICE_F_MORE")


-- | Do not block on I/O. This makes the splice pipe
--   operations nonblocking, but splice() may nevertheless
--   block because the file descriptors that are spliced
--   to/from may block (unless they have the O_NONBLOCK flag
--   set).
sPLICE_F_NONBLOCK :: Word
sPLICE_F_NONBLOCK = (#const "SPLICE_F_NONBLOCK")

#endif