[RUBY] Erstellen Sie mit Java Ihren eigenen einfachen Server und verstehen Sie HTTP

Einführung

In diesem Artikel wird ein HTTP-Server erstellt, der die Methoden GET und POST unterstützt.

Es bedeutet auch, HTTP zu studieren, aber wenn ich versuche, es selbst zu machen, finde ich es oft überraschend und genieße es. Deshalb schreibe ich einen Artikel, den ich verschiedenen Leuten empfehlen möchte.

Angenommener Leser

Quellcode

Der Artikel ist ziemlich kaputt. Wenn Sie das Ganze sehen möchten, klicken Sie bitte hier. https://github.com/ksugimori/SimpleHttpServer

Es hat etwas weniger Funktionalität als die Java-Version, aber ich mache auch eine Ruby-Version. https://github.com/ksugimori/simple_http_server

Spezifikation

Zu implementierende Funktionen

Da es nur zum Studieren von HTTP erstellt wurde, kann eine statische Seite angezeigt werden.

Was soll ich machen?

figure01.png

Bei der Anzeige einer WEB-Seite wird die folgende Verarbeitung auf sehr einfache Weise durchgeführt.

  1. Empfangen Sie eine HTTP-Anforderungsnachricht vom Client
  2. Analysieren Sie die Anfrage
  3. Lesen Sie die angeforderte Ressource
  4. Erstellen Sie eine HTTP-Antwortnachricht
  5. Senden Sie die Antwort an den Client

Im Folgenden werden wir es gemäß diesen 5 Schritten implementieren.

Implementierung

Schritt 0: HTTP-Nachrichtenklasse

Ich sagte "folge 5 Schritten ...", aber erstelle vorher eine Klasse, die die Anforderungs- / Antwortnachricht darstellt, die die Basis von HTTP ist.

Überprüfen Sie zuerst die Spezifikationen → RFC7230

Formatieren Sie die gesamte Nachricht


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

start-line     = request-line / status-line

request-line/status-Zeilenformat


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

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

request-line/status-Zeilenbeispiel


GET /path/to/something HTTP/1.1

HTTP/1.1 200 OK

Die Struktur ist bis auf Start Line dieselbe. Erstellen wir also eine abstrakte Basisklasse und erben sie.

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();
  }
}

Schritt 1: Empfangen Sie die Anfrage

Obwohl es sich um einen HTTP-Server handelt, führt er grundsätzlich nur eine normale Socket-Kommunikation durch. Die Hauptmethode wartet nur auf die Verbindung, und wenn die Verbindung hergestellt ist, wird sie von einem anderen Thread verarbeitet.

python


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

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

    //Übergeben Sie das Socket-Objekt und verarbeiten Sie jede Anforderung in einem separaten Thread
    executor.submit( new WorkerThread(socket) );
  }
}

Schritt 2: Analysieren Sie die Anfrage

Request-Line

Format


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

Da es sich um eine durch Leerzeichen getrennte Zeichenfolge handelt, wird die Musterübereinstimmung mit einem regulären Ausdruck durchgeführt und die HTTP-Methode, der URI und die HTTP-Version werden extrahiert.

Request-Linienperspektive


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);

Header

Format


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

Da der Header eine Reihe von Feldnamen und Werten ist, die durch : getrennt sind, wird dies auch mit einem regulären Ausdruck extrahiert. Selbst wenn der Body 0 Byte groß ist, gibt es immer eine leere Zeile, die den Header vom Body trennt. Lesen Sie ihn daher als Header, bis er auf eine leere Zeile stößt. [^ 1]

