Skip to content

11. Мережеві функції Python

Тепер ми розглянемо мережеві функції Python, які дозволяють нам здійснювати програмування TCP / IP (Transfer Control Protocol / Internet Protocol).

  

11.1. Отримання імені або адреси IP комп’ютера в Інтернеті


Програма (inet_01)


import sys, socket

#------------------------------------------------
def getIPandName(nomMachine):
    #nomMachine : ім'я комп'ютера, адресу якого потрібно отримати IP
    # nomMachine-->адреса IP
    try:
        ip=socket.gethostbyname(nomMachine)
        print "ip[%s]=%s" % (nomMachine,ip)
    except socket.error, erreur:
        print "ip[%s]=%s" % (nomMachine,erreur)
        return

    # адреса IP --> nomMachine
    try:
        name=socket.gethostbyaddr(ip)
        print "name[%s]=%s" % (ip,name[0])
    except socket.error, erreur:
        print "name[%s]=%s" % (ip,erreur)
        return


# ---------------------------------------- main

# константи
HOTES=["istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","localhost","","xx"]

# адреси IP машин з HOTES
for i in range(len(HOTES)):
    getIPandName(HOTES[i])
# кінець
sys.exit()

Примітки:

  • рядок 1: мережеві функції Python інкапсульовані в модулі socket.

Результати

1
2
3
4
5
6
7
8
9
ip[istia.univ-angers.fr]=193.49.146.171
name[193.49.146.171]=istia.istia.univ-angers.fr
ip[www.univ-angers.fr]=193.49.144.40
name[193.49.144.40]=ametys-fo.univ-angers.fr
ip[www.ibm.com]=129.42.58.216
name[129.42.58.216]=[Errno 11004] host not found
ip[localhost]=127.0.0.1
name[127.0.0.1]=Gportpers3.ad.univ-angers.fr
ip[xx]=[Errno 11004] getaddrinfo failed

11.2. Веб-клієнт

Скрипт, що дозволяє отримати вміст вступного абзацу веб-сайту.


Програма (inet_02)


import sys,socket

  #-----------------------------------------------------------------------
def getIndex(site):
    # зчитує сайт URL та зберігає його у файлі site.html
  
    # спочатку помилок немає
    erreur=""
    # створення файлу site.html
    try:
        html=open("%s.html" % (site),"w")
    except IOError, erreur:
        pass
    # помилка?
    if erreur:
        return "Erreur (%s) lors de la création du fichier %s.html" % (erreur,site)

    # відкриття з'єднання на порту 80 сайту
    try:
        connexion=socket.create_connection((site,80))
    except socket.error, erreur:
        pass
    # повернення у разі помилки
    if erreur:
        return "Echec de la connexion au site (%s,80) : %s" % (site,erreur)

    # з'єднання являє собою двосторонній канал зв'язку
    # між клієнтом (цією програмою) та веб-сервером, до якого звертаються
    # цей канал використовується для обміну командами та інформацією
    # протокол взаємодії — HTTP

    # клієнт надсилає команду get для запиту URL /
    # синтаксис get: URL HTTP/1.0
    # заголовки (headers) протоколу HTTP повинні закінчуватися порожнім рядком
    connexion.send("GET / HTTP/1.0\n\n")

    # сервер тепер відповість на каналі з’єднання. Він надішле всі
    # свої дані, а потім закриє канал. Клієнт зчитує все, що надходить по каналу з’єднання
    # до закриття каналу
    ligne=connexion.recv(1000)
    while(ligne):
        html.write(ligne)
        ligne=connexion.recv(1000)

    # у свою чергу клієнт закриває з’єднання
    connexion.close()
    # закриття HTML-файлу
    html.close()
    # повернення
    return "Transfert reussi du paragraphe index du site %s" % (site)

# --------------------------------------------- main    

# отримати текст HTML з URL

# список веб-сайтів
SITES=("istia.univ-angers.fr","www.univ-angers.fr","www.ibm.com","xx")

# читання індексних сторінок сайтів з таблиці SITES
for i in range(len(SITES)):
      # читання індексних сторінок сайту SITES[i]
      resultat=getIndex(SITES[i])
      # виведення результату
      print resultat

# кінець
sys.exit()

