۳۰ - ۳بررسیِ فینگِر

اگه روی دستگاهِ محلی‌تون ‏‎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.yaml

‏‎fingerd.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 رو می‌نویسیم.

*

م. میزبان محلی، "این کامپیوتر"