WEBMASTER

Купить диплом

 

Программирование на языке Ruby(книга Х.Фултон, Глава 18.)

Гла­ва 18. Се­тевое прог­рамми­рова­ние

Ес­ли тор­го­вец в раз­го­воре с ва­ми про­из­но­сит сло­во «сеть», ско­рее все­го, он же­ла­ет всу­чить свою ви­зит­ную кар­точку. Но в ус­тах прог­раммис­та это сло­во обоз­на­ча­ет элек­трон­ное вза­имо­дей­ствие фи­зичес­ки уда­лен­ных ма­шин — не­важ­но, на­ходят­ся они в раз­ных уг­лах ком­на­ты, в раз­ных рай­онах го­рода или в раз­ных час­тях све­та.

Для прог­раммис­тов сеть ча­ще все­го ас­со­ци­иру­ет­ся с на­бором про­токо­лов TCP/IP — тем язы­ком, на ко­тором нес­лышно бе­седу­ют мил­ли­оны ма­шин, под­клю­чен­ных к се­ти Ин­тернет. Нес­коль­ко слов об этом на­боре, пе­ред тем как мы пе­рей­дем к кон­крет­ным при­мерам.

Кон­цепту­аль­но се­тевое вза­имо­дей­ствие при­нято пред­став­лять в ви­де раз­личных уров­ней (или сло­ев) абс­трак­ции. Са­мый ниж­ний — ка­наль­ный уро­вень, на ко­тором про­ис­хо­дит ап­па­рат­ное вза­имо­дей­ствие; о нем мы го­ворить не бу­дем. Сра­зу над ним рас­по­ложен се­тевой уро­вень, ко­торый от­ве­ча­ет за пе­реме­щение па­кетов в се­ти — это епар­хия про­токо­ла IP (Internet Protocol). Еще вы­ше на­ходит­ся тран­спортный уро­вень, на ко­тором рас­по­ложи­лись про­токо­лы TCP (Transmission Control Protocol) и UDP (User Datagram Protocol). Да­лее мы ви­дим прик­ладной уро­вень — это мир telnet, FTP, про­токо­лов элек­трон­ной поч­ти и т.д.

Мож­но об­ме­нивать­ся дан­ны­ми не­пос­редс­твен­но по про­токо­лу IP, но обыч­но так не пос­ту­па­ют. Ча­ще нас ин­те­ресу­ют про­токо­лы TCP и UDP.

Про­токол TCP обес­пе­чива­ет на­деж­ную связь меж­ду дву­мя компь­юте­рами (хос­та­ми). Он упа­ковы­ва­ет дан­ные в па­кеты и рас­па­ковы­ва­ет их, под­твержда­ет по­луче­ние па­кетов, уп­равля­ет тайм-а­ута­ми и т.д. Пос­коль­ку про­токол на­деж­ный, при­ложе­нию нет нуж­ды бес­по­ко­ить­ся о том, по­лучил ли уда­лен­ный хост пос­ланные ему дан­ные.

Про­токол UDP го­раз­до про­ще: он от­прав­ля­ет па­кеты (да­таг­раммы) уда­лен­но­му хос­ту, как буд­то это дво­ич­ные поч­то­вые от­крыт­ки. Нет ни­какой га­ран­тии, что дан­ные бу­дут по­луче­ны, по­это­му про­токол на­зыва­ет­ся не­надеж­ным (а, сле­дова­тель­но, при­ложе­нию при­дет­ся оза­ботить­ся до­пол­ни­тель­ны­ми де­таля­ми).

Ruby под­держи­ва­ет се­тевое прог­рамми­рова­ние на низ­ком уров­не (глав­ным об­ра­зом по про­токо­лам TCP и UDP), а так­же и на бо­лее вы­соких, в том чис­ле по про­токо­лам telnet, FTP, SMTP и т.д.

На рис. 18.1 пред­став­ле­на и­ерар­хия клас­сов, из ко­торой вид­но, как ор­га­низо­вана под­дер­жка се­тево­го прог­рамми­рова­ния в Ruby. По­каза­ны клас­сы HTTP и не­кото­рые дру­гие столь же вы­соко­го уров­ня; кое-что для крат­кости опу­щено.

Рис. 18.1. Часть и­ерар­хии нас­ле­дова­ния для под­дер­жки се­тево­го прог­рамми­рова­ния в Ruby

От­ме­тим, что боль­шая часть этих клас­сов пря­мо или кос­венно нас­ле­ду­ет клас­су IO. Сле­дова­тель­но, мы мо­жет поль­зо­вать­ся уже зна­комы­ми ме­тода­ми дан­но­го клас­са.

По­пыт­ка до­кумен­ти­ровать все фун­кции всех по­казан­ных клас­сов за­вела бы нас да­леко за рам­ки этой кни­ги. Я лишь по­кажу, как мож­но при­менять их к ре­шению кон­крет­ных за­дач, соп­ро­вож­дая при­меры крат­ки­ми по­яс­не­ни­ями. Пол­ный пе­речень всех ме­тодов вы мо­жете най­ти к спра­воч­ном ру­ководс­тве на сай­те ruby-doc.org.

Ряд важ­ных об­ластей при­мене­ния в дан­ной гла­ве во­об­ще не рас­смат­ри­ва­ет­ся, по­это­му сра­зу упо­мянем о них. Класс Net::Telnet упо­мина­ет­ся толь­ко в свя­зи с NTP-сер­ве­рами в раз­де­ле 18.2.2; этот класс мо­жет быть по­лезен не толь­ко для ре­али­зации собс­твен­но­го telnet-кли­ен­та, но и для ав­то­мати­зации всех за­дач, под­держи­ва­ющих ин­терфейс по про­токо­лу telnet.

Биб­ли­оте­ка Net::FTP так­же не рас­смат­ри­ва­ет­ся. В об­щем слу­чае ав­то­мати­зиро­вать об­мен по про­токо­лу FTP нес­ложно и с по­мощью уже име­ющих­ся кли­ен­тов, так что не­об­хо­димость в этом клас­се воз­ни­ка­ет ре­же, чем в про­чих.

Класс Net::Protocol, яв­ля­ющий­ся ро­дитель­ским для клас­сов HTTP, POP3 и SMTP по­лезен ско­рее для раз­ра­бот­ки но­вых се­тевых про­токо­лов, но эта те­ма в дан­ной кни­ге не об­сужда­ет­ся.

На этом за­вер­шим крат­кий об­зор и прис­ту­пим к рас­смот­ре­нию низ­ко­уров­не­вого се­тево­го прог­рамми­рова­ния.

18.1. Се­тевые сер­ве­ры

Жизнь сер­ве­ра про­ходит в ожи­дании вход­ных со­об­ще­ний и от­ве­тах на них.

Не ис­клю­чено, что для фор­ми­рова­ния от­ве­та тре­бу­ет­ся серь­ез­ная об­ра­бот­ка, нап­ри­мер об­ра­щение к ба­зе дан­ных, но с точ­ки зре­ния се­тево­го вза­имо­дей­ствия сер­вер прос­то при­нима­ет зап­ро­сы и от­прав­ля­ет от­ве­ты.

Но да­же это мож­но ор­га­низо­вать раз­ны­ми спо­соба­ми. Сер­вер мо­жет в каж­дый мо­мент вре­мени об­слу­живать толь­ко один зап­рос или иметь нес­коль­ко по­токов. Пер­вый под­ход про­ще ре­али­зовать, за­то у вто­рого есть пре­иму­щес­тва, ког­да мно­го кли­ен­тов од­новре­мен­но об­ра­ща­ет­ся с зап­ро­сами.

ПРОГОН САЙТА

Мож­но пред­ста­вить се­бе сер­вер, единс­твен­ное наз­на­чение ко­торо­го сос­то­ит в том, что­бы об­легчить об­ще­ние меж­ду кли­ен­та­ми. Клас­си­чес­кие при­меры — чат-сер­ве­ры, иг­ро­вые сер­ве­ры и фай­ло­об­менные се­ти.

18.1.1. Прос­той сер­вер: вре­мя дня

Рас­смот­рим са­мый прос­той сер­вер, ко­торый вы толь­ко спо­соб­ны пред­ста­вить. Пусть не­кото­рая ма­шина рас­по­лага­ет та­кими точ­ны­ми ча­сами, что ее мож­но ис­поль­зо­вать в ка­чес­тве стан­дарта вре­мени. Та­кие сер­ве­ры, ко­неч­но, су­щес­тву­ют, но вза­имо­дей­ству­ют не по то­му три­ви­аль­но­му про­токо­лу, ко­торый мы об­су­дим ни­же. (В раз­де­ле 18.2.2 при­веден при­мер об­ра­щения к по­доб­но­му сер­ве­ру по про­токо­лу telnet.)

В на­шем при­мере все зап­ро­сы об­слу­жива­ют­ся в по­ряд­ке пос­тупле­ния од­но­поточ­ным сер­ве­ром. Ког­да при­ходит зап­рос от кли­ен­та, мы воз­вра­ща­ем стро­ку, со­дер­жа­щую те­кущее вре­мя. Ни­же при­веден код сер­ве­ра:

require «socket»

 

PORT = 12321

 

HOST = ARGV[0] || ‘localhost’

 

server = UDPSocket.open # При­меня­ет­ся про­токол UDP…

server.bind nil, PORT

 

loop do

 text, sender = server.recvfrom(1)

 server.send(Time.new.to_s + «\n», 0, sender[3], sender[1])

end

А это код кли­ен­та:

require «socket»

require «timeout»

 

PORT = 12321

 

HOST = ARGV[0] || ‘localhost’

 

socket = UDPSocket.new

socket.connect(HOST, PORT)

 

socket.send(«», 0)

timeout(10) do

time = socket.gets

 puts time

end

Что­бы сде­лать зап­рос, кли­ент по­сыла­ет пус­той па­кет. Пос­коль­ку про­токол UDP не­наде­жен, то, не по­лучив от­ве­та в те­чение не­кото­рого вре­мени, мы за­вер­ша­ем ра­боту по тайм-а­уту.

В сле­ду­ющем при­мере та­кой же сер­вер ре­али­зован на ба­зе про­токо­ла TCP. Он прос­лу­шива­ет порт 12321; зап­ро­сы к это­му пор­ту мож­но по­сылать с по­мощью прог­раммы telnet (или кли­ен­та, код ко­торо­го при­веден ни­же).

require «socket»

 

PORT = 12321

 

server = TCPServer.new(PORT)

while (session = server.accept)

session.puts Time.new

 session.close

end

Об­ра­тите вни­мание, как прос­то ис­поль­зо­вать класс TCPServer. Вот TCP-вер­сия кли­ен­та:

require «socket»

 

PORT = 12321

HOST = ARGV[0] || «localhost»

 

session = TCPSocket.new(HOST, PORT)

time = session.gets

session.close

puts time

18.1.2. Ре­али­зация мно­гопо­точ­но­го сер­ве­ра

Не­кото­рые сер­ве­ры дол­жны об­слу­живать очень ин­тенсив­ный по­ток зап­ро­сов. В та­ком слу­чае эф­фектив­нее об­ра­баты­вать каж­дый зап­рос в от­дель­ном по­токе.

Ни­же по­каза­на ре­али­зация сер­ве­ра те­куще­го вре­мени, с ко­торым мы поз­на­коми­лись в пре­дыду­щем раз­де­ле. Он ра­бота­ет по про­токо­лу TCP и соз­да­ет но­вый по­ток для каж­до­го зап­ро­са.

require «socket»

 

PORT = 12321

 

server = TCPServer.new(PORT)

 

while (session = server.accept)

 Thread.new(session) do |my_session|

my_session.puts Time.new

  my_session.close

 end

end

Мно­гопо­точ­ность поз­во­ля­ет дос­тичь вы­соко­го па­рал­ле­лиз­ма. Вы­зывать ме­тод join не нуж­но, пос­коль­ку сер­вер ис­полня­ет бес­ко­неч­ный цикл, по­ка его не ос­та­новят вруч­ную.

Код кли­ен­та, ко­неч­но, ос­тался тем же са­мым. С точ­ки зре­ния кли­ен­та, по­веде­ние сер­ве­ра не из­ме­нилось (раз­ве что он стал бо­лее на­деж­ным).

18.1.3. При­мер: сер­вер для иг­ры в шах­ма­ты по се­ти

Не всег­да на­шей ко­неч­ной целью яв­ля­ет­ся вза­имо­дей­ствие с са­мим сер­ве­ром. Иног­да сер­вер — все­го лишь средс­тво для со­еди­нения кли­ен­тов друг с дру­гом. В ка­чес­тве при­мера мож­но при­вес­ти фай­ло­об­менные се­ти, столь по­пуляр­ные в 2001 го­ду. Дру­гой при­мер — сер­ве­ры для мгно­вен­ной пе­реда­чи со­об­ще­ний, нап­ри­мер ICQ, и раз­но­го ро­да иг­ро­вые сер­ве­ры.

Да­вай­те на­пишем ске­лет шах­матно­го сер­ве­ра. Мы не име­ем в ви­ду прог­рамму, ко­торая бу­дет иг­рать в шах­ма­ты с кли­ен­том. Нет, на­ша за­дача — свя­зать кли­ен­тов так, что­бы они мог­ли за­тем иг­рать без вме­шатель­ства сер­ве­ра.

Пре­дуп­реждаю, что ра­ди прос­то­ты по­казан­ная ни­же прог­рамма ни­чего не зна­ет о шах­ма­тах. Ло­гика иг­ры прос­то заг­лу­шена, что­бы мож­но бы­ло сос­ре­дото­чить­ся на се­тевых ас­пектах.

Для ус­та­нов­ле­ния со­еди­нения меж­ду кли­ен­том и сер­ве­ром бу­дем ис­поль­зо­вать про­токол TCP. Мож­но бы­ло бы ос­та­новить­ся и на UDP, но этот про­токол не­наде­жен, и нам приш­лось бы ис­поль­зо­вать тайм-а­уты, как в од­ном из при­меров вы­ше.

Кли­ент мо­жет пе­редать два по­ля: свое имя и имя же­латель­но­го про­тив­ни­ка. Для иден­ти­фика­ции про­тив­ни­ка ус­ло­вим­ся за­писы­вать его имя в ви­де user:hostname; мы упот­ре­били дво­ето­чие вмес­то нап­ра­шива­юще­гося зна­ка @, что­бы не вы­зывать ас­со­ци­аций с элек­трон­ным ад­ре­сом, ка­ковым эта стро­ка не яв­ля­ет­ся.

Ког­да от кли­ен­та при­ходит зап­рос, сер­вер сох­ра­ня­ет све­дения о кли­ен­те у се­бя в спис­ке. Ес­ли пос­ту­пили зап­ро­сы от обо­их кли­ен­тов, сер­вер по­сыла­ет каж­до­му из них со­об­ще­ние; те­перь у каж­до­го кли­ен­та дос­та­точ­но ин­форма­ции для ус­та­нов­ле­ния свя­зи с про­тив­ни­ком.

Есть еще воп­рос о вы­боре цве­та фи­гур. Оба пар­тне­ра дол­жны как-то до­гово­рить­ся о том, кто ка­ким цве­том бу­дет иг­рать. Для прос­то­ты пред­по­ложим, что цвет наз­на­ча­ет сер­вер. Пер­вый об­ра­тив­ший­ся кли­ент бу­дет иг­рать бе­лыми (и, ста­ло быть, хо­дить пер­вым), вто­рой — чер­ны­ми.

Уточ­ним: компь­юте­ры, ко­торые пер­во­началь­но бы­ли кли­ен­та­ми, на­чиная с это­го мо­мен­та об­ща­ют­ся друг с дру­гом нап­ря­мую; сле­дова­тель­но, один из них ста­новит­ся сер­ве­ром. Но на эту се­ман­ти­чес­кую тон­кость я не бу­ду об­ра­щать вни­мания.

Пос­коль­ку кли­ен­ты по­сыла­ют зап­ро­сы и от­ве­ты по­пере­мен­но, при­чем се­анс свя­зи вклю­ча­ет мно­го та­ких об­ме­нов, бу­дем поль­зо­вать­ся про­токо­лом TCP. Сле­дова­тель­но, кли­ент, ко­торый на са­мом де­ле иг­ра­ет роль «сер­ве­ра», соз­да­ет объ­ект TCPServer, а кли­ент на дру­гом кон­це — объ­ект TCPSocket. Бу­дем пред­по­лагать, что но­мер пор­та для об­ме­на дан­ны­ми за­ранее из­вестен обо­им пар­тне­рам (ра­зуме­ет­ся, У каж­до­го из них свой но­мер пор­та).

Мы толь­ко что опи­сали прос­той про­токол прик­ладно­го уров­ня. Его мож­но бы­ло бы сде­лать и бо­лее хит­ро­ум­ным.

Сна­чала рас­смот­рим код сер­ве­ра (лис­тинг 18.1). Что­бы его бы­ло про­ще за­пус­кать из ко­ман­дной стро­ки, соз­да­дим по­ток, ко­торый за­вер­шит сер­вер при на­жатии кла­виши Enter. Сер­вер мно­гопо­точ­ный — он мо­жет од­новре­мен­но об­слу­живать нес­коль­ких кли­ен­тов. Дан­ные о поль­зо­вате­лях за­щище­ны мь­ютек­сом, ведь те­оре­тичес­ки нес­коль­ко по­токов мо­гут од­новре­мен­но по­пытать­ся до­бавить но­вую за­пись в спи­сок.

Лис­тинг 18.1. Шах­матный сер­вер

require «thread»

require «socket»

 

PORT = 12000

HOST = «96.97.98.99»              # За­менить этот IP-ад­рес.

 

# Вы­ход при на­жатии кла­виши Enter.

waiter = Thread.new do

 puts «Наж­ми­те Enter для за­вер­ше­ния сер­ве­ра.»

gets

 exit

end

 

$mutex = Mutex.new

$list = {}

 

def match?(p1, p2)

 return false if !$list[p1] or !$list[p2]

 

 if ($list[p1][0] == p2 and $list[p2][0] == p1)

  true

else

  false

 end

end

 

def handle_client(sess, msg, addr, port, ipname)

 $mutex.synchronize do

  cmd, player1, player2 = msg.split

  # При­меча­ние: от кли­ен­та мы по­луча­ем дан­ные в ви­де user:hostname,

# но хра­ним их в ви­де user:address.

  p1short = player1.dup           # Ко­рот­кие име­на

  p2short = player2.split(«:»)[0] # (то есть не «:address»).

player1 << «:#{addr}»           # До­бавить IP-ад­рес кли­ен­та.

 

  user2, host2 = player2.split(«:»)

  host2 = ipname if host2 == nil

  player2 = user2 + «:» + IPSocket.getaddress(host2)

 

if cmd != «login»

   puts «Ошиб­ка про­токо­ла: кли­ент пос­лал со­об­ще­ние #{msg}.»

  end

 

  $list[player1] = [player2, addr, port, ipname, sess]

 

if match?(player1, player2)

   # Име­на те­перь пе­рес­тавле­ны: ес­ли мы по­пали сю­да, зна­чит

   # player2 за­регис­три­ровал­ся пер­вым.

p1 = $list[player1]

   р2 = $list[player2]

   # ID иг­ро­ка = name:ipname:color

# Цвет: 0=бе­лый, 1=чер­ный

   p1id = «#{p1short}:#{p1[3]}:1»

   p2id = «#{p2short}:#{p2[3]}:0»

sess1 = p1[4]

   sess2 = p2[4]

   sess1.puts «#{p2id}»

sess2.puts «#{p1id}»

   sess1.close

   sess2.close

end

 end

end

 

text = nil

 

$server = TCPServer.new(HOST, PORT)

while session = $server.accept do

 Thread.new(session) do |sess|

  text = sess.gets

puts «По­луче­но: #{text}» # Что­бы знать, что сер­вер по­лучил.

  domain, port, ipname, ipaddr = sess.peeraddr

  handle_client sess, text, ipaddr, port, ipname

sleep 1

 end

end

 

waiter.join                # Вы­ходим, ког­да бы­ла на­жата кла­виша Enter.

Ме­тод handle_client сох­ра­ня­ет ин­форма­цию о кли­ен­те. Ес­ли за­пись о та­ком кли­ен­те уже су­щес­тву­ет, то каж­до­му кли­ен­ту по­сыла­ет­ся со­об­ще­ние о том, где на­ходит­ся дру­гой пар­тнер. Этим обя­зан­ности сер­ве­ра ис­черпы­ва­ют­ся.

Кли­ент (лис­тинг 18.2) офор­млен в ви­де единс­твен­ной прог­раммы. При пер­вом за­пус­ке она ста­новит­ся TCP-сер­ве­ром, а при вто­ром — TCP-кли­ен­том. Чес­тно го­воря, ре­шение о том, что сер­вер бу­дет иг­рать бе­лыми, со­вер­шенно про­из­воль­но. Впол­не мож­но бы­ло бы ре­али­зовать при­ложе­ние так, что­бы цвет не за­висел от по­доб­ных де­талей.

