[RUBY] Créez votre propre serveur simple avec Java et comprenez HTTP

introduction

Cet article concerne la création d'un serveur HTTP prenant en charge les méthodes GET et POST.

Cela signifie aussi étudier HTTP, mais quand j'essaie de le faire moi-même, je pense souvent que c'est surprenant et que je l'apprécie, alors j'écris un article que je veux recommander à diverses personnes.

Lecteur supposé

--Personnes impliquées dans le développement WEB

Code source

L'article est assez cassé, donc si vous voulez voir le tout, veuillez cliquer ici. https://github.com/ksugimori/SimpleHttpServer

Il a un peu moins de fonctionnalités que la version Java, mais je fais aussi une version Ruby. https://github.com/ksugimori/simple_http_server

spécification

Fonctions à mettre en œuvre

Comme il est créé uniquement pour étudier HTTP, il peut afficher une page statique.

--Correspondant à la méthode GET --Peut afficher des pages statiques

Que devrais-je faire?

figure01.png

Lors de l'affichage d'une page WEB, le traitement suivant est effectué de manière très simple.

  1. Recevoir un message de requête HTTP du client
  2. Analysez la demande
  3. Lisez la ressource demandée
  4. Créez un message de réponse HTTP
  5. Envoyez la réponse au client

Dans ce qui suit, nous allons l'implémenter selon ces 5 étapes.

la mise en oeuvre

Étape 0: classe de message HTTP

J'ai dit "suivez 5 étapes ...", mais avant cela, créez une classe qui représente le message de demande / réponse qui est la base de HTTP.

Vérifiez d'abord les spécifications → RFC7230

Formater le message entier


HTTP-message   = start-line
                 *( header-field CRLF )
                 CRLF
                 [ message-body ]

start-line     = request-line / status-line

request-line/status-format de ligne


request-line = Method SP Request-URI SP HTTP-Version CRLF

status-line  = HTTP-version SP status-code SP reason-phrase CRLF

request-line/status-exemple de ligne


GET /path/to/something HTTP/1.1

HTTP/1.1 200 OK

La structure est la même sauf pour Start Line, donc créons une classe de base abstraite et héritons-en.

AbstractHttpMessage.java


public abstract class AbstractHttpMessage {
  protected Map<String, String> headers;
  protected byte[] body;

  public AbstractHttpMessage() {
    this.headers = new HashMap<>();
    this.body = new byte[0];
  }

  public void addHeaderField(String name, String value) {
    this.headers.put(name, value);
  }

  public Map<String, String> getHeaders() {
    return headers;
  }

  public void setBody(byte[] body) {
    this.body = body;
  }

  public byte[] getBody() {
    return body;
  }

  protected abstract String getStartLine();

  @Override
  public String toString() {
    return getStartLine() + " headers: " + headers + " body: " + new String(body, StandardCharsets.UTF_8);
  }
}

Request.java


public class Request extends AbstractHttpMessage {
  Method method;
  String target;
  String version;

  public Request(Method method, String target, String version) {
    super();
    this.method = method;
    this.target = target;
    this.version = version;
  }

  public Method getMethod() {
    return method;
  }

  public String getTarget() {
    return target;
  }

  public String getVersion() {
    return version;
  }

  @Override
  public String getStartLine() {
    return method.toString() + " " + target + " " + version;
  }
}

Response.java


public class Response extends AbstractHttpMessage {
  String version;
  Status status;

  public Response(String version, Status status) {
    super();
    this.version = version;
    this.status = status;
  }

  public String getVersion() {
    return version;
  }

  public int getStatusCode() {
    return status.getCode();
  }

  public String getReasonPhrase() {
    return status.getReasonPhrase();
  }

  @Override
  public String getStartLine() {
    return version + " " + getStatusCode() + " " + getReasonPhrase();
  }
}

Étape 1: recevoir la demande

Bien qu'il s'agisse d'un serveur HTTP, il n'effectue en fait qu'une communication socket normale. La méthode principale attend juste la connexion, et lorsque la connexion est établie, elle est traitée par un autre thread.

python


public static void main(String[] args) {
  ServerSocket server = new ServerSocket(8080);
  ExecutorService executor = Executors.newCachedThreadPool();

  while (true) {
    Socket socket = server.accept();

    //Passez l'objet socket et traitez chaque demande dans un thread distinct
    executor.submit( new WorkerThread(socket) );
  }
}

Étape 2: Analysez la demande

Request-Line

format


request-line = Method SP Request-URI SP HTTP-Version CRLF

Puisqu'il s'agit d'une chaîne de caractères séparés par des espaces, la correspondance de modèle est effectuée avec une expression régulière et la méthode HTTP, l'URI et la version HTTP sont extraites.

