[TCP / IP] Après avoir étudié, essayez de créer un client HTTP avec Python

Récemment, je suis en train de créer un mécanisme légèrement inhabituel au travail qui utilise la communication par socket pour envoyer des données à partir d'un PC en utilisant un terminal Android comme serveur.

Jusqu'à présent, je n'étais au courant de HTTP (S) que lorsque je parlais de "communication", mais je voulais profiter de cette occasion pour en savoir plus sur la communication, donc pour le moment, "TCP / IP` J'étudie dans un endroit comme "ha".

Dans le cadre de cette étude, j'ai essayé d'implémenter un client HTTP en utilisant la communication par socket, je voudrais donc présenter son contenu. Le langage est Python.

avant ça

Veuillez vous référer à la page suivante pour le moment sur ce qu'est la communication socket en premier lieu.

HOWTO de programmation de socket | python

Comme décrit sur cette page, cet article part du principe que "la communication socket est TCP pour le moment".

De plus, je résumerai brièvement ce que j'ai appris en étudiant.

Une brève description des protocoles TCP et HTTP

protocole

Avant d'expliquer TCP et HTTP, j'aborderai brièvement le mot «protocole». (Parce que je ne l'ai pas bien compris)

Le protocole est selon le Dr Eijiro "Règles pour l'envoi et la réception de données entre ordinateurs".

Les appareils de communication qui existent partout dans le monde et les logiciels qui y sont exécutés (y compris le système d'exploitation) sont bien sûr fabriqués et développés par diverses entreprises et personnes. Et chaque appareil et logiciel est fabriqué et développé sans faire correspondre les spécifications entre eux.

Dans une telle situation, même si "alors échangeons des données entre des machines du monde entier", s'il n'y a pas de spécification commune, "quel type de machine" "comment envoyer des ondes radio" " Je ne peux pas bouger la main sans obtenir les informations nécessaires à la mise en œuvre, telles que «quelles données représente l'onde radio?

C'est là que la "règle" appelée "TCP / IP" est née. Tant qu'il est développé selon les règles décrites dans TCP / IP, il est possible d'envoyer et de recevoir des données sans avoir à tenir de réunions avec chaque entreprise.

Et cette «règle» est appelée «protocole» en termes informatiques.

Au fait, comment avez-vous créé et diffusé un protocole aussi universel? !! Je vais omettre la question car elle sera longue. C'était facile à comprendre quand je l'ai recherché avec le terme «modèle de référence OSI».

TCP et HTTP

TCP / IP est un ensemble de protocoles individuels requis pour que les appareils du monde entier puissent communiquer.

Il existe plusieurs types de protocoles en fonction de la méthode de communication et de l'utilisation, et ils sont en outre classés en quatre couches en fonction des couches physiques et logicielles. La première couche en bas n'est plus au niveau de "quel type de machine envoie des ondes radio".

«TCP» est situé dans la troisième couche parmi eux, et est un protocole qui définit «des règles pour envoyer et recevoir de manière fiable des contenus de communication entre deux machines, quel que soit le contenu des données».

En regardant uniquement les lettres, c'est la même chose, mais la position du mot lui-même est différente entre "TCP / IP" et "TCP", et "l'un des protocoles de la liste appelée TCP / IP" est TCP.

De plus, «HTTP» est situé dans la 4ème couche, et est une «règle définie en ajoutant plus de règles à TCP pour optimiser le format et le moment de l'envoi et de la réception des données pour la navigation sur les sites Web».

Comme je l'ai écrit un peu plus tôt, TCP est un protocole pour échanger des données entre deux machines sans excès ni défaut, donc peu importe le format des données envoyées et reçues dans quelle application. Il n'y en a pas. C'est HTTP qui détermine la règle et d'autres protocoles situés dans la quatrième couche tels que «SSH» et «FTP». Le concept de «client» et de «serveur» n'est pas si différent dans la communication TCP. La personne qui a créé l'opportunité de se connecter en premier est le "client", et la personne qui est connectée est le "serveur", mais une fois connectés, les deux peuvent envoyer et recevoir des données de la même manière.

Avec cela seul, vous pouvez voir que HTTP n'est pas tout lorsque vous dites «communication». Cela peut être plus facile à comprendre si vous lisez le contenu suivant dans cet esprit.

La façon de procéder

Ensuite, comment procéder à la mise en œuvre concrète.

Si c'est vrai, je pense qu'il est correct de lire correctement la RFC ou de penser à la conception tout en regardant l'implémentation d'autres clients HTTP, mais je le suppose par essais et erreurs sans rien regarder. Je me demande s'il vaut mieux continuer en réfléchissant. Donc, cette fois, nous procédons de la manière suivante.

  1. Implémentez un client qui peut être utilisé comme HTTP tout en gardant à l'esprit le «HTTP» que vous connaissez.
  2. Rapprochez-vous d'un client HTTP approprié tout en examinant l'implémentation de RFC et des bibliothèques client HTTP existantes

Dans cet article, j'écris sur la première partie que je vais essayer de faire en pensant par moi-même.

Essayez de mettre en œuvre

Alors commençons à l'implémenter. Le code peut également être trouvé sur GitHub.

ChooyanHttp - GitHub

Image d'utilisation

http_client.py


if __name__ == '__main__':
    resp = ChooyanHttpClient.request('127.0.0.1', 8010)
    if resp.responce_code == 200:
        print(resp.body)

Le premier est une image de la façon d'utiliser votre propre client HTTP. Après avoir passé l'hôte et le port, nous visons à obtenir l'objet qui contient les données de réponse.

Créez une classe qui ne fait rien

http_client.py


class ChooyanHttpClient:

    def request(host, port=80):
        response = ChooyanResponse()
        return response

class ChooyanResponse:
    def __init__(self):
        self.responce_code = None
        self.body = None

if __name__ == '__main__':

...Ce qui suit est omis

Cliquez ici pour diff

Ensuite, ajoutez les classes ChooyanHttpClient et ChooyanResponse selon l'image d'utilisation ci-dessus.

Je l'ai ajouté, mais je n'ai encore rien fait.

Cette fois, nous visons à obtenir le code et le corps de la réponse qui seront le résultat de la requête dans cet objet response.

Utiliser le module de prise

Ensuite, ajoutez un module socket pour communiquer.

http_client.py


import socket

class ChooyanHttpClient:

    def request(host, port=80):
        response = ChooyanResponse()

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((host, port))

        return response

class ChooyanResponse:

...Ce qui suit est omis

Cliquez ici pour diff

Comme expliqué précédemment, le protocole "HTTP" de la 4ème couche est réalisé en utilisant le protocole "TCP" de la 3ème couche et en ajoutant plus de règles.

Cette fois, le but est d'implémenter une bibliothèque qui communique selon le protocole HTTP, nous allons donc importer le module socket qui communique avec le TCP qui est la base.

Pour utiliser le module socket, consultez le [Socket Programming HOWTO] ](Https://docs.python.jp/3/howto/sockets.html) Il est brièvement décrit sur la page. Dans cet article également, nous allons procéder à l'implémentation en y faisant référence.

Ce que nous avons ajouté ici est d'utiliser le module socket pour établir une connexion avec la machine correspondant à l'hôte et au port spécifiés.

Lorsque vous faites cela, il commencera à communiquer avec le serveur spécifié. (Je ne suis pas sûr car rien n'apparaît à l'écran)

Essayez de demander

Eh bien, c'est dur d'ici.

J'ai pu me connecter à la machine de l'hôte et du port spécifiés en utilisant le module socket plus tôt.

Cependant, en l'état, aucune donnée n'est encore renvoyée. Il est normal d'établir une connexion, mais c'est naturel car nous n'avons pas encore envoyé les données de "demande".

Alors maintenant, je vais écrire le code pour envoyer la requête au serveur.

http_client.py


import socket

class ChooyanHttpClient:

    def request(host, port=80):
        response = ChooyanResponse()

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((request.host, request.port))
        request_str = 'GET / HTTP/1.1\nHost: %s\r\n\r\n' % (host)
        s.send(request_str.encode('utf-8'))

        return response

class ChooyanResponse:

...Ce qui suit est omis

Cliquez ici pour diff

Ajout d'une ligne pour exécuter la fonction send () et d'une ligne pour assembler la chaîne de caractères à lui passer une fois.

Vous pouvez maintenant envoyer des données (fixes) au serveur.

Une fois exécutée, je pense que cette demande apparaîtra dans le journal d'accès côté serveur.

Recevoir une réponse

Vous pouvez maintenant envoyer une requête GET (représentant des données) à l'hôte spécifié, mais cela seul ne suffit pas encore pour communiquer avec le serveur. C'est parce qu'il n'y a pas de code qui correspond à "recevoir".

Si vous vous demandez: "Eh bien, vous avez envoyé une requête, vous obtiendrez donc une réponse, non?", C'est parce qu'une bibliothèque client HTTP disponible dans le commerce est correctement créée. Je ne l'ai pas encore fait correctement, je ne peux donc pas recevoir de réponse.

Dans TCP, il n'y a pas de règles particulières concernant la synchronisation de la transmission des données pour chaque serveur et client. En d'autres termes, les uns les autres peuvent «envoyer leurs données préférées quand ils le souhaitent».

Cependant, si seulement cela est décidé, il n'est pas possible de savoir quel type de données sera envoyé les uns aux autres à quel moment par les serveurs et les clients avec des créateurs différents, de sorte que la liberté sera limitée dans une certaine mesure et la reconnaissance commune en règle générale. Doit avoir. L'une des perceptions courantes est le protocole «HTTP».

En d'autres termes, en HTTP, la règle est que "lorsque vous envoyez une requête, une réponse est renvoyée", le client doit donc implémenter "lorsque vous envoyez une requête, attendez que la réponse soit reçue".

Le code ressemble à ceci.

http_client.py


import socket

class ChooyanHttpClient:

    def request(request):

...réduction

        s.connect((request.host, request.port))
        request_str = 'GET / HTTP/1.1\nHost: %s\r\n\r\n' % (host)
        s.send(request_str.encode('utf-8'))
        response = s.recv(4096)

...Ce qui suit est omis

Cliquez ici pour diff

Ajout d'une ligne à la fonction recv (). Cette ligne bloquera désormais le traitement de ce programme jusqu'à ce que les données soient envoyées depuis le serveur.

Cependant, cela pose encore des problèmes.

J'omettrai les détails (car je ne les comprends pas correctement), mais dans la communication par socket, les données ne peuvent pas être reçues en même temps. En fait, comme mentionné ci-dessus, la communication par socket vous permet d'envoyer vos données préférées à tout moment, il n'est donc pas décidé combien vous pouvez faire une fois.

Par conséquent, le programme ne sait pas combien de données se trouve dans une masse telle quelle et il ne sait pas quand se déconnecter. [^ 1]

La fonction recv () mentionnée précédemment passera également au processus suivant une fois qu'elle aura reçu des données à un bon point (ou jusqu'au nombre d'octets spécifié dans l'argument) au lieu de "all".

En d'autres termes, ce code ne peut accepter que des réponses jusqu'à 4096 octets. Alors, modifiez le code pour recevoir suffisamment de données.

http_client.py


import socket

class ChooyanHttpClient:

    def request(request):
...réduction

        s.send(request_str.encode('utf-8'))

        data = []
        while True:
            chunk = s.recv(4096)
            data.append(chunk)

        response.body = b''.join(data)
        return response

...Ce qui suit est omis

Cliquez ici pour diff

Il reçoit jusqu'à 4096 octets dans une boucle infinie et ajoute de plus en plus au tableau. Enfin, si vous le concaténez, vous pouvez recevoir les données du serveur sans les manquer.

Cependant, cela est encore incomplet. Lorsque j'exécute ce code, il ne sort pas de la boucle infinie et je ne peux pas renvoyer le résultat à l'appelant.

Comme je l'ai écrit précédemment, la communication par socket n'a pas le concept de «une fois» et la communication n'a pas de fin. Cela ne dit pas au programme où terminer la boucle infinie.

Avec cela, la caractéristique de HTTP, "Si vous l'envoyez une fois, il sera renvoyé une fois" ne peut pas être réalisée, donc dans HTTP, il est décidé de spécifier la taille des données (dans la partie du corps) en utilisant l'en-tête Content-Length. Le code suivant crée un mécanisme pour le lire.

http_client.py


import socket

class ChooyanHttpClient:

    def request(request):

...réduction
        s.send(request_str.encode('utf-8'))

        headerbuffer = ResponseBuffer()
        allbuffer = ResponseBuffer()
        while True:
            chunk = s.recv(4096)
            allbuffer.append(chunk)

            if response.content_length == -1:
                headerbuffer.append(chunk)
                response.content_length = ChooyanHttpClient.parse_contentlength(headerbuffer)

            else:
                if len(allbuffer.get_body()) >= response.content_length:
                    break

        response.body = allbuffer.get_body()
        response.responce_code = 200

        s.close()
        return response

    def parse_contentlength(buffer):
        while True:
            line = buffer.read_line()
            if line.startswith('Content-Length'):
                return int(line.replace('Content-Length: ', ''))
            if line == None:
                return -1

class ChooyanResponse:
    def __init__(self):
        self.responce_code = None
        self.body = None
        self.content_length = -1

class ResponseBuffer:
    def __init__(self):
        self.data = b''

    def append(self, data):
        self.data += data

    def read_line(self):
        if self.data == b'':
            return None

        end_index = self.data.find(b'\r\n')
        if end_index == -1:
            ret = self.data
            self.data = b''
        else:
            ret = self.data[:end_index]
            self.data = self.data[end_index + len(b'\r\n'):]
        return ret.decode('utf-8')

    def get_body(self):
        body_index = self.data.find(b'\r\n\r\n')
        if body_index == -1:
            return None
        else:
            return self.data[body_index + len(b'\r\n\r\n'):]

...Ce qui suit est omis

Cliquez ici pour diff

Cela fait longtemps, mais je vais vous expliquer ce que j'essaie de faire dans l'ordre.

Trouvez la ligne Content-Length

En tant que format de réponse HTTP

Ca a été décidé.

Par conséquent, à chaque fois que des données sont reçues, la commande provient du front.

Je fais ça. Vous pouvez maintenant récupérer le Content-Length.

Cependant, «Content-Length» ne décrit que la taille de la partie du corps. L'en-tête et le code de réponse sur la première ligne ne sont pas inclus.

Par conséquent, en utilisant toutes les données reçues, j'essaie de comparer la taille des données après le deuxième saut de ligne consécutif (c'est-à-dire la partie du corps après la première ligne vide) avec Content-Length. ..

