Python i programowanie sieciowe

Kurs opisujący podstawowe moduły Pythona wykorzystywane w programowaniu sieciowych - socket, select, threading wraz z przykładowymi skryptami obrazującymi ich zastosowania

Python udostępnia szeroką gamę narzędzi do programowania sieciowego. Moduły pythona głównie współpracują z protokołami TPC i UDP. Ten pierwszy jest najczęściej używanym i bardziej wiarygodnym od UDP. Oba protokoły sieciowe realizowane są za pomocą abstrakcyjnych obiektów zwanych gniazdami. Gniazdo jest obiektem podobnym do pliku, który pozwala na nawiązywanie połączeń, odbieranie danych i inne operacje. Dodatkowo komputer odbierający dane musi powiązać swoje gniazdo z portem. Przykładowo FTP korzysta z portów 20 (dane) i 21 (sterowanie), SMTP 25, HTTP (www) 80, HTTPS 443.

Moduł Socket

Moduł Pythona "socket" daje nam bezpośredni dostęp do standardowego interfejsu BSD dla gniazd, który wykorzystywany jest w większości współczesnych systemów operacyjnych. By stworzyć serwer musimy:
  • Stworzyć gniazdo
  • Przypisać gniazdo do adresu IP i portu
  • Nasłuchiwać nadchodzących połączeń
  • Oczekiwać klientów
  • Zaakceptować klienta
  • Wysyłać i odbierać dane
By stworzyć klienta (klientem jest np. przeglądarka www) musimy:
  • Stworzyć gniazdo
  • Połączyć się z serwerem
  • Wysyłać i odbierać dane
Interfejs BSD dla gniazd definiuje kilka typów rodzin adresów, takich jak:
  • AF_UNIX - Gniazdo Unixowe umożliwia dwóm procesom działającym na tej samej maszynie do porozumiewania się. W Pythonie adresy gniazd Unixowych reprezentowane są w postaci łańcuchów.
  • AF_INET - Gniazdo IPv4 to gniazdo pomiędzy dwoma procesami, potencjalnie działającymi na dwóch maszynach używające obecnej wersji adresów IP. Ten typ jest obecnie najczęściej używany. W Pythonie gniazda IPv4 reprezentowane są w postaci tupli (host, port), gdzie host to łańcuch a port to liczba całkowita - numer portu. Jako host można podać IP lub adres www, np. www.google.pl
  • AF_INET6 - Podobne do AF_INET, lecz używa IP w wersji 6, która to używa 128 bitowych adresów IP a nie 32 bitowych jak to robi obecnie stosowana wersja IPv4. W Pythonie gniazda tego typu reprezentowane są w postaci tupli (host, port, flowinfo, scopeid) gdzie "flowinfo" to identyfikator przepływu, a "scopeid" identyfikator zakresu. Obecnie IPv6 nie jest powszechnie stosowany.
By utworzyć w Pythonie gniazdo wystarczy użyć metody socket():
socket(rodzina,typ[,protokół])
Rodzina przyjmuje wartości AF_UNIX, AF_INET, lub AF_INET6. Istnieje kilka typów gniazd: "SOCK_STREAM" dla gniazd TCP i SOCK_DGRAM dla UDP. Zazwyczaj można pominąć numer protokołu. Metoda "socket()" zwraca obiekt "socket", który można wykorzystać w celu wykonania na nim metod takich jak bind, listen, accept czy connect.

Oto prosty przykład serwera i klienta komunikujących się poprzez TCP. Serwer:
from socket import *
import time
s = socket(AF_INET, SOCK_STREAM) #utworzenie gniazda
s.bind(('', 8888)) #dowiazanie do portu 8888
s.listen(5)

while 1:
	client,addr = s.accept() # odebranie polaczenia
	print 'Polaczenie z ', addr
	client.send(time.ctime(time.time())) # wyslanie danych do klienta
	client.close()
Klient:
from socket import *
s = socket(AF_INET, SOCK_STREAM) #utworzenie gniazda
s.connect(('localhost', 8888)) # nawiazanie polaczenia
tm = s.recv(1024) #odbior danych (max 1024 bajtów)
s.close()
print 'Czas serwera: ', tm
W jednym terminalu uruchamiany "serwer" a w drugim możemy uruchamiać klienta. Klient będzie zwracał dane typu "Czas serwera: Tue Jan 31 13:05:37 2006" a w terminalu z działającym serwerem pojawią się dane "Polaczenie z ('127.0.0.1', 55300)". Jeżeli serwer nie będzie działał klient zakończy swe działanie z błędem "socket.error: (111, 'Connection refused')"
Dokumentacja modułu