Pattern headerPattern = Pattern.compile("^(?<name>\\S+):[ \\t]?(?<value>.+)[ \\t]?$");
while ( true ) {
  String headerField = br.readLine();
  if ( EMPTY.equals(headerField.trim()) ) break; //Lesen Sie bis zum Trennzeichen zwischen Header und Body

  Matcher matcher = headerPattern.matcher(headerField);

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

Körper

Der Header "Content-Length" oder "Transfer-Encoding" wird immer angegeben, wenn der Body vorhanden ist. Mit anderen Worten, es sollte möglich sein, basierend auf dem Anforderungsheader bedingt zu verzweigen und die folgenden drei Muster zu unterstützen.

  1. Mit Inhaltslänge
  2. Mit Transfer-Encoding
  3. Beides existiert nicht

1. Mit Inhaltslänge

Da es sich um ein einfaches Format von "message-body = * OCTET" handelt, ist es in Ordnung, den gesendeten Inhalt einfach in einem Array vom Typ "Byte" zu speichern.

Die Anzahl der zu lesenden Bytes wird basierend auf "Content-Length" bestimmt.

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. Mit Transfer-Encoding

Wenn "Content-Length" erforderlich ist, kann der Client die Anforderung erst senden, wenn der Anforderungshauptteil erstellt wurde, was ineffizient ist. Daher ermöglicht HTTP / 1.1 das Senden von Chunked-Anforderungskörpern wie folgt. [^ 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

Chunked-Body-Format


Blockgröße (Bytes, hexadezimal) CRLF
Chunked Data CRLF

Auch wenn Sie die Anzahl der Bytes für den gesamten Körper nicht kennen, ist es eine Strategie, die Anzahl der Bytes in der Reihenfolge des vorbereiteten Teils zu addieren und zu senden. Der Client muss schließlich einen 0-Byte-Block senden, da es keine "Inhaltslänge" gibt und das Ende des gesamten Körpers auf der Serverseite nicht bestimmt werden kann.

String transferEncoding = request.getHeaders().get("Transfer-Encoding");
// "Transfer-Encoding: gzip, chunked"Es ist möglich, mehr als ein Like anzugeben, diesmal wird jedoch nur Chunked unterstützt
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); //CRLF-Minuten
    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. Weder Transfer-Encoding noch Content-Length existieren

In diesem Fall sollte der Anforderungshauptteil nicht vorhanden sein. Tun Sie also nichts

Schritt 3: Lesen Sie die angeforderte Ressource

Für die GET-Methode

Lesen Sie einfach die angeforderte Datei als Byte-Array mit "Files # readAllBytes" und es ist OK.

//Stellen Sie mit dem Dokumentstamm eine Verbindung zum tatsächlichen Dateipfad her
Path target = Paths.get(SimpleHttpServer.getDocumentRoot(), request.getTarget()).normalize();

//Machen Sie es nur unterhalb des Dokumentstamms zugänglich
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)); //Die Datei wird als Array von Bytes gespeichert
} catch (IOException e) {
  //Legen Sie einen Fehlercode fest, falls dieser nicht vorhanden ist, und lesen Sie die HTML-Datei für die Fehlerseite
  response = new Response("HTTP/1.1", Status.NOT_FOUND);
  response.setBody(SimpleHttpServer.readErrorPage(Status.NOT_FOUND));
}

Für die POST-Methode

Bei POST wird die Ausgabe an die Standardausgabe gesendet, um zu überprüfen, ob der Body vorerst richtig gelesen wurde. Der Antwortcode sollte 204 lauten: Kein Inhalt, um einen erfolgreichen Abschluss zu signalisieren.

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

Schritt 4: Erstellen Sie eine HTTP-Antwortnachricht

Wenn Sie im Header "Content-Type" entsprechend dem Dateiformat angeben, kümmert sich die Clientseite um den Rest. [^ 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);

Schritt 5: Senden Sie die Antwort an den Client

Das Format der Antwortnachricht lautet

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

Also schreibe ich es ehrlich gesagt entsprechend dem Format in die Steckdose.

 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); //Leere Linie erforderlich, um Header und Body zu trennen
  writer.flush();

  out.write(response.getBody()); //Der Body schreibt das aus der Datei gelesene Byte-Array so wie es ist

Versuchen Sie, die Antwort zu unterteilen

In diesem Programm wird die Antwortnachricht nach Abschluss an die Clientseite gesendet, sodass sich das Blockformat nicht lohnt, aber ich werde versuchen, es zu erstellen.

Chunked-Body-Format


Blockgröße (Bytes, hexadezimal) CRLF
Chunked Data CRLF

Teilen Sie das Bytetyp-Array, das den gesamten Körper darstellt, durch "CHUNK_SIZE" und formatieren Sie es in das obige Format.

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); //Chunk Size Line End
out.write(CRLF); //Blockdaten der Größe 0

Versuche dich zu bewegen

Ich verwende das Objekt "Request" und das Objekt "Response", um ein Apache-ähnliches Zugriffsprotokoll auszugeben, bevor eine Antwort an den Client zurückgegeben wird.

oberste Seite

01_index.png

Andere Seiten

02_about.png

Formular abschicken

Da es in die Standardausgabe geschrieben wird, ohne etwas Besonderes zu tun, wird der Raum mit halber Breite in Prozent in "+" codiert, aber Sie können sehen, dass der Inhalt des Formulars empfangen wird.

03_post.png

Nicht existierender Pfad

Der Statuscode lautet 404, aber da ich die HTML-Datei für die Fehlerseite in den Text eingefügt habe, kann ich zum Fehlerbildschirm wechseln.

04_error.png