Лис­тинг 18.2. Шах­матный кли­ент

require «socket»

require «timeout»

 

ChessServer = ‘96.97.98.99’ # За­менить этот IP-ад­рес.

ChessServerPort = 12000

PeerPort = 12001

 

WHITE, BLACK = 0, 1

Colors = %w[White Black]

 

def draw_board(board)

 puts <<-EOF

+——————————+

| Заг­лушка! Шах­матная дос­ка… |

+——————————+

 EOF

end

 

def analyze_move(who, move, num, board)

 # Заг­лушка — чер­ные всег­да вы­иг­ры­ва­ют на чет­вертом хо­ду.

if who == BLACK and num == 4

  move << » Мат!»

 end

true # Еще од­на заг­лушка — лю­бой ход счи­та­ет­ся до­пус­ти­мым.

end

 

def my_move(who, lastmove, num, board, sock)

 ok = false

 until ok do

print «\nВаш ход: »

  move = STDIN.gets.chomp

  ok = analyze_move(who, move, num, board)

puts «Не­допус­ти­мый ход» if not ok

 end

  sock.puts move

move

end

 

def other_move(who, move, num, board, sock)

 move = sock.gets.chomp

 puts «\nПро­тив­ник: #{move}»

move

end

 

if ARGV[0]

 myself = ARGV[0]

else

print «Ва­ше имя? »

 myself = STDIN.gets.chomp

end

 

if ARGV[1]

 opponent_id = ARGV[1]

else

print «Ваш про­тив­ник? »

 opponent_id = STDIN.gets.chomp

end

 

opponent = opponent_id.split(«:»)[0] # Уда­лить имя хос­та.

# Об­ра­тить­ся к сер­ве­ру

 

socket = TCPSocket.new(ChessServer, ChessServerPort)

 

response = nil

 

socket.puts «login # {myself} #{opponent_id}»

socket.flush

response = socket.gets.chomp

 

name, ipname, color = response.split «:»

color = color.to_i

 

if color == BLACK   # Цвет фи­гур дру­гого иг­ро­ка,

 puts «\nУс­та­нав­ли­ва­ет­ся со­еди­нение…»

 server = TCPServer.new(PeerPort)

session = server.accept

 str = nil

 begin

timeout(30) do

   str = session.gets.chomp

   if str != «ready»

raise «Ошиб­ка про­токо­ла: по­луче­но со­об­ще­ние о го­тов­ности #{str}.»

   end

  end

rescue TimeoutError

  raise «He по­луче­но со­об­ще­ние о го­тов­ности от про­тив­ни­ка.»

 end

 

 puts «Ваш про­тив­ник #{opponent}… у вас бе­лые.\n»

 

who = WHITE

 move = nil

 board = nil        # В этом при­мере не ис­поль­зу­ет­ся.

num = 0

 draw_board(board)  # На­рисо­вать на­чаль­ное по­ложе­ние для бе­лых.

 loop do

num += 1

  move = my_move(who, move, num, board, session)

  draw_board(board)

case move

   when «resign»

    puts «\nВы сда­лись. #{opponent} вы­иг­рал.»

break

  when /Checkmate/

    puts «\nВы пос­та­вили мат #{opponent}!»

draw_board(board)

    break

  end

move = other_move(who, move, num, board, session)

  draw_board(board)

  case move

when «resign»

    puts «\n#{opponent} сдал­ся… вы вы­иг­ра­ли!»

    break

when /Checkmate/

    puts «\n#{opponent} пос­та­вил вам мат.»

    break

end

 end

else                # Мы иг­ра­ем чер­ны­ми,

puts «\nУс­та­нав­ли­ва­ет­ся со­еди­нение…»

 

 socket = TCPSocket.new(ipname, PeerPort)

 socket.puts «ready»

 

 puts «Ваш про­тив­ник #{opponent}… у вас чер­ные.\n»

 

 who = BLACK

 move = nil

board = nil        # В этом при­мере не ис­поль­зу­ет­ся.

 num = 0

 draw_board(board)  # На­рисо­вать на­чаль­ное по­ложе­ние.

 

 loop do

  num += 1

move = other_move(who, move, num, board, socket)

  draw_board(board) # На­рисо­вать дос­ку пос­ле хо­да бе­лых,

  case move

when «resign»

    puts «\n#{opponent} сдал­ся… вы вы­иг­ра­ли!»

    break

when /Checkmate/

    puts «\n#{opponent} пос­та­вил вам мат.»

    break

end

  move = my_move(who, move, num, board, socket)

  draw_board(board)

case move

   when «resign»

    puts «\nВы сда­лись. #{opponent} вы­иг­рал.»

break

   when /Checkmate/

    puts «\n#{opponent} пос­та­вил вам мат.»

break

  end

 end

socket.close

end

Я оп­ре­делил этот про­токол так, что чер­ные по­сыла­ют бе­лым со­об­ще­ние «ready», что­бы пар­тнер знал о го­тов­ности на­чать иг­ру. За­тем бе­лые де­ла­ют пер­вый ход. Ход по­сыла­ет­ся чер­ным, что­бы кли­ент мог на­рисо­вать та­кую же по­зицию на дос­ке, как у дру­гого иг­ро­ка.

Пов­то­рю, при­ложе­ние ни­чего не зна­ет о шах­ма­тах. Вмес­то про­вер­ки до­пус­ти­мос­ти хо­да встав­ле­на заг­лушка; про­вер­ка вы­пол­ня­ет­ся ло­каль­но, то есть на той сто­роне, где де­ла­ет­ся ход. Ни­какой ре­аль­ной про­вер­ки нет — заг­лушка всег­да го­ворит, что ход до­пус­тим. Кро­ме то­го, мы хо­тим, что­бы ими­тация иг­ры за­вер­ша­лась пос­ле нес­коль­ких хо­дов, по­это­му мы на­писа­ли прог­рамму так, что чер­ные всег­да вы­иг­ры­ва­ют на чет­вертом хо­ду. По­беда обоз­на­ча­ет­ся стро­кой «Checkmate!» в кон­це хо­да. Эта стро­ка пе­чата­ет­ся на эк­ра­не со­пер­ни­ка и слу­жит приз­на­ком вы­хода из цик­ла.

По­мимо «тра­дици­он­ной» шах­матной но­тации (нап­ри­мер, «P-K4») су­щес­тву­ет еще «ал­гебра­ичес­кая», ко­торую мно­гие пред­по­чита­ют. Но на­писан­ный код во­об­ще не име­ет пред­став­ле­ния о том, ка­кой но­таци­ей мы поль­зу­ем­ся.

Пос­коль­ку это бы­ло нес­ложно сде­лать, мы поз­во­ля­ем иг­ро­ку в лю­бой мо­мент сдать­ся. Ри­сова­ние дос­ки то­же заг­лу­шено. Же­ла­ющие мо­гут ре­али­зовать гру­бый ри­сунок, вы­пол­ненный ASCII-сим­во­лами.

Ме­тод my_move всег­да от­но­сит­ся к ло­каль­но­му кон­цу, ме­тод other_move — к уда­лен­но­му.

В лис­тинге 18.3 при­веден про­токол се­ан­са. Дей­ствия кли­ен­тов на­рисо­ваны друг про­тив дру­га.

Лис­тинг 18.3. Про­токол се­ан­са шах­матной иг­ры

% ruby chess.rb Hal                      % ruby chess.rb

Capablanca:deepthought.org               Hal:deepdoodoo.org

 

Ус­та­нав­ли­ва­ет­ся со­еди­нение…            Ус­та­нав­ли­ва­ет­ся со­еди­нение…

Ваш про­тив­ник Capablanca… у вас бе­лые. Ваш про­тив­ник Hal… у вас чер­ные.

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Ваш ход: N-QB3                           Про­тив­ник: N-QB3

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Про­тив­ник: P-K4                          Ваш ход: P-K4

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Ваш ход: P-K4                            Про­тив­ник: P-K4

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Про­тив­ник: B-QB4                         Ваш ход: B-QB4

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Ваш ход: B-QB4                           Про­тив­ник: B-QB4

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… +

+——————————+         +——————————+

 

Про­тив­ник: Q-KR5                         Ваш ход: Q-KR5

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Ваш ход: N-KB3                           Про­тив­ник: N-KB3

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Про­тив­ник: QxP Checkmate!                Ваш ход: QxP

+——————————+         +——————————+

| Заг­лушка! Шах­матная дос­ка… |         | Заг­лушка! Шах­матная дос­ка… |

+——————————+         +——————————+

 

Capablanca пос­та­вил вам мат.             Вы пос­та­вили мат Hal!

18.2. Се­тевые кли­ен­ты

Иног­да сер­вер поль­зу­ет­ся хо­рошо из­вес­тным про­токо­лом — тог­да нам на­до лишь спро­ек­ти­ровать кли­ен­та, ко­торый об­ща­ет­ся с сер­ве­ром на по­нят­ном то­му язы­ке.

В раз­де­ле 18.1 мы ви­дели, что это мож­но сде­лать с по­мощью про­токо­лов TCP или UDP. Но ча­ще при­меня­ют­ся про­токо­лы бо­лее вы­соко­го уров­ня, нап­ри­мер HTTP или SNMP. Рас­смот­рим нес­коль­ко при­меров.

18.2.1. По­луче­ние ис­тинно слу­чай­ных чи­сел из Web

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

Джон фон Ней­ман

В мо­дуле Kernel есть фун­кция rand, ко­торая воз­вра­ща­ет слу­чай­ное чис­ло, но вот бе­да — чис­ло-то не яв­ля­ет­ся ис­тинно слу­чай­ным. Ес­ли вы ма­тема­тик, крип­тограф или еще ка­кой-ни­будь пе­дант, то на­зове­те эту фун­кцию ге­нера­тором псев­дослу­чай­ных чи­сел, пос­коль­ку она поль­зу­ет­ся ал­гебра­ичес­ки­ми ме­тода­ми для де­тер­ми­ниро­ван­но­го по­рож­де­ния пос­ле­дова­тель­нос­ти чи­сел. Сто­рон­не­му наб­лю­дате­лю эти чис­ла пред­став­ля­ют­ся слу­чай­ны­ми и да­же об­ла­да­ют не­об­хо­димы­ми ста­тис­ти­чес­ки­ми свой­ства­ми, но ра­но или поз­дно пос­ле­дова­тель­ность нач­нет пов­то­рять­ся. Мы мо­жем да­же на­мерен­но (или слу­чай­но) пов­то­рить ее, за­дав ту же са­мую зат­равку.

