۳۰ - ۳بررسیِ فینگِر
اگه روی دستگاهِ محلیتون fingerd رو تحتِ نامکاربری ِ callen اجرا میکردین، نتیجهش چیزی شبیهِ این میشد:
$ finger callen@localhost
Login: callen Name: callen
Directory: /home/callen Shell: /bin/zshبدون تعیینِ ناممیزبان برای استعلام، همینطوری روی OSX (بدون نیاز به نصب یا اجرای هیچ سرویسِ فینگری) کار میکنه:
$ finger callen
Login: callen Name: callen
Directory: /Users/callen Shell: /bin/bashپروتکل ِ فینگر روی سوکتهای پروتکل کنترل انتقال (TCP) کار میکنه، که همون پروتکل ایه که مرورگرهای وب ازش استفاده میکنن. ولی با اینکه هردوشون از TCP استفاده میکنن، یه فینگر دیمن، وب سرور نیست. یه چیزِ خیلی سادهتریه. بجای اینکه مثلِ وب یه لایه پروتکل کاربُردی ِ کامل روی TCP داشته باشه (HTTP)، فقط یه پروتکلِ متنی ِ تکپیغامی ِه. نمیخوایم توضیحاتِ طولانی برای اینترنت، UDP، و TCP بدیم، فقط میگیم که TCP پروتکلی* برای ارسال و دریافتِ پیغام بینِ یه کارخواه و یه کارساز ِه. پیغامها میتونن متن یا بایتهای خام باشن. یه سوکت، آدرسیه که پیغام بهش ارسال میشه.
م. خیلی خلاصه، پروتکل رو میشه دسته قواعدی برای نحوهی ارتباط و مکاتبه تعریف کرد.
سوکت به کنار، بطور کلی باید اینطوری کار کنه: کلاینت مقداری اطلاعات درخواست میکنه، و اون درخواست به کمکِ جادویِ TCP به سرور رسونده میشه. سرور (دیمن ِ عزیزِمون) اون اطلاعات رو جمعوجور میکنه (البته اگه داشته باشه)، و باز جادوی TCP اون اطلاعات رو به کلاینت میفرسته، بعد هم کلاینت اونها رو تویِ ترمینالِتون چاپ میکنه. ما پروژهمون رو با یه سرورِ اِکو ِ کوچولو شروع میکنیم که دقیقاً همون متنی که کلاینت ارسال میکنه رو چاپ میکنه، تا یه کم با کلیاتِ پروژه آشنا بشیم.
کلیات پروژه
با دستورِ stack new کار رو شروع میکنیم:
$ stack new fingerd simpleاین دستور یه پروژهی ساده با یک پاراگراف ِ executable در فایلِ Cabal درست میکنه. پوشهبندی و فایلهای نسخهی نهایی (بعد از اضافه کردنِ Debug.hs) اینطوری میشه:
$ tree .
.
├── LICENSE
├── Setup.hs
├── fingerd.cabal
├── src
| ├── Debug.hs
| └── Main.hs
└── stack.yamlfingerd.cabal
پاراگرافهای executable در فایلِ Cabal ِمون فایلهایی دارن که فعلاً درست نمیکنیم، پس میتونین فعلاً همون پاراگرافهایی که خودِ Stack ایجاد کرده رو نگه دارین. دقت کنین یه کم فرمتِ نوشتاریش رو تغییر دادیم تا با فرمتِ کتاب جور باشه:
name: fingerd
version: 0.1.0.0
synopsis: Simple project template
description: Please see README.md
homepage: https://github.com/u/fingerd
license: BSD3
license-file: LICENSE
author: Chris Allen
maintainer: cma@bitemyapp.com
copyright: 2016, Chris Allen
category: Web
build-type: Simple
cabal-version: >=1.10
executable debug
ghc-options: -Wall
hs-source-dirs: src
main-is: Debug.hs
default-language: Haskell2010
build-depends: base >= 4.7 && < 5
, network
executable fingerd
ghc-options: -Wall
hs-source-dirs: src
main-is: Main.hs
default-language: Haskell2010
build-depends: base >= 4.7 && < 5
, bytestring
, network
, raw-strings-qq
, sqlite-simple
, textحالا بریم سراغ کدنویسی.
src/Debug.hs
این اولین فایلِ منبعِمون میشه. از این برنامه برای نشون دادنِ چیزی که کلاینت میفرسته، و ارسالِ مجددِ همون چیز استفاده میکنیم. از این لحاظ دقیقاً عینِ سرورِ اِکویی که در مستنداتِ کتابخونه ِ network نوشته شده میمونه. تنها فرقی که داره اینه که ارائهی لیترال ِ متنی که فرستاده شده بوده رو هم چاپ میکنه.
برنامهی اشکالزدایی که اینجا مینویسیم، مشابهِ یه وب سرور ِه که صفحهی وب تأمین میکنه، ولی سطحِ پایینتر، و محدود به ارسال و دریافتِ متنِ خام ِه. فرقی که داره اینه که یه وب سرور از روی سوکت ِ TCP و با استفاده از پروتکلی ساختاردار و غنی از فراداده، مسیرها، و استانداردی که اون پروتکل رو توصیف میکنه با مرورگرها ارتباط برقرار میکنه. کاری که ما میخوایم انجام بدیم قدیمیتر و بدَویتر ِه.
module Main where
import Control.Monad (forever)
import Network.Socket hiding (recv)
import Network.Socket.ByteString
(recv, sendAll)
logAndEcho :: Socket -> IO ()
logAndEcho sock = forever $ do
(soc, _) <- accept sock
printAndKickback soc
sClose soc
where printAndKickback conn = do
msg <- recv conn 1024
print msg
sendAll conn msgاین، سرورِمون رو بَرپا میکنه. آرگومانمون یه سوکت ِه (sock) که منتظرِ شنیدنِ ارتباط های جدیدِ کلاینت هاست؛ به خاطرِ اون forever، سوکت همینطور باز میمونه. اجراییه ِ accept بلوکه میمونه تا یه کلاینت به سرور وصل بشه. سوکت ِ soc نتیجهی accept کردن (م. یا قبول کردن) یه ارتباط برای مکاتبه با کلاینت ِه.
سرور میتونه تا ۱۰۲۴ بایت متن رو از کلاینت دریافت کنه. تنها کاری که اینجا انجام میده اینه که دقیقاً همون متن رو چاپ کنه، و بعد همون چیزی که کلاینت ِ وصلشده فرستاده رو به خودش بفرسته (یا اِکو کنه). بعد ارتباط با کلاینت بسته میشه – sClose رو فقط به soc اعمال میکنیم، یعنی sock، یا سوکتِ سرور، باز میمونه. به دلیلِ اینکه این اجراییه تا ابد حلقه میزنه، کارِ بعدیای که انجام میدیم اینه که منتظر یه ارتباطِ کلاینت ِ دیگه باشیم.
main :: IO ()
main = withSocketsDo $ do
addrinfos <- getAddrInfo
(Just (defaultHints
{addrFlags =
[AI_PASSIVE]}))
Nothing (Just "79")
let serveraddr = head addrinfos
sock <- socket (addrFamily serveraddr)
Stream defaultProtocol
bindSocket sock (addrAddress serveraddr)
listen sock 1
logAndEcho sock
sClose sockاون withSocketsDo در اولِ main، هیچ کاری انجام نمیده، مگر اینکه روی ویندوز باشین. اگه روی ویندوز هستین، حتماً باید از این API ِ سوکتها در کتابخونه ِ network استفاده کنین.* اون کُدهایی که به اطلاعاتِ آدرس ربط دارن، برای توصیف نوعِ سرورِ TCP ای که باز کردیم، و اینکه سرورِمون از چه پورتی داره گوش میده لازماند.
م. التبه در نسخههای جدیدترِ network، این تابع برای این کارِ بخصوص واجب نیست. با این حال، به خاطرِ سازگاری با نسخههای قبلی در ویندوز، پیشنهاد میشه هنوز این تابع رو صدا بزنین (تابعِ خیلی کم هزینهایه).
بخشِ مهمش اون (Just "79") ِه – این اون پورت ایه که برای برقراریِ ارتباط داره بهش گوش میده. دقت کنین که روی اکثر سیستم عاملها، برای گوش دادن به اون پورت باید اختیاراتِ اَدمین داشته باشین.
کتابخونههای TCP مثل network، معمولاً به همه چیز میگن سوکت. سرور که منتظرِ شنیدنِ ارتباط ِه؟ سوکت ِه. ارتباطِ کلاینت که منتظرِ شیندنش بودین؟ اون هم سوکت ِه. همه چیز سوکت ِه. هیچ چیز هم آچار نیست.
تیکهی بعدی، با socket یه جور واصفِ سوکت درست میکنه. بعد سوکت رو به همون آدرسی (یا پورتی) که میخواستیم انقیاد میکنیم. در آخر، با listen به سیستم عامل میگیم که آمادهی گوش سپردن به ارتباطها از کلاینتها هستیم. از اونجا هم منطقی که برای سرورِمون نوشتیم رو اجرا میکنیم، که اون هم تا بینهایت اجرا میشه. اگه logAndEcho کارِش تموم بشه، سوکتِ سرور رو میبندیم و ماجرا تموم میشه.
مرحلهی بعدی (با فرض اینکه پروژهتون رو ساختین) اجرای سرورِ debug ِه – دقت کنین که برای استفاده از پورت ِ ۷۹، اختیاراتِ مدیریتی میخواد:
$ sudo `stack exec which debug`
{... build درخواستِ رمزعبور و بعد شلوغی ...}حالا سرورِ اِکومون راه افتاده و آمادهست که با telnet بهش وصل بشیم. کاربردِ telnet معمولاً برای اشکالزدایی ِ سرویسهای TCP ایه که برای مکاتبه از متن استفاده میکنن. به اون sudo هم دقت کنین – چه از این راه، چه از هر راه دیگهای، باید موقعِ شروعِ این برنامه اختیاراتِ ادمین داشته باشین، چون میخواد از پورتِ شبکهای استفاده کنه که در اکثر سیستم عاملها فقط در اختیارِ ادمینها، یا اکانتهای ریشه هست. معمولاً ۱۰۲۴ پورتِ اول اینطوریاند. بعد از اینکه سرورِ debug رو توی یه ترمینال اجرا کردین، این شکلی از یه ترمینالِ دیگه بهش وصل میشین:
$ telnet local host 79
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
الان telnet منتظرِ شماست تا یه چیزی تایپ کنین و enter بزنین:
blah
blah
Connection closed by foreign host.اینجا تایپ کردیم blah، دکمهی enter رو زدیم، و blah بهمون اِکو شد (بازتاب شد)، بعد سرور ارتباط رو بست. یادتون باشه که تابعِ sClose به soc در logAndEcho اعمال شده، و ارتباط ِ موقتِ telnet رو بست. ولی سرور هنوز بازه، و اگه دوباره ارتباط ِ telnet رو باز کنین، باز هم میتونین درخواست بفرستین.
حالا ببینیم سمتِ سرور چی چاپ شد:
"blah\r\n"ما توی logAndEcho عمداً بجای putStrLn از print استفاده کردیم تا از دادهای که فرستاده شده، ارائهی لیترال (م. دقیق و لفظی) بگیریم. اینجا، نوشتهی blah به همراه کاراکترهای مخصوصِ \r و \n فرستاده شدن. در سیستم عاملهای برپایهی یونیکس مثلِ لینوکس، \n کاراکترِ پیشفرض برای پایانِ خط ِه. در مایکروسافت ویندوز، \r\n (کاراکترِ \r درست قبل از \n) همین نقش رو داره.
حالا همین کار رو با یه کلاینتِ فینگر انجام بدیم:
$ finger callen@localhost
[localhost]
Trying 127.0.0.1...
callen
$ finger @localhost
[localhost]
Trying 127.0.0.1
اگه روی یه Mac باشین، ممکنه یه کم شلوغی مثل اینها بگیرین:
Trying ::1...
finger: connect: Connection refused
Trying 127.0.0.1...بعدش باید وصل شه. اول برای دسترسی به فینگر دیمن، IPv6 رو امتحان میکنه؛ وقتی نمیتونه، باید بره سراغِ IPv4. کلاً میتونین اینها رو نادیده بگیرین.
خروجیای که سمتِ سرور میگیریم، اینطوری میشه:
"callen\r\n"
"\r\n"دستورِ اول، اطلاعاتِ یه کاربر به اسمِ callen رو از فینگر دیمِنی که در localhost* در حالِ اجرا بود، درخواست کرد. دیگه با توجه به اون خروجیای که از سرور گرفتیم، میدونیم استعلامهایی که از یه کلاینتِ فینگر گرفته میشن، از چشمِ سرورِ TCP مون چه شکلیاند. حالا خودِ سرورِ TCP رو مینویسیم.
م. میزبان محلی، "این کامپیوتر"