Moduł Select

Moduł "select" umożliwia aplikacji na oczekiwanie danych z różnych gniazd w tym samym czasie. Oznacza to że serwer używający tego modułu może obsługiwać wielu klientów na raz, jednakże nie używa on wątków, więc może obsługiwać jednego klienta na raz. Medota "select()" używa składni:
select(input,output,exception[,timeout])
Pierwsze trzy argumenty to listy gniazd lub obiektów plików, które oczekują na dane wejściowe lub wyjściowe lub wyjątek. Jeżeli nie zostanie podany "timeout" - maksymalny czas działania, to wywołanie "select" będzie zablokowane dopóki na jednym z listowanych gniazd nie wydarzy się wyjątek/dane wejściowe/wyjściowe. Wartość zero oznacza że wywołanie nie będzie zablokowane jeżeli żadne z gniazd jest gotowe. Metoda "select()" zwraca tuplę składającą się z trzech list: listę gniazd i plików, na których zaszły zdarzenia. Jeżeli został przekroczony maksymalny czas bez zajścia zdarzeń wszystkie listy będą puste.
Dokumentacja modułu


Wątki

Moduł "threading" udostępnia wysokopoziomowy interfejs wątków. Serwer bez wątków może obsługiwać jednocześnie jednego klienta, a reszta musi czekać. W przypadku zastosowania wątków serwer może obsługiwać wielu klientów jednocześnie, co znacznie zwiększa wydajność. Oto przykładowy serwer wykorzystujący wątki:
import select 
import socket 
import sys 
import threading 
 
class Server: 
    def __init__(self): 
        self.host = '' 
        self.port = 50000 
        self.backlog = 5 
        self.size = 1024 
        self.server = None 
        self.threads = [] 
 
    def open_socket(self): 
        try: 
            self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
            self.server.bind((self.host,self.port)) 
            self.server.listen(5) 
        except socket.error, (value,message): 
            if self.server: 
                self.server.close() 
            print "Could not open socket: " + message 
            sys.exit(1) 
 
    def run(self): 
        self.open_socket() 
        input = [self.server,sys.stdin] 
        running = 1 
        while running: 
            inputready,outputready,exceptready = select.select(input,[],[]) 
 
            for s in inputready: 
 
                if s == self.server: 
                    # handle the server socket 
                    c = Client(self.server.accept()) 
                    c.start() 
                    self.threads.append(c) 
 
                elif s == sys.stdin: 
                    # handle standard input 
                    junk = sys.stdin.readline() 
                    running = 0 
 
        # close all threads 
 
        self.server.close() 
        for c in self.threads: 
            c.join() 
 
class Client(threading.Thread): 
    def __init__(self,(client,address)): 
        threading.Thread.__init__(self) 
        self.client = client 
        self.address = address 
        self.size = 1024 
 
    def run(self): 
        running = 1 
        while running: 
            data = self.client.recv(self.size) 
            if data: 
                self.client.send(data) 
            else: 
                self.client.close() 
                running = 0 
 
if __name__ == "__main__": 
    s = Server() 
    s.run()
Kod długi i warty przeanalizowania. Na początek klasa klienta:
class Client(threading.Thread): 
    def __init__(self,(client,address)): 
        threading.Thread.__init__(self) 
        self.client = client 
        self.address = address 
        self.size = 1024 
"Client" dziedziczy klasę "Thread" z modułu "threading", co umożliwia traktowanie instancji klientów jako wątki, oraz wymusza by metoda __init__ klasy "Client" wywołała odpowiadającą metodę inicjalizującą klasy "Thread".
     def run(self): 
        running = 1 
        while running: 
            data = self.client.recv(self.size) 
            if data: 
                self.client.send(data) 
            else: 
                self.client.close() 
                running = 0
