zoukankan      html  css  js  c++  java
  • [Real World Haskell翻译]第27章 网络通信和系统日志 Sockets and Syslog

    第27章 网络通信和系统日志 Sockets and Syslog
    
    基础网络
    
    在本书的前面几章,我们讨论了运转在网络上的服务。其中的两个例子是客户端/服务器架构的数据库和Web服务。当需要制定一个新的协议或者是和一个没有现成库的协议通信时,你就需要使用haskell库中较低级别的网络工具。
    在本章中,我们将讨论这些低级的工具。网络通信是整本书都在阐述的广泛的话题。我们将向您展示如何使用Haskell去应用你已经知道的底层的网络知识。
    Haskell的网络功能几乎总是直接对应于熟悉的C函数调用。由于大多数其他语言也也植根于C之上,你会发现这个接口似曾相识。
    
    UDP通信
    
    UDP将数据包从数据中解封装。它不确保数据到达其目的地只有一次。它使用校验和来确保数据包到达时没有被破坏。UDP倾向于被使用在性能或延迟敏感的应用程序中,相比于系统的整体性能来说,其中每个单独的数据包中的数据并不十分重要。它也可以使用在TCP并不是十分有效的时候,如发送短的、间隔的信息时。倾向于使用UDP的例子包括音频和视频会议,时间同步,基于网络的文件系统和日志系统。
    
    UDP客户端的例子:syslog
    
    传统的UNIX syslog服务允许程序通过网络发送日志消息给记录它们的中央服务器。有些程序对于性能非常敏感,可能会产生大量的消息。在这些程序中,更重要的是用最小的性能开销来记日志而不是保证每个消息被记录。此外,它可能需要程序继续运行即使日志服务器不可达。出于这个原因,UDP是syslog支持的传输日志消息的协议之一。该协议是简单的;这里,我们展示一个Haskell实现的客户端:
    
    -- file: ch27/syslogclient.hs
    import Data.Bits
    import Network.Socket
    import Network.BSD
    import Data.List
    import SyslogTypes
    
    data SyslogHandle = 
        SyslogHandle {slSocket :: Socket,
                      slProgram :: String,
                      slAddress :: SockAddr}
    
    openlog :: HostName -- ^ Remote hostname, or localhost
            -> String -- ^ Port number or name; 514 is default
            -> String -- ^ Name to log under
            -> IO SyslogHandle -- ^ Handle to use for logging
    openlog hostname port progname =
        do -- Look up the hostname and port. Either raises an exception
           -- or returns a nonempty list. First element in that list
           -- is supposed to be the best option.
           addrinfos <- getAddrInfo Nothing (Just hostname) (Just port)
           let serveraddr = head addrinfos
    
           -- Establish a socket for communication
           sock <- socket (addrFamily serveraddr) Datagram defaultProtocol
    
           -- Save off the socket, program name, and server address in a handle
           return $ SyslogHandle sock progname (addrAddress serveraddr)
    
    syslog :: SyslogHandle -> Facility -> Priority -> String -> IO ()
    syslog syslogh fac pri msg =
        sendstr sendmsg
        where code = makeCode fac pri
            sendmsg = "<" ++ show code ++ ">" ++ (slProgram syslogh) ++
                      ": " ++ msg
    
            -- Send until everything is done
            sendstr :: String -> IO ()
            sendstr [] = return ()
            sendstr omsg = do sent <- sendTo (slSocket syslogh) omsg
                                      (slAddress syslogh)
                              sendstr (genericDrop sent omsg)
    
    closelog :: SyslogHandle -> IO ()
    closelog syslogh = sClose (slSocket syslogh)
    
    {- | Convert a facility and a priority into a syslog code -}
    makeCode :: Facility -> Priority -> Int
    makeCode fac pri =
        let faccode = codeOfFac fac
            pricode = fromEnum pri 
            in
              (faccode `shiftL` 3) .|. pricode
    
    这里也需要SyslogTypes.hs,展示在这里:
    
    -- file: ch27/SyslogTypes.hs
    module SyslogTypes where
    {- | Priorities define how important a log message is. -}
    
    data Priority = 
                DEBUG -- ^ Debug messages
                | INFO -- ^ Information
                | NOTICE -- ^ Normal runtime conditions
                | WARNING -- ^ General Warnings
                | ERROR -- ^ General Errors
                | CRITICAL -- ^ Severe situations
                | ALERT -- ^ Take immediate action
                | EMERGENCY -- ^ System is unusable
                          deriving (Eq, Ord, Show, Read, Enum)
    
    {- | Facilities are used by the system to determine where messages
    are sent. -}
    
    data Facility = 
                  KERN -- ^ Kernel messages
                  | USER -- ^ General userland messages
                  | MAIL -- ^ E-Mail system
                  | DAEMON -- ^ Daemon (server process) messages
                  | AUTH -- ^ Authentication or security messages
                  | SYSLOG -- ^ Internal syslog messages
                  | LPR -- ^ Printer messages
                  | NEWS -- ^ Usenet news
                  | UUCP -- ^ UUCP messages
                  | CRON -- ^ Cron messages
                  | AUTHPRIV -- ^ Private authentication messages
                  | FTP -- ^ FTP messages
                  | LOCAL0 
                  | LOCAL1
                  | LOCAL2
                  | LOCAL3
                  | LOCAL4
                  | LOCAL5
                  | LOCAL6
                  | LOCAL7
                    deriving (Eq, Show, Read)
    facToCode = [
                        (KERN, 0),
                        (USER, 1),
                        (MAIL, 2),
                        (DAEMON, 3),
                        (AUTH, 4),
                        (SYSLOG, 5),
                        (LPR, 6),
                        (NEWS, 7),
                        (UUCP, 8),
                        (CRON, 9),
                        (AUTHPRIV, 10),
                        (FTP, 11),
                        (LOCAL0, 16),
                        (LOCAL1, 17),
                        (LOCAL2, 18),
                        (LOCAL3, 19),
                        (LOCAL4, 20),
                        (LOCAL5, 21),
                        (LOCAL6, 22),
                        (LOCAL7, 23)
                ]
    codeToFac = map ((x, y) -> (y, x)) facToCode
    
    
    {- | We can't use enum here because the numbering is discontiguous -}
    codeOfFac :: Facility -> Int
    codeOfFac f = case lookup f facToCode of
                    Just x -> x
                    _ -> error $ "Internal error in codeOfFac"
    
    facOfCode :: Int -> Facility
    facOfCode f = case lookup f codeToFac of
                    Just x -> x
                    _ -> error $ "Invalid code in facOfCode"
    
    用ghci您可以将消息发送到本地syslog服务器。您可以使用本章中展示的syslog服务器或者你可以找到的Linux或其他POSIX系统现存的典型的syslog服务器。需要注意的是这些UDP端口默认情况下是禁用的,在你的供应商提供的syslog守护进程显示收到的邮件之前,你可能需要启用UDP。
    如果你正在发送消息到本地系统上的syslog服务器,您可能会使用像这样的命令:
    
    ghci> :load syslogclient.hs
    [1 of 2] Compiling SyslogTypes ( SyslogTypes.hs, interpreted )
    [2 of 2] Compiling Main ( syslogclient.hs, interpreted )
    Ok, modules loaded: SyslogTypes, Main.
    ghci> h <- openlog "localhost" "514" "testprog"
    Loading package parsec-2.1.0.1 ... linking ... done.
    Loading package network-2.2.0.0 ... linking ... done.
    ghci> syslog h USER INFO "This is my message"
    ghci> closelog h
    
    UDP Syslog服务器
    
    UDP服务器将绑定到服务器上的一个特定的端口。他们将接受指向到该端口的数据包并进行处理。由于UDP是无状态的,面向数据包的协议,程序员通常使用一个调用如recvFrom来无差别地接收发送给它的数据和有关机器的信息,这被用来发送一个响应消息:
    
    -- file: ch27/syslogserver.hs
    import Data.Bits
    import Network.Socket
    import Network.BSD
    import Data.List
    
    type HandlerFunc = SockAddr -> String -> IO ()
    
    serveLog :: String -- ^ Port number or name; 514 is default
             -> HandlerFunc -- ^ Function to handle incoming messages
             -> IO ()
    serveLog port handlerfunc = withSocketsDo $
        do -- Look up the port. Either raises an exception or returns
           -- a nonempty list. 
              addrinfos <- getAddrInfo 
                           (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                           Nothing (Just port)
              let serveraddr = head addrinfos
    
              -- Create a socket
              sock <- socket (addrFamily serveraddr) Datagram defaultProtocol
    
              -- Bind it to the address we're listening to
              bindSocket sock (addrAddress serveraddr)
    
              -- Loop forever processing incoming data. Ctrl-C to abort.
              procMessages sock
        where procMessages sock =
                  do -- Receive one UDP packet, maximum length 1024 bytes,
                     -- and save its content into msg and its source
                     -- IP and port into addr
                     (msg, _, addr) <- recvFrom sock 1024
                     -- Handle it
                     handlerfunc addr msg
                     -- And process more messages
                     procMessages sock
    
    -- A simple handler that prints incoming packets
    plainHandler :: HandlerFunc
    plainHandler addr msg = 
        putStrLn $ "From " ++ show addr ++ ": " ++ msg
    
    您可以在ghci中运行它。serveLog “1514” plainHandler将会在1514端口建立一个UDP服务器,它会使用plainHandlerto打印出每个在该端口上传入的UDP包。按Ctrl-C将终止程序。
    
    %可能出现的一些问题
    %绑定错误:测试这个的时候出现permission denied,请确保您使用的端口号大于1024。有些操作系统只允许root用户绑定小于1024的端口。
    
    TCP通信
    
    TCP的目的是使数据在互联网上的传输尽可能可靠。 TCP通信是数据流。虽然这个流被操作系统拆分成单个的数据包,数据包的边界既不知道,也不和应用程序相关。 TCP保证一旦流量传递到应用程序,那么它就是完整的,未经修改的,只被传输了一次,并且是具有次序的。显然,一些事情如线缆的破坏可能会导致通信中断,并没有协议可以克服这些限制。
    与UDP相比这就需要一些取舍。首先,在TCP会话开始建立连接的时候,有些数据包必须被发送。对于很短的会话,UDP具有性能上的优势。另外,TCP非常努力地尝试使数据通过。如果会话的一端试图将数据发送到远端,但却没有收到一个确认,它会定期重发前一段时间的数据直到放弃。这使得TCP在面对丢包的时候非常的健壮。然而,这也意味着,对于涉及到音频或视频的实时协议,TCP并不是最好的选择。
    
    处理多个TCP流
    
    TCP连接是有状态的。这意味着,客户端和服务器之间有一个专门的逻辑“通道”,而非只是一次性的UDP数据包。对于客户端开发人员来说,这使得事情变得很容易。服务器应用程序几乎总是会想能够一次处理多个TCP连接。那么,如何做到这一点呢?
    在服务器端,你会首先创建一个socket并绑定到一个端口,就像使用UDP 。取代从任何位置反复监听数据,你的主循环将围绕accept调用。每一个客户端连接时,服务器的操作系统为它分配一个新的socket。因此,我们必须有主socket ,仅用于侦听传入的连接,从来不用于传输数据。我们也有一次使用多个子socket的潜力,每一个子socket对应于一个逻辑的TCP会话。
    在Haskell中,你通常会使用forkIO来创建单独的轻量级线程用于处理和每一个子socket的会话。 在这方面Haskell有一个高效的内部实现,而且表现得相当不错。
    
    TCP Syslog服务器
    
    假设我们要使用TCP而不是UDP重新实现syslog。我们可以说,一个单一的消息没有被定义在一个单一的数据包,而是被尾随的换行符'
    '所定义。任何给定的客户可以通过给定的连接发送零个或多个消息给服务器。我们可能像下面这样写:
    
    -- file: ch27/syslogtcpserver.hs
    import Data.Bits
    import Network.Socket
    import Network.BSD
    import Data.List
    import Control.Concurrent
    import Control.Concurrent.MVar
    import System.IO
    
    type HandlerFunc = SockAddr -> String -> IO ()
    
    serveLog :: String -- ^ Port number or name; 514 is default
             -> HandlerFunc -- ^ Function to handle incoming messages
             -> IO ()
    serveLog port handlerfunc = withSocketsDo $
        do -- Look up the port. Either raises an exception or returns
           -- a nonempty list. 
           addrinfos <- getAddrInfo 
                        (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                        Nothing (Just port)
           let serveraddr = head addrinfos
    
           -- Create a socket
           sock <- socket (addrFamily serveraddr) Stream defaultProtocol
    
           -- Bind it to the address we're listening to
           bindSocket sock (addrAddress serveraddr)
    
           -- Start listening for connection requests. Maximum queue size
           -- of 5 connection requests waiting to be accepted.
           listen sock 5
    
           -- Create a lock to use for synchronizing access to the handler
           lock <- newMVar ()
    
           -- Loop forever waiting for connections. Ctrl-C to abort.
           procRequests lock sock
    
        where
              -- | Process incoming connection requests
              procRequests :: MVar () -> Socket -> IO ()
              procRequests lock mastersock = 
                  do (connsock, clientaddr) <- accept mastersock
                     handle lock clientaddr
                        "syslogtcpserver.hs: client connnected"
                     forkIO $ procMessages lock connsock clientaddr
                     procRequests lock mastersock
    
              -- | Process incoming messages
              procMessages :: MVar () -> Socket -> SockAddr -> IO ()
              procMessages lock connsock clientaddr =
                  do connhdl <- socketToHandle connsock ReadMode
                     hSetBuffering connhdl LineBuffering
                     messages <- hGetContents connhdl
                     mapM_ (handle lock clientaddr) (lines messages)
                     hClose connhdl
                     handle lock clientaddr 
                        "syslogtcpserver.hs: client disconnected"
    
              -- Lock the handler before passing data to it.
              handle :: MVar () -> HandlerFunc
              -- This type is the same as
              -- handle :: MVar () -> SockAddr -> String -> IO ()
              handle lock clientaddr msg =
                 withMVar lock 
                    (a -> handlerfunc clientaddr msg >> return a)
    
    -- A simple handler that prints incoming packets
    plainHandler :: HandlerFunc
    plainHandler addr msg = 
        putStrLn $ "From " ++ show addr ++ ": " ++ msg
    
    对于我们SyslogTypes的实现,请参阅第612页上的“UDP客户端的例子:syslog”。
    让我们来看看这段代码。我们的主循环在procRequests中,在这里我们永远循环等待新的客户端连接。accept调用被阻塞直到客户端连接。当一个客户端连接,我们可以得到一个新的socket和客户端的地址。我们传递消息给handler,然后使用forkIO创建一个线程来处理来自客户端的数据。这个线程运行procMessages。
    当处理TCP数据时,通常可以很方便地将socket转换成一个Haskell处理。这里我们就是这样做的,并且明确地设置了缓冲buffering,对于TCP通信这是很重要的一点。接下来,我们设置了lazy read从套接字的Handle。对于每一个进入的连接,我们把它传递给handle。直到远端关闭套接字而没有更多的数据,我们便输出相关消息。
    因为我们可能会一次处理多个传入的消息,我们需要确保没有在handler中一次写出多个消息。这可能会导致输出乱码。我们用一个简单的锁使得对handler的访问有顺序,并写了一个简单的handle函数来处理。
    我们将会用我们即将展示的客户端测试它,或者我们甚至可以使用telnet程序来连接到这台服务器。我们发送给服务器的每一行文本将被打印在显示屏上。让我们尝试一下:
    
    ghci> :load syslogtcpserver.hs
    [1 of 1] Compiling Main ( syslogtcpserver.hs, interpreted )
    Ok, modules loaded: Main.
    ghci> serveLog "10514" plainHandler
    Loading package parsec-2.1.0.0 ... linking ... done.
    Loading package network-2.1.0.0 ... linking ... done. 
    
    现在,服务器将开始在10514端口侦听连接。它几乎不做任何事情,直到客户端连接之前。我们可以使用telnet连接到服务器:
    
    ~$ telnet localhost 10514
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    Test message
    ^]
    telnet> quit
    Connection closed. 
    
    与此同时,我们在运行TCP服务器的终端,看到如下的内容:
    
    From 127.0.0.1:38790: syslogtcpserver.hs: client connnected
    From 127.0.0.1:38790: Test message
    From 127.0.0.1:38790: syslogtcpserver.hs: client disconnected 
    
    这显示了一个在在本地机器上(127.0.0.1)的客户端从38790端口连接进来。当它连接之后,发送了一个消息并断开了连接。当你作为一个TCP客户端,操作系统为你分配一个未使用的端口。此端口号在你每次运行程序的时候通常是不同的。
    
    TCP Syslog客户端
    
    现在,让我们为我们的TCP syslog协议来写一个客户端。该客户端和UDP客户端很相似,但也有一些变化。首先,由于TCP是流协议,我们可以使用Handler发送数据,而不是使用低级的socket操作。其次,我们不再需要在SyslogHandle中存储目标地址,因为我们将使用connect来建立TCP连接。最后,我们需要一种方式来知道一个消息的结束和下一个消息的开始。使用UDP,这很简单,因为每个信息是一个独立的逻辑分组。使用TCP,我们只使用换行符'
    '消息的结束标志,虽然这意味着,单个的消息不可能再包含换行符。下面是我们的代码:
    
    -- file: ch27/syslogtcpclient.hs
    import Data.Bits
    import Network.Socket
    import Network.BSD
    import Data.List
    import SyslogTypes
    import System.IO
    
    data SyslogHandle = 
        SyslogHandle {slHandle :: Handle,
    
    slProgram :: String}
    openlog :: HostName -- ^ Remote hostname, or localhost
            -> String -- ^ Port number or name; 514 is default
            -> String -- ^ Name to log under
            -> IO SyslogHandle -- ^ Handle to use for logging
    
    openlog hostname port progname =
        do -- Look up the hostname and port. Either raises an exception
           -- or returns a nonempty list. First element in that list
           -- is supposed to be the best option.
           addrinfos <- getAddrInfo Nothing (Just hostname) (Just port)
           let serveraddr = head addrinfos
    
           -- Establish a socket for communication
           sock <- socket (addrFamily serveraddr) Stream defaultProtocol
    
           -- Mark the socket for keep-alive handling since it may be idle
           -- for long periods of time
           setSocketOption sock KeepAlive 1
    
           -- Connect to server
           connect sock (addrAddress serveraddr)
    
           -- Make a Handle out of it for convenience
           h <- socketToHandle sock WriteMode
    
           -- We're going to set buffering to BlockBuffering and then
           -- explicitly call hFlush after each message, below, so that
           -- messages get logged immediately
           hSetBuffering h (BlockBuffering Nothing)
    
           -- Save off the socket, program name, and server address in a handle
           return $ SyslogHandle h progname
    
    syslog :: SyslogHandle -> Facility -> Priority -> String -> IO ()
    syslog syslogh fac pri msg =
        do hPutStrLn (slHandle syslogh) sendmsg
           -- Make sure that we send data immediately
           hFlush (slHandle syslogh)
        where code = makeCode fac pri
              sendmsg = "<" ++ show code ++ ">" ++ (slProgram syslogh) ++
                        ": " ++ msg
    
    closelog :: SyslogHandle -> IO ()
    closelog syslogh = hClose (slHandle syslogh)
    
    {- | Convert a facility and a priority into a syslog code -}
    makeCode :: Facility -> Priority -> Int
    makeCode fac pri =
        let faccode = codeOfFac fac
            pricode = fromEnum pri 
            in
              (faccode `shiftL` 3) .|. pricode
    
    我们可以在ghci中实验。如果你之前的TCP服务器还在运行,你的会话可能会是这个样子:
    
    ghci> :load syslogtcpclient.hs
    Loading package base ... linking ... done.
    [1 of 2] Compiling SyslogTypes ( SyslogTypes.hs, interpreted )
    [2 of 2] Compiling Main ( syslogtcpclient.hs, interpreted )
    Ok, modules loaded: Main, SyslogTypes.
    ghci> openlog "localhost" "10514" "tcptest"
    Loading package parsec-2.1.0.0 ... linking ... done.
    Loading package network-2.1.0.0 ... linking ... done.
    ghci> sl <- openlog "localhost" "10514" "tcptest"
    ghci> syslog sl USER INFO "This is my TCP message"
    ghci> syslog sl USER INFO "This is my TCP message again"
    ghci> closelog sl
    
    在服务器上,你会看到这样的内容:
    
    From 127.0.0.1:46319: syslogtcpserver.hs: client connnected
    From 127.0.0.1:46319: <9>tcptest: This is my TCP message
    From 127.0.0.1:46319: <9>tcptest: This is my TCP message again
    From 127.0.0.1:46319: syslogtcpserver.hs: client disconnected
    
    <9>是优先级和设备代码一起被发送,和使用UDP类似。
    作者:Hevienz
    出处:http://www.cnblogs.com/hymenz/
    知识共享许可协议
    本博客原创作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
  • 相关阅读:
    ArcGIS Server TileLayer 跨域读取
    dojo.declare 未定义
    注册部署SOE, 提交SOE只能在IE浏览器中
    在maptalks中加载三维模型obj,fbx,glb
    三维模型 obj 转化为 three Json 文件格式
    leaflet map 地图初始化不能铺满div
    查找进行的过程中被停止 解决办法
    逆向的第一个小代码
    编码不规范导致的错误
    android4.4.2 短信广播变更
  • 原文地址:https://www.cnblogs.com/hymenz/p/3322233.html
Copyright © 2011-2022 走看看