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.
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.
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 / 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.
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.
Dans cet article, j'écris sur la première partie que je vais essayer de faire en pensant par moi-même.
Alors commençons à l'implémenter. Le code peut également être trouvé sur GitHub.
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.
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
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
.
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
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)
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
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.
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
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
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
Cela fait longtemps, mais je vais vous expliquer ce que j'essaie de faire dans l'ordre.
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.
Content-Length
Content-Length
, extrayez uniquement la partie numériqueJe 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.
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.
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.
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.