Pull to refresh

FORTH: POP3 наноклиент. Часть 2

Reading time 6 min
Views 5.5K
Во второй части попробуем написать минимального POP3 клиента. Он будет уметь подключаться к серверу, логиниться в почтовый ящик, узнавать сколько там писем и скачивать последнее. Для иллюстрации этого будет достаточно.

Что достаточно знать из POP3 протокола. Пара команд USER PASS нужна для логина, STAT чтобы получить количество писем в ящике и RETR для получения самого письма. Мы не будем удалять письма командой DELE. Команду QUIT будем отдавать из вежливости.
Посмотрим на команды повнимательнее. Можно заметить, что их можно разделить на две группы: команды без аргументов NOOP QUIT STAT RSET и команды, имеющие аргумент: DELE LIST RETR TOP APOP USER PASS. Причем последние три в качестве аргумента требуют текстовых строк. Строго говоря, все команды требуют строкового аргумента. В одном случае это пустая строка, в другом строка состоит из десятичных цифр, в третьем из произвольных символов.

Рассмотрим самый простой случай, безаргументные команды. Можно написать в лоб

S" NOOP" sockt WriteSocketLine THROW и так 4 раза. 
S" DELE" sockt WriteSocket THROW     S>D (D.) sockt WriteSocketLine THROW 
тоже 3 раза

( TOP APOP не будем реализовывать, а с USER PASS пока не будем мудрить. )


Но мы же пишем на Форте, а в нем есть изящный механизм создания определяющих слов. Используем его.
Для начала вынесем sockt WriteSocketLine THROW и buf bufsize sockt ReadSocket THROW TO bytes_read в отдельные определения. Лень уже выписывать столько букв.
: >sockt   
              sockt WriteSocketLine THROW ;

: sockt>
              buf bufsize sockt ReadSocket THROW TO bytes_read ;



И начнем создавать создающие слова

: pop3_noarg 
                     CREATE LATEST , 
                     DOES> @ COUNT  >sockt   ; 

: pop3_noargs 
                    0 DO pop3_noarg LOOP ;

4 pop3_noargs NOOP QUIT STAT RSET 


Таким образом, тремя строчками мы определили четыре однотипных команды. Поясню немного поподробнее, что же здесь происходит.
Через двоеточие определено новое слово pop3_noarg, со следующим действием: выбрать из входного потока литеры, ограниченные пробелом, использовать их для создания на вершине словаря слова с действием VARIABLE. Это все делает слово CREATE, в момент исполнения слова pop3_noarg. Следующее слово LATEST кладет на стек адрес поля имени определяемого слова, например NOOP. Запятая компилирует значения со стека на вершину словаря. Если бы далее не было части DOES>, вызов NOOP положил бы на стек некий адрес, разыменовав который мы получили бы адрес строки со счетчиком, превратив который в адрес строки счетчик словом COUNT мы могли бы напечатать ее словом TYPE.
В результате увидели бы NOOP. Словами выглядит очень сложно, быстрее попробовать самостоятельно в командной строке форт-системы.
Что же далее? Далее у нас присутствует самое загадочное слово Форта DOES>. Достаточно осознать, что это слово меняет действие по-умолчанию слова, созданного через CREATE, на действие, которое мы пожелаем. Все, что написано после DOES> является общим действием для всех слов, определенных через pop3_noarg. Здесь оно заключается в получении адреса и счетчика строки — имени слова и отправки его в сокет.

: pop3_digiarg 
               CREATE LATEST , 
               DOES>    @ >R       <#    S>D  #S     BL HOLD     R> COUNT HOLDS #>   >sockt   ;

: pop3_digiargs  0 DO pop3_digiarg LOOP ;

3 pop3_digiargs  DELE LIST RETR 


Очень похоже на предыдущую группу слов с заметным отличием в части DOES>. Понятно, что что-то отправляется в сокет. А вот что?
Здесь мы используем еще одну замечательную возможность Форта. У нас есть прекрасное слово (D.) преобразующее число двойной точности (64 бит) со стека в строку. И мы могли бы его легко использовать, но для этого нам бы пришлось написать что-то вроде

S" LIST" sockt WriteSocket S>D (D.) >sockt


Не страшно, но как-то уныло.

Раз уж мы решили сделать красиво, почему бы не поискать другие решения. Если заглянуть в исходники Форта, и найти там определение (D.) можно увидеть что оно состоит из <# #S #>. Эти три слова изобретены еще Чаком Муром и раскладывают преобразование числа в строку на три этапа. Первый — подготовка преобразования, второй — собственно преобразование, и третий — выдача результата. Из-за того, что преобразование числа происходит с младших разрядов, а печать идет со старших разрядов — для преобразования нужен некоторый временный буфер, куда будут складываться цифры и откуда будет забираться результат. Размер буфера берется с хорошим запасом. Слово HOLD как раз и убирает очередной символ со стека в буфер. HOLDS, множественное число от HOLD, делает то же самое с несколькими символами ака строкой. Пара слов >R R> используется как легкая подручная реализация локальных переменных, когда надо некоторое значение со стека ненадолго отложить.
Подобная эквилибристика позволяет не заводить дополнительные буфера, не копировать одно и тоже в кучу мест, не возиться с библиотеками для слияния строк, а сделать все в системном буфере практически предназначенном для этой цели.

