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:
import socket
import re
import os
import threading
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.
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()
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:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
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:
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()
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:
header_re = re.compile(r"(GET|POST) ([^ ]+) HTTP/", re.I)
W funkcji handler
użyto kilku funkcji pomocniczych:
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.
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)
- funkcja
status
, która zwraca status HTTP wraz z kodem, tylko dwa rodzaje 404 oraz 200 zostały użyte.
def status(code):
if code == 200:
return "200 OK"
elif code == 404:
return "404 Not Found"
response
- funkcja, która zwraca odpowiedź HTTP jako ciąg znaków:
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)
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)
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'
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).
Komentarze
Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.