Request-Perspective de ligne


InputStream in = socket.getInputStream();

BufferedReader br = new BufferedReader(new InputStreamReader(in));
String requestLine = br.readLine();

Pattern requestLinePattern
     = Pattern.compile("^(?<method>\\S+) (?<target>\\S+) (?<version>\\S+)$");
Matcher matcher = requestLinePattern.matcher(requestLine);

Method method = Method.valueOf(matcher.group("method"));
String target = matcher.group("target");
String version = matcher.group("version");

Request request = new Request(method, target, version);

entête

format


header-field = field-name ":" OWS field-value OWS

Puisque l'en-tête est un ensemble de nom et de valeur de champ séparés par «:», il est également extrait avec une expression régulière. Même si le corps est de 0 octet, il y a toujours une ligne vide qui sépare l'en-tête du corps, alors lisez-le comme un en-tête jusqu'à ce que la ligne vide soit rencontrée. [^ 1]

Pattern headerPattern = Pattern.compile("^(?<name>\\S+):[ \\t]?(?<value>.+)[ \\t]?$");
while ( true ) {
  String headerField = br.readLine();
  if ( EMPTY.equals(headerField.trim()) ) break; //Lire jusqu'au délimiteur entre l'en-tête et le corps

  Matcher matcher = headerPattern.matcher(headerField);

  if (matcher.matches()) {
    request.addHeaderField(matcher.group("name"), matcher.group("value"));
  } else {
    throw new ParseException(headerField);
  }
}

corps

L'en-tête "Content-Length" ou "Transfer-Encoding" est toujours spécifié si le corps est présent. En d'autres termes, il devrait être possible de créer une branche conditionnelle en fonction de l'en-tête de demande et de prendre en charge les trois modèles suivants.

  1. Avec Content-Length
  2. Avec transfert-encodage
  3. Ni l'un ni l'autre n'existe

1. Avec Content-Length

Puisqu'il s'agit d'un format simple de message-body = * OCTET, il est correct de simplement stocker le contenu envoyé dans un tableau de type octet.

Le nombre d'octets à lire est déterminé en fonction de "Content-Length".

Integer contentLength = Integer.valueOf(request.getHeaders().get("Content-Length"));

char[] body = new char[contentLength];
bufferedReader.read(body, 0, contentLength);

request.setBody((new String(body)).getBytes());

2. Avec transfert-encodage

Si Content-Length est requis, le client ne peut pas envoyer la demande tant que le corps de la demande n'est pas créé, ce qui est inefficace. Par conséquent, HTTP / 1.1 permet d'envoyer des corps de requête fragmentés comme suit. [^ 3]

python


POST /hoge HTTP/1.1
Host: example.jp
Transfer-Encoding: chunked
Connection: Keep-Alive

a
This is a 
a
test messa
3
ge.
0

Format du corps en morceaux


Taille de bloc (octets, hexadécimal) CRLF
Données fragmentées CRLF

Même si vous ne connaissez pas le nombre d'octets pour le corps entier, c'est une stratégie pour ajouter le nombre d'octets dans l'ordre à partir de la partie préparée et l'envoyer. Le client doit enfin envoyer un bloc de 0 octet car il n'y a pas de Content-Length et la fin du corps entier ne peut pas être déterminée côté serveur.

String transferEncoding = request.getHeaders().get("Transfer-Encoding");
// "Transfer-Encoding: gzip, chunked"Il est possible de spécifier plus d'un comme, mais cette fois, seul le bloc est pris en charge
if (transferEncoding.equals("chunked")) { 
  int length = 0;
  ByteArrayOutputStream body = new ByteArrayOutputStream();
  String chunkSizeHex = br.readLine().replaceFirst(" .*$", ""); // ignore chunk-ext
  int chunkSize = Integer.parseInt(chunkSizeHex, 16);
  while (chunkSize > 0) {
    char[] chunk = new char[chunkSize];
    br.read(chunk, 0, chunkSize);
    br.skip(2); //Minutes CRLF
    body.write((new String(chunk)).getBytes());
    length += chunkSize;

    chunkSizeHex = br.readLine().replaceFirst(" .*$", "");
    chunkSize = Integer.parseInt(chunkSizeHex, 16);
 }

 request.addHeaderField("Content-Length", Integer.toString(length));
 request.getHeaders().remove("Transfer-Encoding");
 request.setBody(body.toByteArray());
}

3. Ni Transfer-Encoding ni Content-Length n’existent

Dans ce cas, le corps de la requête ne doit pas exister, alors ne faites rien

Étape 3: lire la ressource demandée

Pour la méthode GET