Примітки:

  • рядок 57: список URL-адрес веб-сайтів, з яких потрібно отримати головну сторінку. Цей список зберігається у текстовому файлі [nomsite.html];
  • рядок 62: функція getIndex виконує цю роботу;
  • рядок 4: функція getIndex;
  • рядок 20: метод create_connection((site,port)) дозволяє встановити з’єднання зі службою TCP / IP, що працює на порту port комп’ютера site;
  • рядок 35: метод send дозволяє надсилати дані через з’єднання TCP / IP. У цьому випадку надсилається текст. Цей текст відповідає протоколу HTTP (HyperText Transfer Protocol);
  • рядок 40: метод recv дозволяє отримувати дані через з’єднання TCP / IP. Тут відповідь веб-сервера зчитується блоками по 1000 символів і записується у текстовий файл [nomsite.html].

Результати

Transfert reussi du paragraphe index du site istia.univ-angers.fr
Transfert reussi du paragraphe index du site www.univ-angers.fr
Transfert reussi du paragraphe index du site www.ibm.com
Echec de la connexion au site (xx,80) : [Errno 11001] getaddrinfo failed

Файл, отриманий для сайту [www.ibm.com]:

HTTP/1.1 302 Found
Date: Wed, 08 Jun 2011 15:43:56 GMT
Server: IBM_HTTP_Server
Content-Type: text/html
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache, must-revalidate
Location: http://www.ibm.com/us/en/
Content-Length: 209
Kp-eeAlive: timeout=10, max=14
Connection: Keep-Alive

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ibm.com/us/en/">here</a>.</p>
</body></html>
  • рядки 1–11 — це заголовки HTTP у відповіді сервера;
  • рядок 1: сервер просить клієнта перенаправитися на URL, вказаний у рядку 8;
  • рядок 2: дата та час відповіді;
  • рядок 3: ідентифікатор веб-сервера;
  • рядок 4: вміст, надісланий сервером. У даному випадку це сторінка HTML, яка починається з рядка 13;
  • рядок 12: порожній рядок, що завершує заголовки HTTP;
  • рядки 13–19: сторінка HTML, надіслана веб-сервером.

11.3. SMTP-клієнт

Серед протоколів TCP / IP, SMTP (SendMail Transfer Protocol) є протоколом зв’язку служби відправлення повідомлень.


Програма (inet_03)


# -*- coding=utf-8 -*-

import sys,socket

