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.
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
Da es nur zum Studieren von HTTP erstellt wurde, kann eine statische Seite angezeigt werden.
Bei der Anzeige einer WEB-Seite wird die folgende Verarbeitung auf sehr einfache Weise durchgeführt.
Im Folgenden werden wir es gemäß diesen 5 Schritten implementieren.
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();
}
}
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) );
}
}
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);
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);
}
}
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.
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());
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());
}
In diesem Fall sollte der Anforderungshauptteil nicht vorhanden sein. Tun Sie also nichts
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));
}
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);
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);
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
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
Ich verwende das Objekt "Request" und das Objekt "Response", um ein Apache-ähnliches Zugriffsprotokoll auszugeben, bevor eine Antwort an den Client zurückgegeben wird.
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.
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.
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.
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