Lisez simplement le fichier demandé sous forme de tableau d'octets avec Files # readAllBytes et c'est OK.

//Connectez-vous à la racine du document sur le chemin du fichier réel
Path target = Paths.get(SimpleHttpServer.getDocumentRoot(), request.getTarget()).normalize();

//Rendez-le accessible uniquement sous la racine du document
if (!target.startsWith(SimpleHttpServer.getDocumentRoot())) {
  return new Response(protocolVersion, Status.BAD_REQUEST);
}

if (Files.isDirectory(target)) {
  target = target.resolve("index.html");
}

try {
  response = new Response("HTTP/1.1", Status.OK);
  response.setBody(Files.readAllBytes(target)); //Le fichier est stocké sous forme de tableau d'octets
} catch (IOException e) {
  //Définissez un code d'erreur s'il n'existe pas et lisez le fichier HTML de la page d'erreur
  response = new Response("HTTP/1.1", Status.NOT_FOUND);
  response.setBody(SimpleHttpServer.readErrorPage(Status.NOT_FOUND));
}

Pour la méthode POST

Dans le cas de POST, sortie vers la sortie standard pour vérifier si le corps est lu correctement pour le moment. Le code de réponse doit être 204: No Content pour signaler la réussite.

System.out.println("POST body: " + new String(request.getBody(), StandardCharsets.UTF_8));
Response response = new Response(protocolVersion, Status.NO_CONTENT);

Étape 4: créer un message de réponse HTTP

Si vous spécifiez Content-Type dans l'en-tête en fonction du format de fichier, le côté client se chargera du reste. [^ 2]

response = new Response(protocolVersion, Status.OK);
response.setBody(Files.readAllBytes(target));

Map<String, String> mimeTypes = new HashMap<>();
mimeTypes.put("html", "text/html");
mimeTypes.put("css", "text/css");
mimeTypes.put("js", "application/js");
mimeTypes.put("png", "image/png");

String ext = StringUtils.getFileExtension(target.getFileName().toString());
String contentType = mimeTypes.getOrDefault(ext, "");
      
response.addHeaderField("Content-Type", contentType);

Étape 5: Envoyez la réponse au client

Le format du message de réponse est

HTTP-version SP status-code SP reason-phrase CRLF
*( header-field CRLF )
CRLF
[ message-body ]

Donc, je l'écris honnêtement sur la socket en fonction du format.

 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));

  String statusLine 
     = resp.getVersion() + SP + resp.getStatusCode() + SP + resp.getReasonPhrase() + CRLF;
  writer.write(statusLine);

  for (Map.Entry<String, String> field : response.getHeaders().entrySet()) {
    writer.write(field.getKey() + ":" + SP + field.getValue() + CRLF);
  }
  writer.write(CRLF); //Ligne vide requise pour séparer l'en-tête et le corps
  writer.flush();

  out.write(response.getBody()); //Le corps écrit le tableau d'octets lu dans le fichier tel quel

Essayez de fragmenter la réponse

Dans ce programme, le message de réponse est envoyé au côté client une fois terminé, il ne vaut donc pas le format de bloc, mais je vais essayer de le faire.

Format du corps en morceaux


Taille de bloc (octets, hexadécimal) CRLF
Données fragmentées CRLF

Divisez le tableau de types d'octets qui représente le corps entier par CHUNK_SIZE et formatez-le dans le format ci-dessus.

byte[] CRLF = new byte[] {0x0D, 0x0A};

byte[] body = response.getBody();
ByteArrayOutputStream out = new ByteArrayOutputStream();

for (int offset = 0; offset < body.length; offset += CHUNK_SIZE) {
  byte[] chunk = Arrays.copyOfRange(body, offset, offset + CHUNK_SIZE);
  String lengthHex = Integer.toHexString(chunk.length);
  out.write(lengthHex.getBytes());
  out.write(CRLF);
  out.write(chunk);
  out.write(CRLF);
}
out.write("0".getBytes());
out.write(CRLF); //Fin de ligne de taille de morceau
out.write(CRLF); //Données de bloc de taille 0

Essayez de bouger

J'utilise l'objet Request et l'objet Response pour générer un journal d'accès de type Apache juste avant de renvoyer une réponse au client.

haut de page

01_index.png

Autres pages

02_about.png

Soumettre le formulaire

Comme il est écrit sur la sortie standard sans rien faire de particulier, l'espace demi-largeur est codé en pourcentage en +, mais vous pouvez voir que le contenu du formulaire est reçu.

03_post.png

Chemin inexistant

Le code d'état est 404, mais comme j'ai mis le fichier HTML de la page d'erreur dans le corps, je peux passer à l'écran d'erreur.

04_error.png

Réponse fragmentée