Но при­род­ные про­цес­сы счи­та­ют­ся ис­тинно слу­чай­ны­ми. По­это­му при ро­зыг­ры­ше при­зов в ло­терее счас­тлив­чи­ки оп­ре­деля­ют­ся ло­тот­ро­ном, ко­торый ха­отич­но выб­ра­сыва­ет ша­ры. Дру­гие ис­точни­ки слу­чай­нос­ти — ра­ди­оак­тивный рас­пад или ат­мосфер­ный шум.

Есть ис­точни­ки слу­чай­ных чи­сел и в Web. Один из них — сайт www.random.org, ко­торый мы за­дей­ству­ем в сле­ду­ющем при­мере.

Прог­рамма в лис­тинге 18.4 ими­тиру­ет под­бра­сыва­ние пя­ти обыч­ных (шес­тигран­ных) кос­тей. Ко­неч­но, иг­ро­вые фа­наты мог­ли бы уве­личить чис­ло гра­ней до 10 или 20, но тог­да ста­ло бы слож­но ри­совать ASCII-кар­тинки.

Лис­тинг 18.4. Слу­чай­ное бро­сание кос­тей

require ‘net/http’

 

HOST = «www.random.org»

RAND_URL = «/cgi-bin/randnum?col=5&»

 

def get_random_numbers(count=1, min=0, max=99)

 path = RAND_URL + «num=#{count}&min=#{min}&max=#{max}»

connection = Net::HTTP.new(HOST)

 response, data = connection.get(path)

 if response.code == «200»

data.split.collect { |num| num.to_i }

 else

  []

end

end

 