Chunked Antwort

Es wurde bestätigt, dass der Körper in 20 (14 in hexadezimaler) Bytes unterteilt war.

$ 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.

Referenz

RFC 7230 [Wie man eine neue Programmiersprache lernt Java, Scala, Clojure, um durch Erstellen eines HTTP-Servers zu lernen](https://speakerdeck.com/todokr/xin-siihurokuraminkuyan-yu-falsexue-hifang-httpsahawozuo-tutexue-hu-java-scala- clojure) : arrow_up: Diese Folie hat mich dazu inspiriert, einen HTTP-Server zu erstellen.


[^ 1]: Wenn der Client eine seltsame Anfrage stellt, bin ich süchtig nach einer Endlosschleife, daher finde ich das nicht cool. .. [^ 2]: Es sollte hinzugefügt werden, es sei denn, es gibt einen bestimmten Grund, aber es scheint nicht notwendig zu sein, es hinzuzufügen, da es "SOLLTE" anstelle von "MUSS" ist → 3.1.1.5. Inhaltstyp /specs/rfc7231.html#header.content-type) [^ 3]: Ein wenig vereinfacht. Klicken Sie hier für Details → 4.1. Chunked Transfer Coding

Recommended Posts

Erstellen Sie mit Java Ihren eigenen einfachen Server und verstehen Sie HTTP
Verstehen Sie die Java-Oberfläche auf Ihre eigene Weise
Erstellen Sie Ihre eigene Persistenz FW (Java)
Behandeln Sie Ihre eigenen Anmerkungen in Java
So erstellen Sie Ihre eigene Anmerkung in Java und erhalten den Wert
[Java] Verstehe in 10 Minuten! Assoziatives Array und HashMap
Organisieren Sie den Unterschied im Schreibkomfort zwischen dem Java-Lambda-Ausdruck und dem Kotlin-Lambda-Ausdruck.
Machen Sie einen Blackjack mit Java
Machen Sie Ihren eigenen Pomodoro
[Persönliches Memo] Erstellen Sie eine einfache, tiefe Kopie mit Java
In der Abbildung verstandene Java-Klassen und -Instanzen
Aktualisieren Sie Ihre Java-Kenntnisse, indem Sie einen gRPC-Server in Java schreiben (2).
So lesen Sie Ihre eigene YAML-Datei (*****. Yml) in Java
[Java] Machen Sie die Variablen der erweiterten for-Anweisung und für jede Anweisung unveränderlich
JSON in Java und Jackson Teil 1 Gibt JSON vom Server zurück
Refactoring: Machen Sie Blackjack in Java
Aktualisieren Sie Ihre Java-Kenntnisse, indem Sie einen gRPC-Server in Java schreiben (1)
Erstellen Sie Ihre eigenen Java-Anmerkungen
2 Implementieren Sie eine einfache Syntaxanalyse in Java
Java: Starten Sie WAS mit Docker und stellen Sie Ihre eigene Anwendung bereit
Erstellen Sie Ihr eigenes Elasticsearch-Plugin
Erstellen Sie in Docker Ihre eigene Tastatur-QMK. Für Windows einzigartiges Volume
Erstellen Sie mit JMeter Ihren eigenen Sampler
Sehr einfacher Eingangsempfang in Java
3 Implementieren Sie einen einfachen Interpreter in Java
StringBuffer- und StringBuilder-Klasse in Java
Ein einfaches Beispiel für Rückrufe in Java
Verstehe gleich und hashCode in Java
1 Implementieren Sie eine einfache Phrasenanalyse in Java
Hallo Welt in Java und Gradle
Verstehen Sie den Unterschied zwischen int und Integer und BigInteger in Java und float und double
Java: Versuchen Sie, einen durch Kommas getrennten Formatierer selbst zu implementieren
Unterschied zwischen final und Immutable in Java
Java-Referenz zum Verständnis in der Abbildung
Blasensortierung durchführen und mit Ruby sortieren auswählen
Abrufen des Verlaufs vom Zabbix-Server in Java
Unterschied zwischen Arrylist und verknüpfter Liste in Java
Programmieren Sie PDF-Kopf- und Fußzeilen in Java
Lernen Sie Flyweight-Muster und ConcurrentHashMap in Java
Die Richtung von Java in "C ++ Design and Evolution"
Von Java nach C und von C nach Java in Android Studio
Lesen und Schreiben von GZIP-Dateien in Java
Unterschied zwischen int und Integer in Java
Diskriminierung von Enum in Java 7 und höher
Gedanke: Wie man eine funktionale Schnittstelle für eigene Funktionen verwendet (Java)