Il a été confirmé que le corps était divisé en 20 octets (14 en hexadécimal).

$ curl localhost:8080/chunked/sample.txt --trace-ascii /dev/stdout
== Info:   Trying ::1...
== Info: TCP_NODELAY set
== Info: Connected to localhost (::1) port 8080 (#0)
=> Send header, 96 bytes (0x60)
0000: GET /chunked/sample.txt HTTP/1.1
0022: Host: localhost:8080
0038: User-Agent: curl/7.54.0
0051: Accept: */*
005e:
<= Recv header, 17 bytes (0x11)
0000: HTTP/1.1 200 OK
<= Recv header, 28 bytes (0x1c)
0000: Transfer-Encoding: chunked
<= Recv header, 26 bytes (0x1a)
0000: Content-Type: text/plain
<= Recv header, 2 bytes (0x2)
0000:
<= Recv data, 109 bytes (0x6d)
0000: 14
0004: This is a sample tex
001a: 14
001e: t..Response is chunk
0034: 14
0038: ed in every 20 bytes
004e: 14
0052: ....................
0068: 0
006b:
This is a sample text.
Response is chunked in every 20 bytes.

référence

RFC 7230 [Comment apprendre un nouveau langage de programmation Java, Scala, Clojure à apprendre en créant un serveur HTTP](https://speakerdeck.com/todokr/xin-siihurokuraminkuyan-yu-falsexue-hifang-httpsahawozuo-tutexue-hu-java-scala- clojure) : arrow_up: Cette diapositive m'a inspiré pour créer un serveur HTTP.


[^ 1]: Avec ça, si le client lance une requête étrange, je suis accro à une boucle infinie, donc je ne pense pas que ce soit cool. .. [^ 2]: Il devrait être ajouté à moins qu'il n'y ait une raison particulière, mais il semble qu'il ne soit pas nécessaire de l'ajouter car il est "DEVRAIT" au lieu de "DOIT" → 3.1.1.5. Content-Type /specs/rfc7231.html#header.content-type) [^ 3]: Un peu simplifié. Cliquez ici pour plus de détails → 4.1. Codage de transfert par blocs

Recommended Posts

Créez votre propre serveur simple avec Java et comprenez HTTP
Comprenez l'interface java à votre manière
Créez votre propre FW de persistance (Java)
Gérez vos propres annotations en Java
Comment créer votre propre annotation en Java et obtenir la valeur
[Java] Comprenez en 10 minutes! Tableau associatif et HashMap
Organisez la différence de confort d'écriture entre l'expression lambda Java et l'expression lambda Kotlin.
Faites un blackjack avec Java
Faites votre propre pomodoro
[Mémo personnel] Créez une copie complète simple avec Java
Classes et instances Java comprises dans la figure
Mettez à jour vos connaissances Java en écrivant un serveur gRPC en Java (2)
Comment lire votre propre fichier YAML (*****. Yml) en Java
[Java] Rendre les variables de l'instruction for étendue et de chaque instruction immuables
JSON en Java et Jackson Partie 1 Renvoyer JSON à partir du serveur
Refactoring: faire du Blackjack en Java
Mettez à jour vos connaissances Java en écrivant un serveur gRPC en Java (1)
Créez vos propres annotations Java
2 Implémentez une analyse syntaxique simple en Java
Java: démarrez WAS avec Docker et déployez votre propre application
Créez votre propre plugin Elasticsearch
Créez votre propre clavier QMK dans Docker. Volume unique à Windows
Créez votre propre échantillonneur avec JMeter
Réception d'entrée très simple en Java
3 Implémentez un interpréteur simple en Java
Classe StringBuffer et StringBuilder en Java
Un exemple simple de rappels en Java
Comprendre equals et hashCode en Java
1 Implémentez une analyse de phrase simple en Java
Bonjour tout le monde en Java et Gradle
Comprendre la différence entre int et Integer et BigInteger en java et float et double
Java: essayez d'implémenter vous-même un formateur séparé par des virgules
Différence entre final et immuable en Java
Référence Java à comprendre dans la figure
Faire un tri à bulles et sélectionner le tri avec Ruby
Obtenir l'historique du serveur Zabbix en Java
Différence entre les listes d'arry et les listes liées en Java
Programmer les en-têtes et pieds de page PDF en Java
Apprenez les modèles Flyweight et ConcurrentHashMap en Java
La direction de Java dans "C ++ Design and Evolution"
De Java à C et de C à Java dans Android Studio
Lire et écrire des fichiers gzip en Java
Différence entre int et Integer en Java
Discrimination d'énum dans Java 7 et supérieur
Réflexion: Comment utiliser une interface fonctionnelle pour vos propres fonctions (java)