DICE_LINES = [

 «+——+ +——+ +——+ +——+ +——+ +——+ «,

 «|     | |  *  | |  *  | | * * | | * * | | * * | «,

«|  *  | |     | |  *  | |     | |  *  | | * * | «,

 «|     | |  *  | |  *  | | * * | | * * | | * * | «,

 «+——+ +——+ +——+ +——+ +——+ +——+ »

 

DIE_WIDTH = DICE_LINES[0].length/6

 

def draw_dice(values)

 DICE_LINES.each do | line |

  for v in values

print line[(v-1)*DIE_WIDTH, DIE_WIDTH]

   print » «

  end

puts

 end

end

 

draw_dice(get_random_numbers(5, 1, 6))

Здесь мы вос­поль­зо­вались клас­сом Net::НТТР для пря­мого вза­имо­дей­ствия с Web-сер­ве­ром. Счи­тай­те, что эта прог­рамма — уз­коспе­ци­али­зиро­ван­ный бра­узер. Мы фор­ми­ру­ем URL и пы­та­ем­ся ус­та­новить со­еди­нение; ког­да оно бу­дет ус­та­нов­ле­но, мы по­луча­ем от­вет, воз­можно, со­дер­жа­щий не­кие дан­ные. Ес­ли код от­ве­та по­казы­ва­ет, что оши­бок не бы­ло, то мож­но ра­зоб­рать по­лучен­ные дан­ные. Пред­по­лага­ет­ся, что ис­клю­чения бу­дут об­ра­бота­ны вы­зыва­ющей прог­раммой.

Пос­мотрим на ва­ри­ацию этой идеи. Что ес­ли вы за­хоте­ли бы при­менить слу­чай­ные чис­ла в ка­ком-ни­будь при­ложе­нии? Пос­коль­ку об­слу­жива­ющая прог­рамма на сто­роне сер­ве­ра поз­во­ля­ет ука­зать ко­личес­тво воз­вра­ща­емых чи­сел, то бы­ло бы ло­гич­но сох­ра­нить их в бу­фере. Учи­тывая, что при об­ра­щении к уда­лен­но­му сер­ве­ру за­дер­жки не­из­бежны, сле­ду­ет сра­зу за­пол­нить бу­фер во из­бе­жание лиш­них зап­ро­сов по се­ти.

В лис­тинге 18.5 эта мысль ре­али­зова­на. Бу­фер за­пол­ня­ет­ся от­дель­ным по­током и сов­мес­тно ис­поль­зу­ет­ся все­ми эк­зем­пля­рами клас­са. Раз­мер бу­фера и «ниж­няя от­метка» (@slack) нас­тра­ива­ют­ся; ка­кие зна­чения за­дать в ре­аль­ной прог­рамме, за­висит от ве­личи­ны за­дер­жки при об­ра­щении к сер­ве­ру и от то­го, как час­то при­ложе­ние вы­бира­ет слу­чай­ное чис­ло из бу­фера.

Лис­тинг 18.5. Ге­нера­тор слу­чай­ных чи­сел с бу­фери­заци­ей

require «net/http»

require «thread»

 

class TrueRandom

 

def initialize(min=nil,max=nil,buff=nil,slack=nil)

  @buffer = []

  @site = «www.random.org»

if ! defined? @init_flag

   # При­нять умол­ча­ния, ес­ли они не бы­ли за­даны яв­но И

   # это пер­вый соз­данный эк­зем­пляр клас­са…

@min = min || 0

   @max = max || 1

   @bufsize = buff || 1000

@slacksize = slack || 300

   @mutex = Mutex.new

   @thread = Thread.new { fillbuffer }

@init_flag = TRUE # Зна­чение мо­жет быть лю­бым.

  else

   @min = min || @min

@max = max || @max

   @bufsize = buff || @bufsize

   @slacksize = slack || @slacksize

end

  @url = «/cgi-bin/randnum» +

   «?num=#@bufsize&min=#@min&max=#@max&col=1»

end

 

 def fillbuffer

  h = Net::HTTP.new(@site, 80)

resp, data = h.get(@url, nil)

  @buffer += data.split

 end

 

 def rand

  num = nil

@mutex.synchronize { num = @buffer.shift }

  if @buffer.size < @slacksize

   if ! @thread.alive?

@thread = Thread.new { fillbuffer }

   end

  end

if num == nil

   if @thread.alive?

    @thread.join

else

    @thread = Thread.new { fillbuffer }

    @thread.join

end

   @mutex.synchronize { num = @buffer.shift }

  end

num.to_i

 end

 

end

 

t = TrueRandom.new(1,6,1000,300)

 

count = {1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0}

 

10000.times do |n|

 x = t.rand

 count[x] += 1

end

 

p count

 

# При од­ном про­гоне:

# {4=>1692, 5=>1677, 1=>1678, 6=>1635, 2=>1626, 3=>1692}

18.2.2. Зап­рос к офи­ци­аль­но­му сер­ве­ру вре­мени

Как мы и обе­щали, при­ведем прог­рамму для об­ра­щения к NTP-сер­ве­ру в се­ти (NTP — Network Time Protocol (син­хро­низи­ру­ющий се­тевой про­токол). По­казан­ный ни­же код за­имс­тво­ван с не­боль­шой пе­рера­бот­кой у Дэй­ва То­маса.

require «net/telnet»

timeserver = «www.fakedomain.org»

 

local = Time.now.strftime(«%H:%M:%S»)

tn = Net::Telnet.new(«Host» => timeserver,

 «Port» => «time»,

«Timeout» => 60,

 «Telnetmode» => false)

msg = tn.recv(4).unpack(‘N’)[0]

# Пре­об­ра­зовать сме­щение от точ­ки от­сче­та

remote = Time.at(msg — 2208988800).strftime(«%H:%M:%S»)

 

puts «Мес­тное : #{local}»

puts «Уда­лен­ное : #{remote}»

Мы ус­та­нав­ли­ва­ем со­еди­нение и по­луча­ем че­тыре бай­та. Они пред­став­ля­ют 32-раз­рядное чис­ло в се­тевом (ту­поко­неч­ном) по­ряд­ке бай­тов. Это чис­ло пре­об­ра­зу­ет­ся в по­нят­ную фор­му, а за­тем — из сме­щения от точ­ки от­сче­та в объ­ект Time.

Мы не ука­зали имя ре­аль­но­го сер­ве­ра. Де­ло в том, что его по­лез­ность час­то за­висит от то­го, где вы на­ходи­тесь. Кро­ме то­го, мно­гие сер­ве­ры ог­ра­ничи­ва­ют дос­туп, так что для зап­ро­са вы дол­жны по­лучить раз­ре­шение или хо­тя бы уве­домить вла­дель­ца. По­ис­ко­вая ма­шина по­может най­ти от­кры­тый NTP-сер­вер в ра­ди­усе 1000 км от вас.

18.2.3. Вза­имо­дей­ствие с РОР-сер­ве­ром

Мно­гие сер­ве­ры элек­трон­ной поч­ты поль­зу­ют­ся поч­то­вым про­токо­лом (Post Office Protocol — POP). Име­ющий­ся в Ruby класс POP3 поз­во­ля­ет прос­матри­вать за­голов­ки и те­ла всех со­об­ще­ний, хра­нящих­ся для вас на сер­ве­ре, и об­ра­баты­вать их как вы соч­те­те нуж­ным. Пос­ле об­ра­бот­ки со­об­ще­ния мож­но уда­лить.

Для соз­да­ния объ­ек­та клас­са Net::POP3 нуж­но ука­зать до­мен­ное имя или IP-ад­рес сер­ве­ра; но­мер пор­та по умол­ча­нию ра­вен 110. Со­еди­нение ус­та­нав­ли­ва­ет­ся толь­ко пос­ле вы­зова ме­тода start (ко­торо­му пе­реда­ет­ся имя и па­роль поль­зо­вате­ля).

Вы­зов ме­тода mails соз­данно­го объ­ек­та воз­вра­ща­ет мас­сив объ­ек­тов клас­са POPMail. (Име­ет­ся так­же ите­ратор each для пе­ребо­ра этих объ­ек­тов.)

Объ­ект POPMail со­от­ветс­тву­ет од­но­му поч­то­вому со­об­ще­нию. Ме­тод header по­луча­ет за­голов­ки со­об­ще­ния, а ме­тод all — за­голов­ки и те­ло (у ме­тода all, как мы вско­ре уви­дим, есть и дру­гие при­мене­ния).

Фраг­мент ко­да сто­ит ты­сячи слов. Вот при­мер об­ра­щения к сер­ве­ру с пос­ле ду­ющей рас­пе­чат­кой те­мы каж­до­го со­об­ще­ния:

require «net/pop»

 

pop = Net::POP3.new(«pop.fakedomain.org»)

pop.start(«gandalf», «mellon») # Имя и па­роль поль­зо­вате­ля.

pop.mails.each do |msg|

 puts msg.header.grep /^Subject: /

end

Ме­тод delete уда­ля­ет со­об­ще­ние с сер­ве­ра. (Не­кото­рые сер­ве­ры тре­бу­ют, что­бы POP-со­еди­нение бы­ло зак­ры­то ме­тодом finish, толь­ко тог­да ре­зуль­тат уда­ления ста­новит­ся не­об­ра­тимым.) Вот прос­тей­ший при­мер филь­тра спа­ма:

require «net/pop»

 

pop = Net::POP3.new(«pop.fakedomain.org»)

pop.start(«gandalf», «mellon») # Имя и па­роль поль­зо­вате­ля.

pop.mails.each do |msg|

 if msg.all =~ /.*make money fast.*/

msg.delete

 end

end

pop.finish

От­ме­тим, что при вы­зове ме­тода start мож­но так­же за­давать блок. По ана­логии с ме­тодом File.open в этом слу­чае от­кры­ва­ет­ся со­еди­нение, ис­полня­ет­ся блок, а за­тем со­еди­нение зак­ры­ва­ет­ся.

Ме­тод all так­же мож­но вы­зывать с бло­ком. В бло­ке прос­то пе­реби­ра­ют­ся все стро­ки со­об­ще­ния, как ес­ли бы мы выз­ва­ли ите­ратор each для стро­ки, воз­вра­щен­ной ме­тодом all.

# На­печа­тать все стро­ки в об­ратном по­ряд­ке… по­лез­ная шту­ка!

msg.all { |line| print line.reverse }

# To же са­мое…

msg.all.each { |line| print line.reverse }

Ме­тоду all мож­но так­же пе­редать объ­ект. В та­ком слу­чае для каж­дой строч­ки (line) в по­лучен­ной стро­ке (string) бу­дет выз­ван опе­ратор кон­ка­тена­ции (<<). Пос­коль­ку в раз­личных объ­ек­тах он мо­жет быть оп­ре­делен по-раз­но­му, в ре­зуль­та­те та­кого об­ра­щения воз­можны са­мые раз­ные дей­ствия:

arr = []       # Пус­той мас­сив.

str = «Mail: » # String.

out = $stdout  # Объ­ект IO.

 

msg.all(arr)   # Пос­тро­ить мас­сив стро­чек.

msg.all(str)   # Кон­ка­тени­ровать с str.

msg.all(out)   # Вы­вес­ти на stdout.

На­конец, по­кажем еще, как вер­нуть толь­ко те­ло со­об­ще­ния, иг­но­рируя все за­голов­ки.

module Net

 

 class POPMail

 

  def body

   # Про­пус­тить бай­ты за­голов­ка

self.all[self.header.size..-1]

  end

 

end

 

end

Ес­ли вы пред­по­чита­ете про­токол IMAP, а не POP3, об­ра­титесь к раз­де­лу 18.2.5

18.2.4. От­прав­ка поч­ты по про­токо­лу SMTP

Это по­нял бы и пя­тилет­ний ре­бенок. Дай­те мне пя­тилет­не­го ре­бен­ка.

Гро­учо Маркс

Наз­ва­ние «прос­той про­токол элек­трон­ной поч­ты» (Simple Mail Transfer Protocol — SMTP) не впол­не пра­виль­но. Ес­ли он и «прос­той», то толь­ко по срав­не­нию с бо­лее слож­ны­ми про­токо­лами.

Ко­неч­но, биб­ли­оте­ка smtp.rb скры­ва­ет от прог­раммис­та боль­шую часть де­талей про­токо­ла. Но, на наш взгляд, эта биб­ли­оте­ка ин­ту­итив­но не впол­не оче­вид­на и, по­жалуй, слиш­ком слож­на (на­де­ем­ся, что в бу­дущем это из­ме­нит­ся). В этом раз­де­ле мы при­ведем нес­коль­ко при­меров, что­бы по­мочь вам ос­во­ить­ся.

В клас­се Net::SMTP есть два ме­тода клас­са: new и start. Ме­тод new при­нима­ет два па­рамет­ра: имя сер­ве­ра (по умол­ча­нию localhost) и но­мер пор­та (по умол­ча­нию 25).

Ме­тод start при­нима­ет сле­ду­ющие па­рамет­ры:

• server — до­мен­ное имя или IP-ад­рес SMTP-сер­ве­ра; по умол­ча­нию это «localhost»;

• port — но­мер пор­та, по умол­ча­нию 25;

• domain — до­мен­ное имя от­пра­вите­ля, по умол­ча­нию ENV[«HOSTNAME»];

• account — имя поль­зо­вате­ля, по умол­ча­нию nil;

• password — па­роль, по умол­ча­нию nil;

• authtype — тип ав­то­риза­ции, по умол­ча­нию :cram_md5.

Обыч­но боль­шую часть этих па­рамет­ров мож­но не за­давать.

Ес­ли ме­тод start вы­зыва­ет­ся «нор­маль­но» (без бло­ка), то он воз­вра­ща­ет объ­ект клас­са SMTP. Ес­ли же блок за­дан, то этот объ­ект пе­реда­ет­ся пря­мо в блок.

У объ­ек­та SMTP есть ме­тод эк­зем­пля­ра sendmail, ко­торый обыч­но и за­нима­ет­ся все­ми де­таля­ми от­прав­ки со­об­ще­ния. Он при­нима­ет три па­рамет­ра:

• source — стро­ка или мас­сив (или лю­бой объ­ект, у ко­торо­го есть ите­ратор each, воз­вра­ща­ющий на каж­дой ите­рации од­ну стро­ку);

• sender — стро­ка, за­писы­ва­емая в по­ле «from» со­об­ще­ния;

• recipients — стро­ка или мас­сив строк, опи­сыва­ющие од­но­го или нес­коль­ких по­луча­телей.

Вот при­мер от­прав­ки со­об­ще­ния с по­мощью ме­тодов клас­са:

require ‘net/smtp’

 

msg = <<EOF

Subject: Раз­ное

… приш­ла по­ра

По­думать о де­лах:

О баш­ма­ках, о сур­гу­че,

Ка­пус­те, ко­ролях.

И по­чему, как суп в кот­ле,

Ки­пит во­да в мо­рях.

EOF

 

Net::SMTP.start(«smtp-server.fake.com») do |smtp|

 smtp.sendmail msg, ‘walrus@fake1.com’, ‘alice@fake2.com’

end

Пос­коль­ку в на­чале стро­ки на­ходит­ся сло­во Subject:, то по­луча­тель со­об­ще­ния уви­дит те­му Раз­ное.

Име­ет­ся так­же ме­тод эк­зем­пля­ра start, ко­торый ве­дет се­бя прак­ти­чес­ки так же, как ме­тод клас­са. Пос­коль­ку поч­то­вый сер­вер оп­ре­делен в ме­тоде new, то за­давать его еще и в ме­тоде start не нуж­но. По­это­му этот па­раметр про­пус­ка­ет­ся, а ос­таль­ные не от­ли­ча­ют­ся от па­рамет­ров, пе­реда­ва­емых ме­тоду клас­са. Сле­дова­тель­но, со­об­ще­ние мож­но пос­лать и с по­мощью объ­ек­та SMTP:

require ‘net/smtp’

 

msg = <<EOF

Subject: Яс­но и ло­гич­но

«С дру­гой сто­роны, — до­бавил Та­рарам, —

ес­ли все так и бы­ло, то все имен­но так и бы­ло.

Ес­ли же все бы­ло бы так, то все не мог­ло бы быть

не так. Но пос­коль­ку все бы­ло не сов­сем так, все

бы­ло со­вер­шенно не так. Яс­но и ло­гич­но!»

EOF

 

smtp = Net::SMTP.new(«smtp-server.fake.com»)

smtp.start

smtp.sendmail msg, ‘tweedledee@fake1.com’, ‘alice@fake2.com’

Ес­ли вы еще не за­пута­лись, до­бавим, что ме­тод эк­зем­пля­ра мо­жет при­нимать ещё и блок:

require ‘net/smtp’

 

msg = <<EOF

Subject: Мо­би Дик

Зо­вите ме­ня Из­ма­ил.

EOF

 

addressees = [‘readerl@fake2.com’, ‘reader2@fake3.com’]

smtp = Net::SMTP.new(«smtp-server.fake.com»)

smtp.start do |obj|

 obj.sendmail msg, ‘narrator@fake1.com’, addressees

end

Как вид­но из при­мера, объ­ект, пе­редан­ный в блок (obj), не обя­зан на­зывать­ся так же, как объ­ект, от име­ни ко­торо­го вы­зыва­ет­ся ме­тод (smtp). Кро­ме то­го, хо­чу под­чер­кнуть: нес­коль­ко по­луча­телей мож­но пред­ста­вить в ви­де мас­си­ва строк.

Су­щес­тву­ет еще ме­тод эк­зем­пля­ра со стран­ным наз­ва­ни­ем ready. Он по­хож на sendmail, но есть и важ­ные раз­ли­чия. За­да­ют­ся толь­ко от­пра­витель и по­луча­тели, те­ло же со­об­ще­ния конс­тру­иру­ет­ся с по­мощью объ­ек­та adapter клас­са Net::NetPrivate::WriteAdapter, у ко­торо­го есть ме­тоды write и append. Адап­тер пе­реда­ет­ся в блок, где мо­жет ис­поль­зо­вать­ся про­из­воль­ным об­ра­зом[17]:

require «net/smtp»

 

smtp = Net::SMTP.new(«smtp-server.fake1.com»)

 

smtp.start

 

smtp.ready(«t.s.eliot@fake1.com», «reader@fake2.com») do |obj|

 obj.write «Пош­ли вдво­ем, по­жалуй.\r\n»

 obj.write «Уж ве­чер не­бо нав­зничью рас­пя­ло\r\n»

 obj.write «Как па­ци­ен­та под но­жом нар­коз… \r\n»

end

От­ме­тим, что па­ры сим­во­лов «воз­врат ка­рет­ки», «пе­ревод стро­ки» обя­затель­ны (ес­ли вы хо­тите раз­бить со­об­ще­ние на строч­ки). Чи­тате­ли, зна­комые с де­таля­ми про­токо­ла, об­ра­тят вни­мание на то, что со­об­ще­ние «за­вер­ша­ет­ся» (до­бав­ля­ет­ся точ­ка и сло­во «QUIT») без на­шего учас­тия.

Мож­но вмес­то ме­тода write вос­поль­зо­вать­ся опе­рато­ром кон­ка­тена­ции:

smtp.ready(«t.s.eliot@fake1.com», «reader@fake2.com») do |obj|

 obj << «В гос­ти­ной раз­го­вари­ва­ют те­ти\r\n»

 obj << «О Ми­келан­дже­ло Бу­она­рот­ти.\r\n»

end

И еще од­но не­боль­шое усо­вер­шенс­тво­вание: мы до­бавим ме­тод puts, ко­торый вста­вит в со­об­ще­ние сим­во­лы пе­рехо­да на но­вую стро­ку:

class Net::NetPrivate::WriteAdapter

 def puts(args)

  args << «\r\n»

self.write(*args)

 end

end

Но­вый ме­тод поз­во­ля­ет фор­ми­ровать со­об­ще­ние и так:

smtp.ready(«t.s.eliot@fake1.com», «reader@fake2.com») do |obj|

 obj.puts «Мы бы­ли приз­ва­ны в глу­хую глу­бину,»

obj.puts «В мир дев мор­ских, в вол­шебную стра­ну,»

 obj.puts «Но нас ок­ликну­ли — и мы пош­ли ко дну.»

end

Ес­ли все­го из­ло­жен­но­го вам не хва­та­ет, по­эк­спе­римен­ти­руй­те са­мос­то­ятель­но. А ес­ли со­бере­тесь на­писать но­вый ин­терфейс к про­токо­лу SMTP, не стес­няй­тесь.

18.2.5. Вза­имо­дей­ствие с IMAP-сер­ве­ром

Про­токол IMAP нель­зя наз­вать вер­ши­ной со­вер­шенс­тва, но во мно­гих от­но­шени­ях он пре­вос­хо­дит POP3. Со­об­ще­ния мо­гут хра­нить­ся на сер­ве­ре сколь угод­но дол­го (с ин­ди­виду­аль­ны­ми по­мет­ка­ми «про­чита­но» и «не про­чита­но»). Для хра­нения со­об­ще­ний мож­но ор­га­низо­вать и­ерар­хию па­пок. Этих воз­можнос­тей уже дос­та­точ­но для то­го, что­бы счи­тать про­токол IMAP бо­лее раз­ви­тым, чем POP3.

Для вза­имо­дей­ствия с IMAP-сер­ве­ром пред­назна­чена стан­дар­тная биб­ли­оте­ка net/imap. Ес­тес­твен­но, вы дол­жны сна­чала ус­та­новить со­еди­нение с сер­ве­ром, а за­тем иден­ти­фици­ровать се­бя с по­мощью име­ни и па­роля:

require ‘net/imap’

 

host = «imap.hogwarts.edu»

user, pass = «lupin», «riddikulus»

 

imap = Net::IMAP.new(host)

begin

 imap.login(user, pass)

# Или ина­че:

 # imap.authenticate(«LOGIN», user, pass)

rescue Net::IMAP::NoResponseError

abort «He уда­лось а­утен­ти­фици­ровать поль­зо­вате­ля #{user}»

end

 

# Про­дол­жа­ем ра­боту…

 

imap.logout # Ра­зор­вать со­еди­нение.

Ус­та­новив со­еди­нение, мож­но про­верить поч­то­вый ящик ме­тодом examine; по умол­ча­нию поч­то­вый ящик в IMAP на­зыва­ет­ся INBOX. Ме­тод responses воз­вра­ща­ет ин­форма­цию из поч­то­вого ящи­ка в ви­де хэ­ша мас­си­вов (на­ибо­лее ин­те­рес­ные дан­ные на­ходят­ся в пос­леднем эле­мен­те мас­си­ва). По­казан­ный ни­же код по­казы­ва­ет об­щее чис­ло со­об­ще­ний в поч­то­вом ящи­ке («EXISTS») и чис­ло неп­ро­читан­ных со­об­ще­ний («RESENT»):

imap.examine(«INBOX»)

total = imap.responses[«EXISTS»].last  # Все­го со­об­ще­ний.

recent = imap.responses[«RECENT»].last # Неп­ро­читан­ных со­об­ще­ний.

imap.close                             # Зак­рыть поч­то­вый ящик.

От­ме­тим, что ме­тод examine поз­во­ля­ет толь­ко чи­тать со­дер­жи­мое поч­то­вого ящи­ка. Ес­ли нуж­но уда­лить со­об­ще­ния или про­из­вести ка­кие-то дру­гие из­ме­нения, поль­зуй­тесь ме­тодом select.

Поч­то­вые ящи­ки в про­токо­ле IMAP ор­га­низо­ваны и­ерар­хи­чес­ки, как име­на пу­тей в UNIX. Для ма­нипу­лиро­вания поч­то­выми ящи­ками пре­дус­мотре­ны ме­тоды create, delete и rename:

imap.create(«lists»)

imap.create(«lists/ruby»)

imap.create(«lists/rails»)

imap.create(«lists/foobar»)

 

# Унич­то­жить пос­ледний соз­данный ящик:

imap.delete(«lists/foobar»)

Име­ют­ся так­же ме­тоды list (по­лучить спи­сок всех поч­то­вых ящи­ков) и lsub (по­лучить спи­сок «ак­тивных» ящи­ков, на ко­торые вы «под­пи­сались»). Ме­тод status воз­вра­ща­ет ин­форма­цию о сос­то­янии ящи­ка.

Ме­тод search на­ходит со­об­ще­ния, удов­летво­ря­ющие за­дан­но­му кри­терию, а ме­тод fetch воз­вра­ща­ет зап­ро­шен­ное со­об­ще­ние:

msgs = imap.search(«ТО»,»lupin»)

msgs.each do |mid|

 env = imap.fetch(mid, «ENVELOPE»)[0].attr[«ENVELOPE»]

puts «От #{env.from[0].name} #{env.subject}»

end

Ко­ман­да fetch в пре­дыду­щем при­мере выг­ля­дит так слож­но, по­тому что воз­вра­ща­ет мас­сив хэ­шей. Сам кон­верт то­же пред­став­ля­ет со­бой слож­ную струк­ту­ру; не­кото­рые ме­тоды дос­ту­па к не­му воз­вра­ща­ют сос­тавные объ­ек­ты, дру­гие — прос­то стро­ки.

В про­токо­ле IMAP есть по­нятия UID (уни­каль­но­го иден­ти­фика­тора) и по­ряд­ко­вого но­мера со­об­ще­ния. Обыч­но ме­тоды ти­па fetch об­ра­ща­ют­ся к со­об­ще­ни­ям по но­мерам, но есть и ва­ри­ан­ты (нап­ри­мер, uid_fetch) для об­ра­щения по UID. У нас нет мес­та объ­яс­нять, по­чему нуж­ны обе сис­те­мы иден­ти­фика­ции, но ес­ли вы со­бира­етесь серь­ез­но ра­ботать с IMAP, то дол­жны по­нимать раз­ли­чие меж­ду ни­ми (и ни­ког­да не пу­тать од­ну с дру­гой).

Биб­ли­оте­ка net/imap рас­по­лага­ет раз­но­об­разны­ми средс­тва­ми для ра­боты с поч­то­выми ящи­ками, со­об­ще­ни­ями, вло­жени­ями и т.д. До­пол­ни­тель­ную ин­форма­цию по­ищи­те в он­лай­но­вой до­кумен­та­ции на сай­те ruby-doc.org.

18.2.6. Ко­диро­вание и де­коди­рова­ние вло­жений

Для вло­жения в поч­то­вое со­об­ще­ние или в со­об­ще­ние, от­прав­ля­емое в кон­фе­рен­цию, файл обыч­но ко­диру­ет­ся. Как пра­вило, при­меня­ет­ся ко­диров­ка base64, для ра­боты с ко­торой слу­жит ме­тод pack с ар­гу­мен­том m:

bin = File.read(«new.gif»)

str = [bin].pack(«m»)     # str за­коди­рова­на.

 

orig = str.unpack(«m»)[0] # orig == bin

Ста­рые поч­то­вые кли­ен­ты ра­бота­ли с ко­диров­кой uuencode/uudecode. В этом слу­чае вло­жение прос­то до­бав­ля­ет­ся в ко­нец тек­ста со­об­ще­ния и ог­ра­ничи­ва­ет­ся стро­ками begin и end, при­чем в стро­ке begin ука­зыва­ют­ся так­же раз­ре­шения на дос­туп к фай­лу (ко­торые мож­но и про­иг­но­риро­вать) и имя фай­ла. Ар­гу­мент u ме­тода pack поз­во­ля­ет пред­ста­вить стро­ку в ко­диров­ке uuencode. При­мер:

# Пред­по­ложим, что mailtext со­дер­жит текст со­об­ще­ния.

 

filename = «new.gif»

bin = File.read(filename)

encoded = [bin].pack(«u»)

 

mailtext << «begin 644 #{filename}»

mailtext << encoded

mailtext << «end»

# …

На при­нима­ющей сто­роне мы дол­жны из­влечь за­коди­рован­ную ин­форма­цию и де­коди­ровать ее ме­тодом unpack:

# …

# Пред­по­ложим, что ‘attached’ со­дер­жит за­коди­рован­ные дан­ные

# (вклю­чая стро­ки begin и end).

 

lines = attached.split(«\n»)

filename = /begin \d\d\d (.*)/.scan(lines[0]).first.first

encoded = lines[1..-2].join(«\n»)

decoded = encoded.unpack(«u») # Все го­тово к за­писи в файл.

Сов­ре­мен­ные поч­то­вые кли­ен­ты ра­бота­ют с поч­той в фор­ма­те MIME; да­же тек­сто­вая часть со­об­ще­ния обер­ну­та в кон­верт (хо­тя кли­ент уда­ля­ет все за­голов­ки, преж­де чем по­казать со­об­ще­ние поль­зо­вате­лю).

Под­робное рас­смот­ре­ние фор­ма­та MIME за­няло бы слиш­ком мно­го мес­та, да и не от­но­сит­ся к рас­смат­ри­ва­емой те­ме. Но в сле­ду­ющем прос­том при­мере по­каза­но, как мож­но за­коди­ровать и от­пра­вить со­об­ще­ние, со­дер­жа­щее тек­сто­вую часть и дво­ич­ное вло­жение. Дво­ич­ные кус­ки обыч­но пред­став­ле­ны в ко­диров­ке base64:

require ‘net/smtp’

 

def text_plus_attachment(subject, body, filename)

 marker = «MIME_boundary»

middle = «—#{marker}\n»

 ending = «—#{middle}—\n»

 content = «Content-Type: Multipart/Related; » +

«boundary=#{marker}; » +

  «typw=text/plain»

 head1 = <<-EOF

MIME-Version: 1.0

#{content}

Subject: #{subject}

EOF

 binary = File.read(filename)

 encoded = [binary].pack(«m») # base64

head2 = <<EOF

Content-Description: «#{filename}»

Content-Type: image/gif; name=»#{filename}»

Content-Transfer-Encoding: Base64

Content-Disposition: attachment; filename=»#{filename}»

 EOF

 

 # Воз­вра­ща­ем…

 head1 + middle + body + middle + head2 + encoded + ending

end

 

domain = «someserver.com»

smtp = «smtp.#{domain}»

user, pass = «elgar»,»enigma»

 

body = <<EOF

Это мое со­об­ще­ние. Осо­бо

го­ворить не о чем. Я вло­жил

не­боль­шой GIF-файл.

 

          — Боб

EOF

mailtext = text_plus_attachment(«При­вет…»,body,»new.gif»)

 

Net::SMTP.start(smtp, 25, domain, user, pass, :plain) do |mailer|

 mailer.sendmail(mailtext, ‘fromthisguy@wherever.com’,

  [‘destination@elsewhere.com’])

end

18.2.7. При­мер: шлюз меж­ду поч­той и кон­фе­рен­ци­ями

В он­лай­но­вых со­об­щес­твах об­ще­ние про­ис­хо­дит раз­ны­ми спо­соба­ми. К на­ибо­лее рас­простра­нен­ным от­но­сят­ся спис­ки рас­сылки и кон­фе­рен­ции (но­вос­тные груп­пы).

Но не каж­дый хо­чет под­пи­сывать­ся на спи­сок рас­сылки и ежед­невно по­лучать де­сят­ки со­об­ще­ний; кто-то пред­по­чита­ет вре­мя от вре­мени за­ходить в кон­фе­рен­цию и прос­матри­вать но­вые со­об­ще­ния. С дру­гой сто­роны, есть лю­ди, ко­торым сис­те­ма Usenet ка­жет­ся слиш­ком мед­ли­тель­ной — они хо­тели бы ви­деть со­об­ще­ние, по­ка еще элек­тро­ны не ус­пе­ли ос­тыть.

Та­ким об­ра­зом, мы име­ем си­ту­ацию, ког­да в срав­ни­тель­но не­боль­шом зак­ры­том спис­ке рас­сылки рас­смат­ри­ва­ют­ся те же те­мы, что в не­моде­риру­емой кон­фе­рен­ции, от­кры­той все­му ми­ру. В кон­це кон­цов ко­му-то приш­ла в го­лову мысль ор­га­низо­вать зер­ка­ло — шлюз меж­ду обе­ими сис­те­мами.

По­доб­ный шлюз под­хо­дит не к лю­бой си­ту­ации, но в слу­чае спис­ка рас­сылки Ruby он впол­не го­дит­ся. Со­об­ще­ния из кон­фе­рен­ции нуж­но ско­пиро­вать в спи­сок, а со­об­ще­ния, от­прав­ля­емые в спи­сок рас­сылки, нап­ра­вить так­же и кон­фе­рен­цию.

Эта за­дача бы­ла ре­шена Дэй­вом То­масом (Dave Thomas) — ко­неч­но, на Ruby, — и с его лю­без­но­го раз­ре­шения мы при­водим код в лис­тингах 18.6 и 18.7.

Но сна­чала не­боль­шое вступ­ле­ние. Мы уже нем­но­го поз­на­коми­лись с тем, как от­прав­лять и по­лучать элек­трон­ную поч­ту, но как быть с кон­фе­рен­ци­ями Usenet? Дос­туп к кон­фе­рен­ци­ям обес­пе­чива­ет про­токол NNTP (Network News Transfer Protocol — се­тевой про­токол пе­реда­чи но­вос­тей). Кста­ти, соз­дал его Лар­ри У­олл (Larry Wall), ко­торый поз­же по­дарил нам язык Perl.

В Ruby нет «стан­дар­тной» биб­ли­оте­ки для ра­боты с NNTP. Од­на­ко один япон­ский прог­раммист (из­вес­тный нам толь­ко по псев­до­ниму greentea) на­писал прек­расную биб­ли­оте­ку для этой це­ли.

В биб­ли­оте­ке nntp.rb оп­ре­делен мо­дуль NNTP, со­дер­жа­щий класс NNTPIO. В этом клас­се име­ют­ся, в час­тнос­ти, ме­тоды эк­зем­пля­ра connect, get_head, get_body и post. Что­бы по­лучить со­об­ще­ния, не­об­хо­димо ус­та­новить со­еди­нение с сер­ве­ром и в цик­ле вы­зывать ме­тоды get_head и get_body (мы, прав­да, нем­но­го уп­ро­ща­ем). Что­бы от­пра­вить со­об­ще­ние, нуж­но сконс­тру­иро­вать его за­голов­ки, со­еди­нить­ся с сер­ве­ром и выз­вать ме­тод post.

В при­веден­ных ни­же прог­раммах ис­поль­зу­ет­ся биб­ли­оте­ка smtp, с ко­торой мы уже поз­на­коми­лись. В ори­гиналь­ной вер­сии ко­да про­из­во­дит­ся так­же про­токо­лиро­вание хо­да про­цес­са и оши­бок, но для прос­то­ты мы эти фраг­менты опус­ти­ли.

Файл params.rb ну­жен обе­им прог­раммам. В нем опи­саны па­рамет­ры, уп­равля­ющие всем про­цес­сом зер­ка­лиро­вания: име­на сер­ве­ров, име­на поль­зо­вате­лей и т.д. Ни­же при­веден при­мер, ко­торый вы мо­жете из­ме­нить са­мос­то­ятель­но. (Все до­мен­ные име­на, со­дер­жа­щие сло­во «fake», оче­вид­но, фик­тивные.)

# Раз­личные па­рамет­ры, не­об­хо­димые шлю­зу меж­ду поч­той и кон­фе­рен­ци­ями.

 

module Params

 NEWS_SERVER = «usenet.fake1.org»      # Имя но­вос­тно­го сер­ве­ра.

NEWSGROUP = «comp.lang.ruby»          # Зер­ка­лиру­емая кон­фе­рен­ция.

 LOOP_FLAG = «X-rubymirror: yes»       # Что­бы из­бе­жать цик­лов.

 LAST_NEWS_FILE = «/tmp/m2n/last_news» # Но­мер пос­ледне­го про­читан­но­го

# со­об­ще­ния.

 SMTP_SERVER = «localhost»             # Имя хос­та для ис­хо­дящей поч­ты.

 

MAIL_SENDER = «myself@fake2.org»      # От чь­его име­ни по­сылать поч­ту.

 # (Для спис­ков, на ко­торые под­пи­сыва­ют­ся, это дол­жно быть имя

 # за­регис­три­рован­но­го учас­тни­ка спис­ка.)

 

 mailing_list = «list@fake3.org»       # Ад­рес спис­ка рас­сылки.

end

Мо­дуль Params со­дер­жит лишь кон­стан­ты, нуж­ные обе­им прог­раммам. Боль­шая их часть не нуж­да­ет­ся в объ­яс­не­ни­ях, упо­мянем лишь па­роч­ку. Во-пер­вых, кон­стан­та LAST_NEWS_FILE со­дер­жит путь к фай­лу, в ко­тором хра­нит­ся иден­ти­фика­тор пос­ледне­го про­читан­но­го из кон­фе­рен­ции со­об­ще­ния; эта «ин­форма­ция о сос­то­янии» поз­во­ля­ет из­бе­жать дуб­ли­рова­ния или про­пус­ка со­об­ще­ний.

Кон­стан­та LOOP_FLAG оп­ре­деля­ет стро­ку, ко­торой по­меча­ют­ся со­об­ще­ния, уже про­шед­шие че­рез шлюз. Тем са­мым мы пре­пятс­тву­ем воз­никно­вению бес­ко­неч­ной

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

Воз­ни­ка­ет воп­рос: «А как во­об­ще поч­та пос­ту­па­ет в прог­рамму mail2news?» Ведь она, по­хоже, чи­та­ет из стан­дар­тно­го вво­да. Ав­тор ре­комен­ду­ет сле­ду­ющую нас­трой­ку: сна­чала в фай­ле .forward прог­раммы sendmail вся вхо­дящая поч­та пе­ренап­равля­ет­ся на прог­рамму procmail. Файл .procmail кон­фи­гури­ру­ет­ся так, что­бы из­вле­кать со­об­ще­ния, при­ходя­щие из спис­ка рас­сылки, и по кон­вей­еру нап­равлять их прог­рамме mail2news. Уточ­нить де­тали мож­но в до­кумен­та­ции, соп­ро­вож­да­ющей при­ложе­ние RubyMirror (в ар­хи­ве RAA). Ес­ли вы ра­бота­ете не в UNIX, то при­дет­ся изоб­рести собс­твен­ную схе­му кон­фи­гури­рова­ния.

Ну а все ос­таль­ное рас­ска­жет сам код, при­веден­ный в лис­тингах 18.6 и 18.7.

Лис­тинг 18.6. Пе­ренап­равле­ние поч­ты в кон­фе­рен­цию

# mail2news: При­нима­ет поч­то­вое со­об­ще­ние и от­прав­ля­ет

# его в кон­фе­рен­цию.

 

require «nntp»

include NNTP

 

require «params»

 

# Про­читать со­об­ще­ние, вы­делив из не­го за­голо­вок и те­ло.

# Про­пус­ка­ют­ся толь­ко оп­ре­делен­ные за­голов­ки.

 

HEADERS = %w{From Subject References Message-ID

 Content-Type Content-Transfer-Encoding Date}

 

allowed_headers = Regexp.new(%{^(#{HEADERS.join(«|»)}):})

 

# Про­читать за­голо­вок. До­пус­ка­ют­ся толь­ко не­кото­рые за­голов­ки.

# До­бавить стро­ки Newsgroups и X-rubymirror.

 

head = «Newsgroups: #{Params::NEWSGROUP}\n»

subject = «unknown»

while line = gets

 exit if line /^#{Params::LOOP_FLAG}/о # Та­кого не дол­жно быть!

 break if line =~ /^s*$/

next if line =~ /^\s/

next unless line =~ allowed_headers

 

 # Вы­резать пре­фикс [ruby-talk:nnnn] из те­мы, преж­де чем

 # от­прав­лять в кон­фе­рен­цию.

 if line =~ /^Subject:\s*(.*)/

  subject = $1

 

  # Сле­ду­ющий код вы­реза­ет спе­ци­аль­ный но­мер ruby-talk

  # из на­чала со­об­ще­ния в спис­ке рас­сылки, пе­ред тем

# как от­прав­лять его но­вос­тно­му сер­ве­ру.

  line.sub!(/\[ruby-talk:(\d+)\]\s*/, »)

  subject = «[#$1] #{line}»

head << «X-ruby-talk: #$1\n»

 end

 head << line

end

 

head << «#{Params::LOOP_FLAG}\n»

 

body = «»

while line = gets

body << line

end

 

msg = head + «\n» + body

msg.gsub!(/\r?\n/, «\r\n»)

 

nntp = NNTPIO.new(Params::NEWS_SERVER)

raise «Failed to connect» unless nntp.connect

nntp.post(msg)

Лис­тинг 18.7. Пе­ренап­равле­ние кон­фе­рен­ции в поч­ту

##

# Прос­той сце­нарий для зер­ка­лиро­вания тра­фика

# из кон­фе­рен­ции comp.lang.ruby в спи­сок рас­сылки ruby-talk.

#

# Вы­зыва­ет­ся пе­ри­оди­чес­ки (ска­жем, каж­дые 20 ми­нут).

# Зап­ра­шива­ет у но­вос­тно­го сер­ве­ра все со­об­ще­ния с но­мером,

# боль­шим но­мера пос­ледне­го со­об­ще­ния, по­лучен­но­го

# в прош­лый раз. Ес­ли та­ковые есть, то чи­та­ет со­об­ще­ния,

# от­прав­ля­ет их в спи­сок рас­сылки и за­поми­на­ет но­мер пос­ледне­го.

 

require ‘nntp’

require ‘net/smtp’

require ‘params’

 

include NNTP

 

##

# # От­пра­вить со­об­ще­ния в спи­сок рас­сылки. Со­об­ще­ние дол­жно

# быть от­прав­ле­но учас­тни­ком спис­ка, хо­тя в стро­ке From:

# мо­жет сто­ять лю­бой до­пус­ти­мый ад­рес.

#

 

def send_mail(head, body)

 smtp = Net::SMTP.new

smtp.start(Params::SMTP_SERVER)

 smtp.ready(Params::MAIL_SENDER, Params::MAILING_LIST) do |a|

  a.write head

a.write «#{Params::LOOP_FLAG}\r\n»

  a.write «\r\n»

  a.write body

end

end

 

##

# За­поми­на­ем иден­ти­фика­тор пос­ледне­го про­читан­но­го из кон­фе­рен­ции

# со­об­ще­ния.

 

begin

 last_news = File.open(Params::LAST_NEWS_FILE) {|f| f.read}.to_i

rescue

 last_news = nil

end

 

##

# Со­еди­ня­ем­ся с но­вос­тным сер­ве­ром и по­луча­ем но­мера со­об­ще­ний

# из кон­фе­рен­ции comp.lang.ruby.

#

nntp = NNTPIО.new(Params::NEWS_SERVER)

raise «Failed to connect» unless nntp.connect

count, first, last = nntp.set_group(Params::NEWSGROUP)

 

##

# Ес­ли но­мер пос­ледне­го со­об­ще­ния не был за­пом­нен рань­ше,

# сде­ла­ем это сей­час.

 

if not last_news

 last_news = last

end

 

##

# Пе­рей­ти к пос­ледне­му про­читан­но­му ра­нее со­об­ще­нию

# и по­пытать­ся по­лучить сле­ду­ющие за ним. Это мо­жет при­вес­ти

# к ис­клю­чению, ес­ли со­об­ще­ния с ука­зан­ным но­мером

# не су­щес­тву­ет, но мы не об­ра­ща­ем на это вни­мания.

begin

 nntp.set_stat(last_news)

rescue

end

 

##

# Чи­та­ем все име­ющи­еся со­об­ще­ния и от­прав­ля­ем каж­дое

# в спи­сок рас­сылки.

new_last = last_news

begin

 loop do

  nntp.set_next

head = «»

  body = «»

  new_last, = nntp.get_head do |line|

head << line

  end

 

# He по­сылать со­об­ще­ния, ко­торые прог­рамма mail2news

  # уже от­прав­ля­ла в кон­фе­рен­цию ра­нее (ина­че за­цик­лимся).

  next if head =~ %r{^X-rubymirror:}

 

  nntp.get_body do |line|

   body << line

end

 

  send_mail(head, body)

 end

rescue

end

 

##

#И за­писать в файл но­вую от­метку.

 

File.open(Params::LAST_NEWS_FILE, «w») do |f|

 f.puts new_last

end unless new_last == last_news

18.2.8. По­луче­ние Web-стра­ницы с из­вес­тным URL

Пусть нам нуж­но по­лучить HTML-до­кумент из Web. Воз­можно, вы хо­тите про­верить кон­троль­ную сум­му и уз­нать, не из­ме­нил­ся ли до­кумент, что­бы пос­лать ав­то­мати­чес­кое уве­дом­ле­ние. А быть мо­жет, вы пи­шете собс­твен­ный бра­узер — тог­да это пер­вый шаг на пу­ти дли­ной в ты­сячу ки­ломет­ров.

require «net/http»

 

begin

 h = Net::HTTP.new(«www.marsdrive.com», 80) # MarsDrive Consortium

resp, data = h.get(«/index.html», nil)

rescue => err

 puts «Ошиб­ка: #{err}»

exit

end

 

puts «По­луче­но #{data.split.size} строк, #{data.size} бай­тов»

# Об­ра­ботать…

Сна­чала мы соз­да­ем объ­ект клас­са HTTP, ука­зывая до­мен­ное имя и но­мер пор­та сер­ве­ра (обыч­но ис­поль­зу­ет­ся порт 80). За­тем вы­пол­ня­ет­ся опе­рация get, ко­торая воз­вра­ща­ет от­вет по про­токо­лу HTTP и вмес­те с ним стро­ку дан­ных. В при­мере вы­ше мы не про­веря­ем от­вет, но ес­ли воз­никла ошиб­ка, то пе­рех­ва­тыва­ем ее и вы­ходим.

Ес­ли мы бла­гопо­луч­но ми­нова­ли пред­ло­жение rescue, то мо­жем ожи­дать, что со­дер­жи­мое стра­ницы на­ходит­ся в стро­ке data. Мы мо­жем об­ра­ботать ее как соч­тем нуж­ным.

Что мо­жет пой­ти не так, ка­кие ошиб­ки мы пе­рех­ва­тыва­ем? Нес­коль­ко. Мо­жет не су­щес­тво­вать или быть не­дос­тупным сер­вер с ука­зан­ным име­нем; ука­зан­ный ад­рес мо­жет быть пе­ренап­равлен на дру­гую стра­ницу (эту си­ту­ацию мы не об­ра­баты­ва­ем); мо­жет быть воз­вра­щена прес­ло­вутая ошиб­ка 404 (ука­зан­ный до­кумент не най­ден). Об­ра­бот­ку по­доб­ных оши­бок мы ос­тавля­ем вам.

Сле­ду­ющий раз­дел ока­жет­ся в этом смыс­ле по­лез­ным. В нем мы пред­ста­вим нес­коль­ко бо­лее прос­той спо­соб ре­шения дан­ной за­дачи.

18.2.9. Биб­ли­оте­ка Open-URI

Биб­ли­оте­ку Open-URI на­писал Та­нака Аки­ра (Tanaka Akira). Ее цель — уни­фици­ровать ра­боту с се­тевы­ми ре­сур­са­ми из прог­раммы, пре­дос­та­вив ин­ту­итив­но оче­вид­ный и прос­той ин­терфейс.

По су­щес­тву она яв­ля­ет­ся обер­ткой вок­руг биб­ли­отек net/http, net/https и net/ftp и пре­дос­тавля­ет ме­тод open, ко­торо­му мож­но пе­редать про­из­воль­ный URI. При­мер из пре­дыду­щего раз­де­ла мож­но бы­ло бы пе­репи­сать сле­ду­ющим об­ра­зом:

require ‘open-uri’

 

data = nil

open(«http://www.marsdrive.com/») {|f| data = f.read }

 

puts «По­луче­но #{data.split.size} строк, #{data.size} бай­тов»

Объ­ект, воз­вра­ща­емый ме­тодом open (f в при­мере вы­ше), — не прос­то файл. У не­го есть так­же ме­тоды из мо­дуля OpenURI::Meta, по­это­му мы мо­жем по­лучить ме­тадан­ные:

uri = f.base_uri        # Объ­ект URI с собс­твен­ны­ми ме­тода­ми дос­ту­па.

ct = f.content_type     # «text/html»

cs = f.charset          # «utf-8»

ce = f.content_encoding # []

Биб­ли­оте­ка поз­во­ля­ет за­дать и до­пол­ни­тель­ные за­голо­воч­ные по­ля, пе­реда­вая ме­тоду open хэш. Она так­же спо­соб­на ра­ботать че­рез прок­си-сер­ве­ры и об­ла­да­ет ря­дом дру­гих по­лез­ных фун­кций. В не­кото­рых слу­ча­ях этой биб­ли­оте­ки не­дос­та­точ­но (нап­ри­мер, ес­ли не­об­хо­димо раз­би­рать за­голов­ки HTTP, бу­фери­зовать очень боль­шой ска­чива­емый файл, от­прав­лять ку­ки и т.д.). До­пол­ни­тель­ную ин­форма­цию мож­но най­ти в он­лай­но­вой до­кумен­та­ции на сай­те http://ruby-doc.org.

18.3. Зак­лю­чение

Эта гла­ва пред­став­ля­ет со­бой вве­дение в се­тевое прог­рамми­рова­ние на низ­ком уров­не. В час­тнос­ти, при­веде­ны прос­тые при­меры сер­ве­ров и кли­ен­тов. Мы ви­дели, как на­писать кли­ент для су­щес­тву­юще­го сер­ве­ра, соз­данно­го не на­ми.

Мы рас­смот­ре­ли так­же про­токо­лы бо­лее вы­соко­го уров­ня, нап­ри­мер POP и IMAP для по­луче­ния поч­ты. Ана­логич­но мы го­вори­ли о про­токо­ле от­прав­ки поч­ты SMTP. По­пут­но был про­демонс­три­рован спо­соб ко­диро­вания и де­коди­рова­ния вло­жений в поч­то­вые со­об­ще­ния. В кон­тек­сте раз­ра­бот­ки шлю­за меж­ду спис­ком рас­сылки и кон­фе­рен­ци­ями мы упо­мяну­ли о про­токо­ле NNTP.

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

Автор публикации

не в сети 2 дня

Paul Maul

ПРОГОНЫ САЙТОВ - быстро и недорого!
Обращайтесь: http://wb-master.ru/uslugi/seo.html - Прогоны

Комментарии: 1Публикации: 19967Регистрация: 20-02-2017

Добавить комментарий

Войти с помощью: 

Ваш e-mail не будет опубликован. Обязательные поля помечены *

восемнадцать − 9 =

БЕСПЛАТНО!
Получить бонус