Теперь посмотрим на ответы сервера. Их всего два: +OK или -ERR, за которыми могут идти, а могут не идти какой-нибудь текст или числа. Было бы прелестно, если бы ответы сервера как-нибудь сами делали всю работу. Чтобы нам не приходилось скакать по бесконечным ветвлениям. И Форт может нам это позволить.
Представим буфер, в котором сохранен ответ сервера как входной поток для интерпретации. Словом EVALUATE мы можем интерпретировать произвольную строку. Используем эту возможность.
Ответ сервера всегда начинается с +OK или -ERR. Вложим же смысл в эти ответы.

Проще всего с -ERR. В случае ошибки неплохо бы вывести остаток строки на терминал и прервать исполнение программы.

 : -ERR     1 PARSE TYPE CR ABORT ; 


PARSE служит для получения хвоста интерпретируемой строки.

+OK не так прост. Он всегда выскакивает, если команда обработана успешно. Говорит нам все хорошо, но каков контекст этого хорошо?
Первый +OK сообщает об успешном подключении к серверу. Нам надо принять этот факт и вывести на терминал приветствие сервера.
Второй и третий +OK возникают в процедуре логина. Просто игнорируем остаток строки.
Четвертый +OK появляется в ответ на STAT и к нему, к +OKею, прилагаются два числа. Первое — количество сообщений в ящике. Второе — число попугаев, занимаемых ними. Попугаи нам не нужны. Значит нам надо будет превратить текстовое число из ответа сервера в число на стеке, а остаток строки игнорировать.
Пятый +OK сообщит что следом за ним будет нужное нам письмо. Тут есть один нюанс в протоколе. Во-первых, пауза между ответом сервера и передачей письма. Во-вторых, два числа следом за +OK — номер сообщения и его размер в попугаях.
Шестого +OK, можно не ждать, а можно обработать также, как и первый.
Итак, +OK должен уметь три разных действия.
Форт позволяет обойтись без флагов и сложной логики. В нем есть так называемые векторные слова. Слова, действие которым можно назначить в любой нужный момент.

VECT +OK

: do_type       1 PARSE TYPE CR ;
: do_ignore    1 PARSE 2DROP ;
: do_number     1 PARSE ?SLITERAL ;
: do_msg         
                sockt>                 
                BEGIN   buf bytes_read  TYPE CR 
                bytes_read  bufsize - 0< 0= WHILE  sockt>  REPEAT do_ignore ;



Теперь у нас есть все, чтобы собрать код воедино.

Код

~nn/lib/sock2.f  

0 VALUE sockt
0 VALUE bytes_read
0 VALUE num_of_msgs

8192 CONSTANT bufsize
bufsize ALLOCATE THROW CONSTANT buf
 
MODULE:

: >sockt   
              sockt WriteSocketLine THROW ;

: sockt>
              buf bufsize sockt ReadSocket THROW TO bytes_read ;

: response  
                    sockt>
                     buf bytes_read EVALUATE ;


: pop3_noarg 
                     CREATE LATEST , 
                     DOES> @ COUNT  >sockt   response ; 

: pop3_noargs 
                    0 DO pop3_noarg LOOP ;

4 pop3_noargs NOOP QUIT STAT RSET


: pop3_digiarg 
               CREATE LATEST , 
               DOES>    @ >R       <#    S>D  #S     BL HOLD     R> COUNT HOLDS #>   >sockt  response  ;

: pop3_digiargs  0 DO pop3_digiarg LOOP ;

3 pop3_digiargs  DELE LIST RETR


: user              S" USER username"  >sockt response  ;
: pass             S" PASS password"  >sockt  response ;


VECT +OK

: do_type           1 PARSE TYPE CR ;
: do_ignore         1 PARSE 2DROP ;
: do_number    1 PARSE ?SLITERAL do_ignore ;
: do_msg         
                sockt>                 
                BEGIN   buf bytes_read  TYPE CR 
                bytes_read  bufsize - 0< 0= WHILE  sockt>  REPEAT do_ignore ;

: -ERR    do_type ABORT ;


: start_sockets   
                       SocketsStartup THROW
                       CreateSocket THROW TO sockt ;

: connect_to_host
                      GetHostIP THROW 
                     110 sockt ConnectSocket THROW 
                      response  ;

\   Ниже исполняемый код. Выше - необходимые определения.

start_sockets

' do_type TO +OK 

S" pop.server.com" connect_to_host 

' do_ignore TO +OK

user  pass  

' do_number TO +OK 

STAT   TO  num_of_msgs

' do_msg TO +OK 
num_of_msgs RETR  

' do_type TO +OK
QUIT


Данный код несложно дополнить, чтобы он выкачивал все или какое-то количество сообщений с сервера, сохранял их на локальный диск, удалял с сервера и т.п. Здесь я постарался продемонстрировать важный для программирования на языке Форт принцип декомпозиции, развитый и описанный Лео Броуди, и некоторые имеющиеся в языке инструменты для решения задач.

FORTH: наносервера и наноклиенты. Часть 1

PS: При написании топика Хабр несколько пострадал.
Tags:
Hubs:
+14
Comments 12
Comments Comments 12

Articles