В данной статье мы рассмотрим написание простейшего 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.
Следует отметить, что recv не гарантирует получения ровно того количества байт, которое указано в агрументе. К примеру, ользователь может передать 6 байт, которые разобьются на два пакета по 3 байта, тогда recv вернет 3 байта из 6-и. В Network.Socket была замечена функция recvLen - возможно, это аналог юниксового read, но нужно разбираться.
ОтветитьУдалитьСпасибо за статью. Небольшое дополнение: в ветке else метода receiveMessage перед рекурсивным вызовом следует добавить строку
ОтветитьУдалитьsend sockh (B8.reverse msg)
Иначе сервер не будет выполнять свое обещание, данное клиенту из следующей статьи, т.е. переворачивать строку и отсылать ее обратно. В результате после отправки первого сообщения клиент переходит в состояние ожидания ответа, который никогда не придет