Avec cela, si la taille de Content-Length et la taille de la partie du corps correspondent (dans le code, juste au cas où elle serait supérieure ou égale à Content-Length"), vous pouvez quitter la boucle et renvoyer les données à l'appelant. Je peux le faire.

Améliorer

Maintenant que nous sommes enfin en mesure d'envoyer des requêtes et de recevoir des réponses, il est toujours inutilisable en tant que client HTTP.

La demande est terrible, limitée à la méthode GET, limitée au chemin racine et sans en-tête de demande, et la réponse renvoie simplement toutes les données, y compris le code de réponse et l'en-tête sous forme de chaîne d'octets.

Il y a encore beaucoup de choses à faire, comme formater les données ici, changer le comportement en fonction de l'en-tête, affiner le timing de transmission / réception, le traitement du timeout, etc., mais cet article est devenu assez long. Je l'ai fait, alors j'aimerais le faire la prochaine fois.

Une fois résumé

Pour le moment, j'ai essayé d'implémenter un traitement de type client HTTP, mais j'ai le sentiment d'avoir pu approfondir ma compréhension de TCP et HTTP. Il est difficile de créer une bibliothèque cliente HTTP ... De quel type d'implémentation s'agit-il, comme requests ou ʻurllib`?

C'est pourquoi je continuerai la prochaine fois.

référence

Pour référence, après avoir vu cet article, j'ai décidé d'étudier de la même manière. Dans cet article, j'ai créé le "serveur" HTTP, mais il y avait de nombreux contenus qui ont été très utiles pour créer le client.

Il a expliqué la communication par socket d'une manière très légère et facile à comprendre, ce qui m'a été très utile lors de l'étude de la communication socket. Bien qu'il s'agisse d'un document Python, il est utile quelle que soit la langue.


[^ 1]: Je pensais que cela était dû à l'en-tête KeepAlive ajouté dans HTTP 1.1. Si vous désactivez cette option, le serveur se déconnectera lorsqu'il enverra les données à la fin, et la fonction recv () côté client renverra 0, vous pouvez donc le détecter et sortir de la boucle.

Recommended Posts

[TCP / IP] Après avoir étudié, essayez de créer un client HTTP avec Python
Essayez de créer un code de "décryptage" en Python
Essayez de créer un groupe de dièdre avec Python
Faisons un outil de veille de commande avec python
Suite ・ J'ai essayé de créer Slackbot après avoir étudié Python3
Essayez d'exploiter Facebook avec Python
Essayez d'extraire une chaîne de caractères d'une image avec Python3
Essayez de créer un logiciel de capture aussi précis que possible avec python (1)
J'ai essayé de créer une fonction de similitude d'image avec Python + OpenCV
Essayez de reproduire un film couleur avec Python
Essayez de vous connecter à qiita avec Python
Fractal pour faire et jouer avec Python
Essayez de générer une image avec aliénation
Essayez de créer un logiciel de capture aussi précis que possible avec python (2)
Essayez de créer foldl et foldr avec Python: lambda. Aussi mesure du temps
Essayez de créer votre propre AWS-SDK avec bash
Envoyez un email à l'adresse de Spushi avec python
Essayez de résoudre le diagramme homme-machine avec Python
Essayez de dessiner une courbe de vie avec python
Je veux faire un jeu avec Python
Comment recadrer une image avec Python + OpenCV
Essayez de générer automatiquement des documents Python avec Sphinx
WEB grattage avec python et essayez de créer un nuage de mots à partir des critiques
Essayez de créer un serveur HTTP en utilisant Node.js
Essayez de rendre le client FTP le plus rapide avec Pythonista
Essayez de détecter les poissons avec python + OpenCV2.4 (inachevé)
[Mac] Je souhaite créer un serveur HTTP simple qui exécute CGI avec Python
Les débutants essaient de créer une application Web de combat en ligne Othello avec Django + React + Bootstrap (1)
[Cloud 9] Essayez de créer un environnement avec django 1.11 de Python 3.4 sans même comprendre 1 mm
Essayez de résoudre le livre des défis de programmation avec python3
Essayez de créer un module Python en langage C
Expliquez en détail comment créer un son avec python
Essayez de résoudre le problème d'affectation du médecin de formation avec Python
Changer les paramètres IP en ACL de conoha avec python
Essayez le fonctionnement de la base de données avec Python et visualisez avec d3
Introduction au traitement parallèle distribué Python par Ray
Note de lecture: Introduction à l'analyse de données avec Python
Essayez de créer une API RESTful avec MVC à l'aide de Flask 1.0.2
J'ai essayé d'implémenter le perceptron artificiel avec python
Comment créer un serveur HTTPS avec Go / Gin
J'ai essayé de créer une application OCR avec PySimpleGUI
Essayez de gratter avec Python.
Communication HTTP avec Python
[Python] J'ai essayé de faire une application qui calcule le salaire en fonction des heures de travail avec tkinter
Précautions lors de la saisie à partir de CSV avec Python et de la sortie vers json pour faire exe
Essayez de le faire avec GUI, PyQt en Python
Après tout, il est faux de chat avec le sous-processus python.
J'ai essayé de créer diverses "données factices" avec Python faker
Essayez d'afficher diverses informations utiles pour le débogage avec python
[Python] Comment créer une matrice de contiguïté / liste de contiguïté [Théorie des graphes]
Comment convertir un tableau en dictionnaire avec Python [Application]
La première API à créer avec le framework Python Djnago REST
Essayez d'exploiter un fichier Excel en utilisant Python (Pandas / XlsxWriter) ①