Metoda "run" nadpisuje tą z klasy "Thread", dzięki czemu przy wywołaniu "start()" na wątku "Client" wykona ona automatycznie metodę "run". Zwróć uwagę że w tej metodzie serwer może wejść w pętlę ciągłego odczytu danych od klienta aż do czasu rozłączenia się (klienta z serwerem). Odpada stosowanie "select". Teraz zobaczmy wątek serwera:
class Server: 
    def __init__(self): 
        self.host = '' 
        self.port = 50000 
        self.backlog = 5 
        self.size = 1024 
        self.server = None 
        self.threads = []
Inicjalizacja robi standardowe rzeczy oraz inicjalizuje listę wątków klientów.
     def open_socket(self): 
        try: 
            self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
            self.server.bind((self.host,self.port)) 
            self.server.listen(5) 
        except socket.error, (value,message): 
            if self.server: 
                self.server.close() 
            print "Could not open socket: " + message 
            sys.exit(1)
Metoda "open_socket" otwiera gniazdo i obsługuje kilka wyjątków.
def run(self): 
        self.open_socket() 
        input = [self.server,sys.stdin] 
        running = 1 
        while running: 
            inputready,outputready,exceptready = select.select(input,[],[]) 
 
            for s in inputready: 
 
                if s == self.server: 
                    # handle the server socket 
                    c = Client(self.server.accept()) 
                    c.start() 
                    self.threads.append(c) 
 
                elif s == sys.stdin: 
                    # handle standard input 
                    junk = sys.stdin.readline() 
                    running = 0 
 
        # close all threads 
 
        self.server.close() 
        for c in self.threads: 
            c.join()
Metoda "run" obsługuje gniazdo serwera jak i standardowe wejście (dzięki czemu wpisanie czegoś w terminalu zakończy działanie serwera). Serwer tworzyć nowy wątek dla każdego zaakceptowanego klienta i dodaje go do listy nagłówków. Zanim serwer wyłączy się czeka aż wszystkie wątki klientów zakończą swoje działanie.
Jako klient posłuży nam:
import socket
import sys
import time

host = 'localhost'
port = 50000
size = 1024
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
sys.stdout.write('%')

while 1:
    line = str(time.ctime(time.time())) + '
'
    s.send(line)
    data = s.recv(size)
    sys.stdout.write(data)
s.close()
Nasz klient będzie ciągle wysyłał datę do serwera, a serwer będzie odsyłał dane, co zobaczymy w konsoli - "sys.stdout.write(data)". Po uruchomieniu serwera można uruchomić kilka klientów i zobaczyć że będą działać jednocześnie (tak "aktywne" klienty będą zużywały dużo zasobów).
Dokumentacja modułu


HTTP

Python umożliwia też programowanie serwerów HTTP za pomocą kilku modułów. Poniższy przykład prezentuje zastosowanie dwa z nich:
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
import os
os.chdir("sciezka/do_katalogu/z_danymi")
serv = HTTPServer(("",8888), SimpleHTTPRequestHandler)
serv.serve_forever()
Po uruchomieniu wystarczy przejść pod adres http://localhost:8888/. Jeżeli chcemy by serwer działał na standardowym porcie wystarczy zmienić "serv = HTTPServer(("",80), SimpleHTTPRequestHandler)". Moduł SimpleHTTPServer umożliwia tworzenie prostych serwerów HTTP wyświetlających zawartość podanego katalogu z dostępem do podkatalogów.

urllib - moduł stosowany do pobierania stron www - poprzez adresy URL.
urllib2 - ten moduł obsługuje również autoryzację, cookies i inne dodatkowe opcje.
import urllib2
f = urllib2.urlopen('http://www.strona.pl/index.html')
print f.read()
telnetlib - moduł do obsługi połączeń poprzez Telnet
smtplib - moduł do obsługi protokołu smtp - wysyłania maili itp.
robotparser - moduł parsujący robots.txt na serwerach www
imaplib - moduł do obsługi protokołu IMAP
ftplib - moduł do obsługi połączeń FTP
Oraz: asyncore, CGIHTTPServer, htmllib, HTMLParser i inne :)

Jeżeli interesuje nas dość szczegółowo programowanie sieciowe w pythonie to warto przyjrzeć się frameworkowi Twisted - twistedmatrix.com.

W sieci

Kurs Pythona - Wykład 10, 11, PL
Tutorial on Network Programming with Python - PDF
Socket Programming HOWTO
Untwisting Python Network Programming
Python Network Programming
blog comments powered by Disqus

Kategorie

Strony