#-----------------------------------------------------------------------
def getInfos(fichier):
    # повертає інформацію (SMTP, відправник, одержувач, повідомлення), взяту з текстового файлу [fichier]
    # рядок 1: smtp, відправник, одержувач
    # наступні рядки: текст повідомлення

    # відкриття [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # читання першого рядка
    ligne=infos.readline()
    # видалення позначки кінця рядка
    ligne=cutNewLineChar(ligne)
    # витяг полів smtp, відправник, одержувач
    champs=ligne.split(",")
    # чи правильна кількість полів?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # «обробка» отриманої інформації
    # видалення з кожного з 3 полів «пробілів», що передують або йдуть після корисної інформації
    for i in range(3):
        champs[i]=champs[i].strip()
    # витяг полів
    (smtpServer,expediteur,destinataire)=champs
    message=""
    # читання решти повідомлення
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # повернення
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # надсилає повідомлення на SMTP-сервер smtpserver від імені відправника
    # для одержувача. Якщо verbose=True, здійснює відстеження обміну даними між клієнтом і сервером

    # отримується ім’я клієнта
    try:
        client=socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
    except socket.error, erreur:
        return "Erreur IP / Nom du client : %s" % (erreur)
    
    # відкриття з'єднання на порту 25 сервера smtpServer
    try:
        connexion=socket.create_connection((smtpServer,25))
    except socket.error, erreur:
        return "Echec de la connexion au site (%s,25) : %s" % (smtpServer,erreur)

    # з'єднання являє собою двосторонній канал зв'язку
    # між клієнтом (цією програмою) та SMTP-сервером, до якого встановлено з’єднання
    # цей канал використовується для обміну командами та інформацією

    # після встановлення з’єднання сервер надсилає привітальне повідомлення, яке ми читаємо
    erreur=sendCommand(connexion,"",verbose,1)
    if(erreur) :
        connexion.close()
        return erreur
    # команда ehlo:
    erreur=sendCommand(connexion,"EHLO %s" % (client),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # команда «mail from»:
    erreur=sendCommand(connexion,"MAIL FROM: <%s>" % (expediteur),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # команда rcpt to:
    erreur=sendCommand(connexion,"RCPT TO: <%s>" % (destinataire),verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # команда data
    erreur=sendCommand(connexion,"DATA",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # підготовка повідомлення для відправлення
    # воно має містити такі рядки
    # From: відправник
    # To: одержувач
    # порожній рядок
    # Повідомлення
    # .
    data="From: %s\r\nTo: %s\r\n%s\r\n.\r\n" % (expediteur,destinataire,message)
    # відправлення повідомлення
    erreur=sendCommand(connexion,data,verbose,0)
    if erreur :
        connexion.close()
        return erreur
    # команда виходу
    erreur=sendCommand(connexion,"QUIT",verbose,1)
    if erreur :
        connexion.close()
        return erreur
    # кінець
    connexion.close()
    return "Message envoye"

# --------------------------------------------------------------------------
def sendCommand(connexion,commande,verbose,withRCLF):
    # відправляє команду в канал з'єднання
    # режим детального виведення, якщо verbose=1
    # якщо withRCLF=1, додає послідовність RCLF до команди

    # дані
    RCLF="\r\n" if withRCLF else ""
    # відправлення команди, якщо команда не порожня
    if commande:
        connexion.send("%s%s" % (commande,RCLF))
        # можливе відлуння
        if verbose:
            affiche(commande,1)
        # читання відповіді довжиною менше 1000 символів
        reponse=connexion.recv(1000)
        # можливе відлуння
        if verbose:
            affiche(reponse,2)
        # отримання коду помилки
        codeErreur=reponse[0:3]
        # помилка, повернута сервером?
        if int(codeErreur) >=500:
            return reponse[4:]
        # повернення без помилки
        return ""

# --------------------------------------------------------------------------
def affiche(echange,sens):
    # відобразити обмін на екрані?
    # якщо sens=1, відображає -->обмін
    # якщо sens=2, виводить <-- обмін без останніх 2 символів RCLF
    if sens==1:
        print "--> [%s]" % (echange)
        return
    elif sens==2:
        l=len(echange)
        print "<-- [%s]" % echange[0:l-2]
        return

      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # видаляється позначка кінця рядка з [ligne], якщо вона є
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# головний ----------------------------------------------------------------
  
# клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
# інформація береться з файлу INFOS, що містить такі рядки
# рядок 1: smtp, відправник, одержувач
# наступні рядки: текст повідомлення

# відправник: електронна адреса відправника
# адресат: електронна адреса адресата
# smtp: назва сервера smtp, який слід використовувати


# протокол зв’язку SMTP «клієнт-сервер»
# -> клієнт підключається до порту 25 сервера SMTP
# <- сервер надсилає йому привітальне повідомлення
# -> клієнт надсилає команду EHLO: ім’я свого комп’ютера
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду mail from: <відправник>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду rcpt to: <адресат>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду data
# <- сервер відповідає OK або ні
# -> клієнт надсилає всі рядки свого повідомлення і завершує його рядком, що містить лише один символ .
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду quit
# <- сервер відповідає OK або ні

# відповіді сервера мають вигляд xxx текст, де xxx — трицифрове число. Будь-яке число xxx >=500
# вказує на помилку. Відповідь може містити кілька рядків, які починаються з xxx-, за винятком останнього
# у форматі xxx (пробіл)

# обмінювані рядки тексту повинні закінчуватися символами RC(#13) та LF(#10)

# # параметри відправлення листа
MAIL="mail2.txt" 
# отримуємо параметри листа
res=getInfos(MAIL)
# помилка?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# відправлення листа у режимі детального виведення
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# кінець
sys.exit()

Примітки:

  • на комп’ютері з ОС Windows, на якому встановлено антивірус, останній може заблокувати підключення скрипта Python до порту 25 сервера SMTP. У такому разі необхідно вимкнути антивірус. Наприклад, для McAfee можна вчинити так:
  • на [1] увімкнути консоль VirusScan
  • у [2] зупиняємо службу [Protection lors de l'accès]
  • у [3] — служба зупинена

Результати

Файл infos.txt:

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

Результати на екрані:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-angers.fr]
--> [EHLO Gportpers3.ad.univ-angers.fr]
<-- [220 smtp.univ-angers.fr ESMTP Postfix
250-smtp.univ-angers.fr
250-PIPELINING
250-SIZE 20480000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN]
--> [MAIL FROM: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.0 Ok]
--> [RCPT TO: <serge.tahe@univ-angers.fr>]
<-- [250 2.1.5 Ok]
--> [DATA]
<-- [354 End data with <CR><LF>.<CR><LF>]
--> [From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

.
]
<-- [250 2.0.0 Ok: queued as 63412114203]
--> [QUIT]
<-- [221 2.0.0 Bye]
Resultat de l'envoi : Message envoye

Повідомлення, прочитане поштовим клієнтом Thunderbird:

  

11.4. Другий SMTP-клієнт

Цей другий скрипт виконує ті самі дії, що й попередній, але використовує функції модуля [smtplib].


Програма (inet_04)


# -*- coding=utf-8 -*-

import sys,socket, smtplib

#-----------------------------------------------------------------------
def getInfos(fichier):
    # повертає інформацію (SMTP, відправник, одержувач, повідомлення), взяту з текстового файлу [fichier]
    # рядок 1: SMTP, відправник, одержувач
    # наступні рядки: текст повідомлення

    # відкриття файлу [fichier]
    erreur=""
    try:
        infos=open(fichier,"r")
    except IOError, erreur:
        return ("Le fichier %s n'a pu etre ouvert en lecture : %s" % (fichier, erreur))
  
    # читання першого рядка
    ligne=infos.readline()
    # видалення позначки кінця рядка
    ligne=cutNewLineChar(ligne)
    # витяг полів smtp, відправник, одержувач
    champs=ligne.split(",")
    # чи правильна кількість полів?
    if len(champs)!=3 :
        return ("La ligne 1 du fichier %s (serveur smtp, expediteur, destinataire) a un nombre de champs incorrect" % (fichier))
    # «обробка» отриманої інформації — видалення «пробілів», що передують або йдуть за нею
    for i in range(3):
        champs[i]=champs[i].strip()
    # витяг полів
    (smtpServer,expediteur,destinataire)=champs
    # читання повідомлення, яке потрібно надіслати
    message=""
    ligne=infos.readline()
    while ligne!='':
        message+=ligne
        ligne=infos.readline()
    infos.close()
    # повернення
    return ("",smtpServer,expediteur,destinataire,message)

#-----------------------------------------------------------------------
def sendmail(smtpServer,expediteur,destinataire,message,verbose):
    # надсилає повідомлення на SMTP-сервер smtpserver від імені відправника
    # для одержувача. Якщо verbose=True, здійснює відстеження обміну даними між клієнтом і сервером

    # використовується бібліотека smtplib 
    try:
        server = smtplib.SMTP(smtpServer)
        if verbose:
            server.set_debuglevel(1)
            server.sendmail(expediteur, destinataire, message)
            server.quit()
    except Exception, erreur:
        return "Erreur envoi du message : %s" % (erreur)
    # кінець
    return "Message envoye"
      
# --------------------------------------------------------------------------
def cutNewLineChar(ligne):
    # видаляється позначка кінця рядка [ligne], якщо вона існує
    l=len(ligne)
    while(ligne[l-1]=="\n" or ligne[l-1]=="\r"):
        l-=1
    return(ligne[0:l])

# main ----------------------------------------------------------------
  
# клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
# інформація береться з файлу INFOS, що містить такі рядки
# рядок 1: smtp, відправник, одержувач
# наступні рядки: текст повідомлення

# відправник: електронна адреса відправника
# адресат: електронна адреса адресата
# smtp: назва сервера smtp, який слід використовувати


# протокол зв’язку SMTP «клієнт-сервер»
# -> клієнт підключається до порту 25 сервера SMTP
# <- сервер надсилає йому привітальне повідомлення
# -> клієнт надсилає команду EHLO: ім’я свого комп’ютера
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду mail from: <відправник>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду rcpt to: <адресат>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду data
# <- сервер відповідає OK або ні
# -> клієнт надсилає всі рядки свого повідомлення і завершує його рядком, що містить лише один символ .
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду quit
# <- сервер відповідає OK або ні

# відповіді сервера мають вигляд xxx текст, де xxx — це трицифрове число. Будь-яке число xxx >=500
# вказує на помилку. Відповідь може містити кілька рядків, які починаються з xxx-, за винятком останнього
# у форматі xxx (пробіл)

# обмінювані рядки тексту повинні закінчуватися символами RC(#13) та LF(#10)

# # параметри відправлення листа
MAIL="mail2.txt" 
# отримуємо параметри листа
res=getInfos(MAIL)
# помилка?
if res[0]:
      print "%s" % (erreur)
      sys.exit()
# відправлення листа в режимі детального виведення
(smtpServer,expediteur,destinataire,message)=res[1:]
print "Envoi du message [%s,%s,%s]" % (smtpServer,expediteur,destinataire)
resultat=sendmail(smtpServer,expediteur,destinataire,message,True)
print "Resultat de l'envoi : %s" % (resultat)
# кінець
sys.exit()

Примітки:

  • цей скрипт ідентичний попередньому, за винятком функції sendmail. Ця функція тепер використовує можливості модуля [smtplib] (рядок 3).

Результати

Файл mail2.txt:

smtp.univ-angers.fr, serge.tahe@univ-angers.fr , serge.tahe@univ-angers.fr
From: serge.tahe@univ-angers.fr
To: serge.tahe@univ-angers.fr
Subject: test

ligne1
ligne2

ligne3

Результати на екрані:

Envoi du message [smtp.univ-angers.fr,serge.tahe@univ-angers.fr,serge.tahe@univ-
angers.fr]
send: 'ehlo Gportpers3.ad.univ-angers.fr\r\n'
reply: '250-smtp.univ-angers.fr\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-SIZE 20480000\r\n'
reply: '250-VRFY\r\n'
reply: '250-ETRN\r\n'
reply: '250-ENHANCEDSTATUSCODES\r\n'
reply: '250-8BITMIME\r\n'
reply: '250 DSN\r\n'
reply: retcode (250); Msg: smtp.univ-angers.fr
PIPELINING
SIZE 20480000
VRFY
ETRN
ENHANCEDSTATUSCODES
8BITMIME
DSN
send: 'mail FROM:<serge.tahe@univ-angers.fr> size=99\r\n'
reply: '250 2.1.0 Ok\r\n'
reply: retcode (250); Msg: 2.1.0 Ok
send: 'rcpt TO:<serge.tahe@univ-angers.fr>\r\n'
reply: '250 2.1.5 Ok\r\n'
reply: retcode (250); Msg: 2.1.5 Ok
send: 'data\r\n'
reply: '354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: End data with <CR><LF>.<CR><LF>
data: (354, 'End data with <CR><LF>.<CR><LF>')
send: 'From: serge.tahe@univ-angers.fr\r\nTo: serge.tahe@univ-angers.fr\r\nSubje
ct: test\r\n\r\nligne1\r\nligne2\r\n\r\nligne3\r\n.\r\n'
reply: '250 2.0.0 Ok: queued as D4977114358\r\n'
reply: retcode (250); Msg: 2.0.0 Ok: queued as D4977114358
data: (250, '2.0.0 Ok: queued as D4977114358')
send: 'quit\r\n'
reply: '221 2.0.0 Bye\r\n'
reply: retcode (221); Msg: 2.0.0 Bye
Resultat de l'envoi : Message envoye

11.5. Клієнт / сервер відлуння

Створюємо службу відлуння. Сервер повертає великими літерами всі рядки тексту, які йому надсилає клієнт. Служба може обслуговувати декількох клієнтів одночасно завдяки використанню потоків.


Програма сервера (inet_08)


# -*- coding=utf-8 -*-

# загальний TCP-сервер у Windows

# завантаження файлів заголовків
import re,sys,SocketServer,threading

# багатопотоковий TCP-сервер
class ThreadedTCPRequestHandler(SocketServer.StreamRequestHandler):

  def handle(self):
    # поточний потік
    cur_thread = threading.currentThread()
    # дані клієнта
    self.data="on"
    # зупинка при порожньому ланцюжку
    while self.data:
      # текстові рядки клієнта зчитуються за допомогою методу readline
      self.data =self.rfile.readline().strip()
      # відстеження консолі
      print "client %s : %s (%s)" % (self.client_address[0],self.data,cur_thread.getName())
      # відправлення відповіді клієнту
      response = "%s: %s" % (cur_thread.getName(), self.data.upper())
      # self.wfile — потік запису до клієнта
      self.wfile.write(response)                    

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

# ------------------------------------------------------ main        
# синтаксис виклику: argv[0] порт
# сервер запущено на порту з назвою

# Дані
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- перевіряється виклик
# має бути лише один аргумент
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# порт повинен бути числовим
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# запуск сервера — обслуговує кожного клієнта в окремому потоці
host="localhost"
server = ThreadedTCPServer((host,int(port)), ThreadedTCPRequestHandler)

# сервер запускається в окремому потоці
# кожен клієнт обслуговуватиметься в окремому потоці
server_thread = threading.Thread(target=server.serve_forever)
# запускає сервер — нескінченний цикл очікування клієнтів
server_thread.start()
# спостереження
print "Serveur d'echo a l'ecoute sur le port % s" % (port)

Примітки:

  • рядок 39: sys.argv — це параметри скрипта. Вони повинні мати такий вигляд: nom_du_script порт. Отже, їх має бути два. sys.argv[0] тоді стане nom_du_script, а sys.argv[1]port ;
  • рядок 53: сервер відлуння є екземпляром класу ThreadedTcpServer. Конструктор цього класу очікує два параметри:
    • параметр 1: кортеж із двох елементів (host, port), що визначає комп’ютер та порт, на якому сервер приймає запити;
    • параметр 2: ім’я класу, відповідального за обробку запитів клієнта.
  • рядок 57: створюється потік (але ще не запускається). Цей потік виконує метод [serve_forever] сервера TCP. Цей метод — це цикл, що очікує на підключення клієнтів. Як тільки буде виявлено клієнтське з’єднання, буде створено екземпляр класу ThreadedTCPRequestHandler. Його метод handle відповідає за взаємодію з клієнтом;
  • рядок 59: запускається потік служби відлуння. З цього моменту клієнти можуть підключатися до служби;
  • рядок 27: клас сервера ехо. Він походить від двох класів: SocketServer.ThreadingMixIn та SocketServer.TCPServer. Це робить його багатопотоковим сервером TCP: сервер виконується в одному потоці, а кожен клієнт обслуговується в окремому додатковому потоці;
  • рядок 9: клас, що обробляє запити клієнтів. Він походить від класу SocketServer.StreamRequestHandler. Таким чином, він успадковує два атрибути:
    • rfile: це потік читання даних, надісланих клієнтом — його можна розглядати як текстовий файл;
    • wfile: це потік запису, що дозволяє надсилати дані клієнту — його можна обробляти як текстовий файл.
  • рядок 11: метод handle обробляє запити клієнтів;
  • рядок 13: потік, що виконує цей метод;
  • рядок 17: цикл обробки запитів клієнта. Цикл завершується, коли клієнт надсилає порожній рядок;
  • рядок 19: читання запиту клієнта;
  • рядок 21: self.client_address[0] — це адреса клієнта IP. cur_thread.getName() — це ім’я потоку, що виконує метод handle;
  • рядок 23: відповідь клієнту складається з двох частин — імені потоку, що обслуговує клієнта, та команди, яку він надіслав, написаної великими літерами.

Програма клієнта (inet_06)


# -*- coding=utf-8 -*-
import re,sys,socket

# ------------------------------------------------------ main        
# загальний TCP-клієнт
# синтаксис виклику: argv[0] хост порт
# клієнт підключається до служби відлуння (хост, порт)
# сервер повертає рядки, введені клієнтом

# синтаксис
syntaxe="syntaxe : %s hote port" % (sys.argv[0])

#------------------------------------- перевіряється виклик
# має бути два аргументи
nbArguments=len(sys.argv)
if nbArguments!=3:
    print syntaxe
    sys.exit(1)

# отримуємо аргументи
hote=sys.argv[1]
# порт повинен бути числовим
port=sys.argv[2]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s foit être un nombre entier positif" % (port)
    sys.exit(2)

try:
  # підключення клієнта до сервера
  connexion=socket.create_connection((hote,int(port)))
except socket.error, erreur:
  print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
  sys.exit(3)
  
try:
    # цикл введення даних
    ligne=raw_input("Commande (rien pour arreter): ").strip()
    while ligne!="":
        # відправляємо рядок на сервер
        connexion.send("%s\n" % (ligne))
        # очікування відповіді
        reponse=connexion.recv(1000)
        print "<-- %s" % (reponse)
        ligne=raw_input("Commande (rien pour arreter): ")
except socket.error, erreur:
    print "Echec de la connexion au site (%s,%s) : %s" % (hote,port,erreur)
    sys.exit(3)
finally:
    # завершення з'єднання
    connexion.close()

Результати

Сервер запускається у першому командному вікні:

cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100

Перший клієнт запускається у другому вікні:

1
2
3
4
5
6
7
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 1
[cmde 1 du client 1]
<-- Thread-2: CMDE 1 DU CLIENT 1
Commande (rien pour arreter): cmde 2 du client 1
<-- Thread-2: CMDE 2 DU CLIENT 1
Commande (rien pour arreter):

Клієнт отримує у відповідь на команду, яку він надсилає серверу, цю саму команду, написану великими літерами. Другий клієнт запускається у третьому вікні:

1
2
3
4
5
6
cmd>%python% inet_06.py localhost 100
Commande (rien pour arreter): cmde 1 du client 2
<-- Thread-3: CMDE 1 DU CLIENT 2
Commande (rien pour arreter): cmde 2 du client 2
<-- Thread-3: CMDE 2 DU CLIENT 2
Commande (rien pour arreter):

Консоль сервера виглядає наступним чином:

1
2
3
4
5
6
cmd>%python% inet_08.py 100
Serveur d'echo a l'ecoute sur le port 100
client 127.0.0.1 : cmde 1 du client 1 (Thread-2)
client 127.0.0.1 : cmde 2 du client 1 (Thread-2)
client 127.0.0.1 : cmde 1 du client 2 (Thread-3)
client 127.0.0.1 : cmde 2 du client 2 (Thread-3)

Клієнти дійсно обслуговуються в різних потоках. Щоб зупинити клієнта, достатньо ввести порожню команду.

11.6. Загальний TCP-сервер

Ми пропонуємо написати скрипт на Python, який

  • буде TCP-сервером, здатним обслуговувати по одному клієнту за раз,
  • приймав би текстові рядки, надіслані клієнтом,
  • приймав текстові рядки з клавіатури, які надсилаються у відповідь клієнту.

Таким чином, саме користувач за клавіатурою виконує роль сервера:

  • він бачить на своїй консолі текстові рядки, надіслані клієнтом;
  • він відповідає клієнту, вводячи відповідь з клавіатури.

Таким чином, він може адаптуватися до будь-якого типу клієнта. Саме тому його називатимуть «універсальним TCP-сервером». Це зручний інструмент для вивчення протоколів зв’язку TCP. У наведеному нижче прикладі клієнтом TCP буде веб-браузер, що дозволить нам вивчити протокол HTTP, який використовують веб-клієнти.


Програма (inet_10)


# -*- coding=utf-8 -*-

# загальний TCP-сервер

# завантаження файлів заголовків
import re,sys,SocketServer,threading

# загальний TCP-сервер
class MyTCPHandler(SocketServer.StreamRequestHandler):
    def handle(self):
        # відображення клієнта
        print "client %s" % (self.client_address[0])
        # створено потік для зчитування команд клієнта
        thread_lecture = threading.Thread(target=self.lecture)
        thread_lecture.start()
        # зупинка за командою «bye»
        # читання команди, введеної з клавіатури
        cmde=raw_input("--> ")
        while cmde!="bye":
            # відправлення команди клієнту. self.wfile — це потік запису до клієнта
            self.wfile.write("%s\n" % (cmde))
            # читання наступної команди
            cmde=raw_input("--> ")

    def lecture(self):
        # виводимо всі рядки, надіслані клієнтом, до отримання команди «bye»
        ligne=""
        while ligne!="bye":
            # текстові рядки від клієнта зчитуються за допомогою методу readline
            ligne = self.rfile.readline().strip()
            # відстеження консолі
            print "<--- %s : %s" % (self.client_address[0], ligne)
    
# ------------------------------------------------------ main        
# синтаксис виклику: argv[0] порт
# сервер запускається на вказаному порту

# Дані
syntaxe="syntaxe : %s port" % (sys.argv[0])

#------------------------------------- перевіряється виклик
# має бути лише один аргумент
nbArguments=len(sys.argv)
if nbArguments!=2:
    print syntaxe
    sys.exit(1)

# порт повинен бути числовим
port=sys.argv[1]
modele=r"^\s*\d+\s*$"
if not re.match(modele,port):
    print "Le port %s n'est pas un nombre entier positif\n" % (port)
    sys.exit(2)

# запуск сервера
host="localhost"
server = SocketServer.TCPServer((host, int(port)), MyTCPHandler)
print "Service tcp generique lance sur le port %s. Arret par Ctrl-C" % (port)
server.serve_forever()

Примітки:

  • рядок 57: сервером TCP буде екземпляр класу SocketServer.TCPServer. Його конструктор приймає два параметри:
    • перший параметр — це кортеж із двох елементів (host, port), де host — це машина, на якій працює служба (зазвичай localhost), а port — — порт, на якому служба очікує (прислухається) на запити клієнтів;
    • другий параметр визначає клас обслуговування клієнта. Коли клієнт підключається, створюється екземпляр класу обслуговування, і саме його метод handle повинен керувати з’єднанням із клієнтом.

TCP-сервер SocketServer.TCPServer не є багатопотоковим. Він обслуговує лише одного клієнта за раз;

  • рядок 59: виконується метод serve_forever сервера Tcp. Це нескінченний цикл очікування клієнтів;
  • рядок 9: сервер TCP тут є класом, похідним від класу SocketServer.StreamRequestHandler. Це дозволяє розглядати потоки даних, що обмінюються з клієнтом, як текстові файли. Ми вже зустрічали цей клас. У нашому розпорядженні є методи:
    • readline для читання рядка тексту, що надходить від клієнта;
    • write — для відправки йому текстових рядків.
  • рядок 12: self.client_address[0] — це IP-адреса клієнта;
  • рядок 14: сервер TCP буде взаємодіяти з клієнтом за допомогою двох потоків
    • потоку для читання рядків від клієнта;
    • потік для запису рядків клієнту.
  • рядок 14: створюється потік для читання рядків від клієнта. Його параметр target визначає метод, який виконуватиме потік. Це той метод, що визначений у рядку 25;
  • рядок 25: запускається потік читання;
  • рядки 25–32: метод, що виконується потоком читання;
  • рядок 28: потік читання зчитує всі рядки тексту, надіслані клієнтом, до отримання рядка «bye»;
  • рядок 18: ми перебуваємо у потоці запису для клієнта. Принцип полягає в тому, щоб надсилати клієнту всі рядки тексту, введені користувачем з клавіатури.

Результати

Сервер

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C

Браузер клієнта

 

Запит, отриманий сервером

dos>%python% inet_10.py 100
Service tcp generique lance sur le port 100. Arret par Ctrl-C
client 127.0.0.1
<--- 127.0.0.1 : GET / HTTP/1.1
<--> --- 127.0.0.1 : Host: localhost:100
<--- 127.0.0.1 : User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 GTB7.1 ( .NET CLR 3.5.30729)
<--- 127.0.0.1 : Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
<--- 127.0.0.1 : Accept-Language: fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3
<--- 127.0.0.1 : Accept-Encoding: gzip,deflate
<--- 127.0.0.1 : Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
<--- 127.0.0.1 : Keep-Alive: 115
<--- 127.0.0.1 : Connection: keep-alive
<--- 127.0.0.1 : Cache-Control: max-age=0
<--- 127.0.0.1 :

Відповідь сервера, введена користувачем з клавіатури (без знака -->)

1
2
3
4
5
6
7
HTTP/1.1 200 OK
--> Server: serveur tcp generique
--> Connection: close
--> Content-Type: text/html
-->
--> <html><body><h2>Serveur tcp generique</h2></body></html>
--> bye
  • рядки 1–5: відповідь HTTP, надіслана клієнту;
  • рядок 6: HTML-сторінка, надіслана клієнту;
  • рядок 7: кінець діалогу з клієнтом. Обслуговування клієнта завершиться, і з’єднання буде закрито, що різко перерве потік читання рядків тексту, надісланих клієнтом;
  • рядки 1–5 HTTP-відповіді, надісланої клієнту, мають таке значення:
  • рядок 1: ресурс, запитуваний клієнтом, було знайдено;
  • рядок 2: ідентифікація сервера;
  • рядок 3: сервер закриє з’єднання після надсилання ресурсу;
  • рядок 4: тип ресурсу, надісланого сервером: HTML-документ;
  • рядок 5: порожній рядок.

Сторінка, що відображається браузером [1]:

Якщо переглянути вихідний код, отриманий браузером [2], можна побачити, що код HTML, отриманий веб-браузером, дійсно є тим, який йому було надіслано.