Skip to content

Latest commit

 

History

History
189 lines (161 loc) · 6.53 KB

2018-02-04-prosty-serwer-www-w-pythonie.markdown

File metadata and controls

189 lines (161 loc) · 6.53 KB
layout title date categories tags author description image
post
Prosty serwer www w Pythonie
2018-02-04 19:06:32 +0100
python www
jcubic
Prosty serwer www, stworzony przy pomocy gniazd (ang. sockets) w Pythonie.
url alt
/img/python.jpg
Zdjęcie pytona

Python posiada wbudowany serwer www, który można uruchomić za pomocą polecenia python -m SimpleHTTPServer 8000, który serwuje pliki z aktualnego katalogu. W tym artykule natomiast, przedstawię jak napisać prosty serwer HTTP za pomocą gniazd (ang. sockets).

Pierwszą rzeczą, jest zaimportowanie potrzebnych modułów:

{% highlight python %} import socket import re import os import threading {% endhighlight %}

Nasz główny program powinien otworzyć gniazdo, i nasłuchiwać na wybranym porcie, następnie powinien utworzyć wątek dla każdego połączenia, który obsłuży tego klienta.

{% highlight python %} if name == 'main': try: server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # jeśli proces został zakończony ale nie zamknięto gniazda # poniższe wywołanie odzyska gniazdo server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(("0.0.0.0", 8080)) server.listen(5) # główna pętla serwera while True: # dla każdego przychodzącego połączenia wywołanie funkcji w wątku client, addr = server.accept() client_handler = threading.Thread(target = handler, args=(client,)) client_handler.start() except KeyboardInterrupt: # w przypadku gdy ktoś naciśnie CTRL+C musimy zamknąć gniazdo server.close() {% endhighlight %}

Jeśli z jakiegoś powodu nie zamkniecie połączenia i zabijecie proces Pythona, bind wyrzuci wyjątek socket.error, aby temu zaradzić będziecie musieli "odzyskać" gniazdo, za pomocą tej linijki:

{% highlight python %} server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) {% endhighlight %}

Ale powyższy skrypt obsługuje zabicie procesu za pomocą CTRL+C dlatego nie powinno się to wydarzyć, chyba że wasz kod wyrzuci wyjątek.

Następnym krokiem, jest napisanie głównej funkcji, która jest przekazywana jako parametr target do konstruktora threading.Thread. Funkcja handler wygląda tak:

{% highlight python %} def handler(socket): global header_re request = get_request_data(socket) m = re.search(header_re, request[0]) if m: root = os.getcwd() matches = m.groups() # informacja o tym jaki plik jest pobierany print "request %s" % matches[1] if matches[1] == "/": fname = "index.html" else: fname = matches[1][1:] path = os.path.join(root, fname) # metoda HEAD zwraca same nagłówki if os.path.exists(path): if matches[0] == "HEAD": content = "" else: content = open(path, "r").read() socket.send(response(200, content, mime(fname))) else: if matches[0] == "HEAD": content = "" else: content = "404 Page Not found" socket.send(response(404, content)) socket.close() {% endhighlight %}

W powyższej funkcji, użyto zmiennej header_re, która zawiera wyrażenie regularne, które pozwala na wyłuskanie metody HTTP oraz ścieżki do pliku:

{% highlight python %} header_re = re.compile(r"(GET|POST) ([^ ]+) HTTP/", re.I) {% endhighlight %}

W funkcji handler użyto kilku funkcji pomocniczych:

  1. get_request_data, która czyta wszystkie dane z gniazda i zwraca listę. W naszym programie używamy tylko pierwszego elementu czyli nagłówków protokołu HTTP. Drugim elementem byłyby dane wysłane za pomocą metody POST.

{% highlight python %} def get_request_data(socket): request = [] while True: data = socket.recv(100) request.append(data) if len(data) < 100: break return "".join(request).split("\r\n\r\n", 1) {% endhighlight %}

{:start="2"} 2. funkcja status, która zwraca status HTTP wraz z kodem, tylko dwa rodzaje 404 oraz 200 zostały użyte.

{% highlight python %} def status(code): if code == 200: return "200 OK" elif code == 404: return "404 Not Found" {% endhighlight %}

{:start="3"} 3. response - funkcja, która zwraca odpowiedź HTTP jako ciąg znaków:

{% highlight python %} def response(code, data, mime = "text/plain", headers = None): response_headers = { "Server": "Python", "Content-Type": mime, "Content-Length": len(data), "Connection": "close" } if headers: response_headers.update(headers) headers = "\r\n".join([ "%s: %s" % (k,v) for k, v in response_headers.items()]) res = "HTTP/1.1 %s\r\n%s\r\n\r\n%s" return res % (status(code), headers, data) {% endhighlight %}

{:start="4"} 4. mime jest ostatnią użytą funkcją, która zwraca MIME czyli typ, który jest rozpoznawany przez przeglądarkę, np. text/html. Typ MIME informuje przeglądarkę, jak wyświetlić odpowiedź z serwera. Nic nie stoi na przeszkodzie aby np. wyświetlić stronę z rozszerzeniem html jako obrazek. (jeśli nie jest to obrazek, to wyświetli się ikonka niepoprawnego obrazka)

{% highlight python %} def mime(fname): ext = os.path.splitext(fname)[1] if ext == '.html': return 'text/html' elif ext == '.js': return 'application/javascript' elif ext == '.jpg' or ext == '.jpeg': return 'image/jpeg' elif ext == ".png": return 'image/png' elif ext == '.css': return 'text/css' else: return 'text/plain' {% endhighlight %}

Zamiast funkcji, można by też użyć słownika, którego kluczami byłyby rozszerzenia, natomiast wartościami typy MIME.

Jest to przykład prostego serwera, który może być przydatny w debugowaniu, można go rozszerzyć np. o skrypty CGI albo o obsługę plików PHP (aby dodać pliki PHP należałoby skorzystać z polecenia PHP, ale przed wywołaniem należałoby przypisać odpowiednie zmienne środowiskowe, dodam że nie testowałem).

Cały skrypt można znaleźć na githubie.

Więcej informacji o protokole HTTP, możesz znaleźć w Wikipedii, natomiast pełny opis protokołu, można znaleźć w dokumentach RFC (ang. Request for Comments).