суббота, 23 июля 2011 г.

Простой TCP сервер на Haskell с Network.Socket


Написание сервера при наличии богатых библиотек для работы с сетью - вещь нетрудная. Но новичкам чаще всего не хватает хорошего комментированного примера.

В данной статье мы рассмотрим написание простейшего TCP-сервера на  Haskell.

Наш сервер будет получать сообщение, переворачивать его и посылать обратно. Для этого нам необходимы модули Network.Socket и Network.Socket.ByteString.

Конечно можно обойтись функционалом модуля Network.Socket, но использование ByteString придаст скорости нашему серверу.
Также есть модуль для работы с ленивыми ByteString. Он так и называется Network.Socket.ByteString.Lazy.

Сам модуль Network.Socket.ByteString содержит лишь четыре функции send, sendTo, recv, recvFrom. Остальные мы импортируем из Network.Socket.
import Network.Socket hiding (send, recv)
import Network.Socket.ByteString
import Control.Concurrent (forkIO)
import qualified Data.ByteString.Char8 as B8
import System.Environment (getArgs)

Вы заметили, что из модуля Network.Socket мы импортировали все, кроме send и recv? Send и recv имеются в модуле для работы с ByteString. Именно эти две функции мы и будем использовать. Поэтому импорт Network.Socket.ByteString можно сделать таким:
import Network.Socket.ByteString (send, recv)

Для работы с ByteString мы импортировали Data.ByteString.Char8 (данный модуль можно было импортировать без префикса). Для многопоточности мы будем использовать forkIO.

Теперь беремся за функционал!
server :: PortNumber -> IO ()
server port = withSocketsDo $ do
                sock <- socket AF_INET Stream defaultProtocol
                bindSocket sock (SockAddrInet port 0)
-- Слушаем сокет. 
-- Максимальное кол-во клиентов для подключения - 5.
                listen sock 5
-- Запускаем наш Socket Handler.
                sockHandler sock                 
                sClose sock

sockHandler :: Socket -> IO ()
sockHandler sock = do
-- Принимаем входящее подключение.
  (sockh, _) <- accept sock
-- В отдельном потоке получаем сообщения от клиента.
  forkIO $ putStrLn "Client connected!" >> receiveMessage sockh
  sockHandler sock

receiveMessage :: Socket -> IO ()
receiveMessage sockh = do
  msg <- recv sockh 10 -- Получаем только 10 байт.
  B8.putStrLn msg -- Выводим их.
-- Если сообщение было пусто или оно равно "q" (quit)
  if msg == B8.pack "q" || B8.null msg
-- Закрываем соединение с клиентом.
  then sClose sockh >> putStrLn "Client disconnected"
  else receiveMessage sockh -- Или получаем следующее сообщение.
Собственно это все. Чтобы запустить наш сервер на 3000-м порту, необходимо в ghci(hugs) вызвать функцию:
server 3000
Либо через функцию main:
main = do
[port'] <- getArgs
  server (fromIntegral $ read port')
Тогда можно будет скомпилировать все в бинарник:
ghc -o tcpserver TCPServer.hs
И запустить:
tcpserver 3000
На самом деле это чуть более "низкоуровневый" вариант, нежели если использовать функции модуля Network.  Пример вы можете посмотреть здесь. Полученный код можно посмотреть на github:gist.

2 комментария:

  1. Следует отметить, что recv не гарантирует получения ровно того количества байт, которое указано в агрументе. К примеру, ользователь может передать 6 байт, которые разобьются на два пакета по 3 байта, тогда recv вернет 3 байта из 6-и. В Network.Socket была замечена функция recvLen - возможно, это аналог юниксового read, но нужно разбираться.

    ОтветитьУдалить
  2. Спасибо за статью. Небольшое дополнение: в ветке else метода receiveMessage перед рекурсивным вызовом следует добавить строку

    send sockh (B8.reverse msg)

    Иначе сервер не будет выполнять свое обещание, данное клиенту из следующей статьи, т.е. переворачивать строку и отсылать ее обратно. В результате после отправки первого сообщения клиент переходит в состояние ожидания ответа, который никогда не придет

    ОтветитьУдалить