Przejdź do treści

Prosty serwer HTTP bez frameworka

  • WWW i API
  • poziom: średni
  • aktualizacja: 16 sierpnia 2026

Do prostego webhooka, lokalnego panelu albo atrapy API nie zawsze potrzebujesz Springa. Moduł JDK jdk.httpserver zawiera klasę HttpServer, która obsługuje nasłuchiwanie, ścieżki i nagłówki. Poniższy program udostępnia stronę główną oraz endpoint /powitanie?imie=.... Demonstracja wiąże serwer wyłącznie z adresem 127.0.0.1, wybiera wolny port, wykonuje lokalne żądanie i zatrzymuje usługę w bloku finally, więc nie wymaga internetu ani nie pozostaje uruchomiona po teście. Uważaj na jedną pułapkę, którą większość tutoriali przemilcza: createContext dopasowuje ścieżki prefiksowo, więc kontekst / łapie każdy adres, którego nie przejął nikt inny — jeśli sam nie porównasz ścieżki, literówka w URL-u dostanie 200 zamiast 404.

JAVA ProstySerwerHttp.java 119 linii Pobierz .java
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

/**
 * Minimalny serwer HTTP z biblioteki standardowej — skryptyJAVA.pl
 * Zapisz jako: ProstySerwerHttp.java
 */
public class ProstySerwerHttp {

    /** Tworzy serwer na wskazanym porcie; wartość 0 oznacza automatyczny wybór wolnego portu. */
    public static HttpServer utworz(int port) throws IOException {
        // 127.0.0.1 ogranicza dostęp do tego komputera i jest bezpiecznym ustawieniem demonstracyjnym.
        HttpServer serwer = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
        serwer.createContext("/", ProstySerwerHttp::stronaGlowna);
        serwer.createContext("/powitanie", ProstySerwerHttp::powitanie);
        return serwer;
    }

    private static void stronaGlowna(HttpExchange wymiana) throws IOException {
        // Kontekst "/" jest prefiksem awaryjnym, więc sam rozróżniamy stronę główną od 404.
        if (!wymiana.getRequestURI().getPath().equals("/")) {
            odpowiedz(wymiana, 404, "text/plain", "Nie znaleziono takiej strony.");
            return;
        }
        if (!tylkoOdczyt(wymiana)) { return; }
        String html = """
                <!doctype html>
                <html lang="pl"><meta charset="UTF-8"><title>Serwer Java</title>
                <body><h1>Serwer działa</h1><p>Wypróbuj /powitanie?imie=Ola</p></body></html>
                """;
        odpowiedz(wymiana, 200, "text/html", html);
    }

    private static void powitanie(HttpExchange wymiana) throws IOException {
        // Konteksty HttpServer działają prefiksowo: /powitanie/inne też trafiłoby do tej metody.
        if (!wymiana.getRequestURI().getPath().equals("/powitanie")) {
            odpowiedz(wymiana, 404, "text/plain", "Nie znaleziono takiej strony.");
            return;
        }
        if (!tylkoOdczyt(wymiana)) { return; }
        // Adres z błędnym zapisem procentowym (np. %ZZ) HttpServer odrzuca sam, kodem 400.
        String imie = parametr(wymiana.getRequestURI(), "imie");
        if (imie == null || imie.isBlank()) { imie = "świecie"; }
        odpowiedz(wymiana, 200, "text/plain", "Cześć, " + imie.strip() + "!");
    }

    /** Przepuszcza GET i HEAD, a inne metody odrzuca z nagłówkiem dozwolonych metod. */
    private static boolean tylkoOdczyt(HttpExchange wymiana) throws IOException {
        String metoda = wymiana.getRequestMethod();
        if (metoda.equalsIgnoreCase("GET") || metoda.equalsIgnoreCase("HEAD")) { return true; }
        wymiana.getResponseHeaders().set("Allow", "GET, HEAD");
        odpowiedz(wymiana, 405, "text/plain", "Dozwolone są tylko metody GET i HEAD.");
        return false;
    }

    /** Odczytuje pojedynczy parametr query string i dekoduje zapis procentowy oraz znak +. */
    private static String parametr(URI adres, String szukanaNazwa) {
        String zapytanie = adres.getRawQuery();
        if (zapytanie == null) { return null; }
        for (String para : zapytanie.split("&")) {
            String[] czesci = para.split("=", 2);
            String nazwa = URLDecoder.decode(czesci[0], StandardCharsets.UTF_8);
            if (nazwa.equals(szukanaNazwa)) {
                return czesci.length == 2
                        ? URLDecoder.decode(czesci[1], StandardCharsets.UTF_8) : "";
            }
        }
        return null;
    }

    /** Wysyła odpowiedź UTF-8 i domyka jej strumień; dla HEAD wysyła same nagłówki. */
    private static void odpowiedz(HttpExchange wymiana, int status, String typ, String tresc)
            throws IOException {
        byte[] bajty = tresc.getBytes(StandardCharsets.UTF_8);
        wymiana.getResponseHeaders().set("Content-Type", typ + "; charset=UTF-8");
        wymiana.getResponseHeaders().set("X-Content-Type-Options", "nosniff");
        if (wymiana.getRequestMethod().equalsIgnoreCase("HEAD")) {
            wymiana.sendResponseHeaders(status, -1); // HEAD wymaga długości -1, inaczej JDK ostrzega
            wymiana.close();
            return;
        }
        wymiana.sendResponseHeaders(status, bajty.length);
        try (var wyjscie = wymiana.getResponseBody()) {
            wyjscie.write(bajty);
        }
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) throws Exception {
        HttpServer serwer = utworz(0);
        serwer.start();
        int port = serwer.getAddress().getPort();
        System.out.println("Serwer działa lokalnie na porcie " + port);

        try {
            HttpClient klient = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(2)).build();
            HttpRequest zadanie = HttpRequest.newBuilder(
                            URI.create("http://127.0.0.1:" + port + "/powitanie?imie=Ola%20Nowak"))
                    .timeout(Duration.ofSeconds(3)).GET().build();
            HttpResponse<String> wynik = klient.send(
                    zadanie, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
            System.out.println("HTTP " + wynik.statusCode() + ": " + wynik.body());
        } finally {
            serwer.stop(0); // zatrzymanie jest konieczne, bo serwer utrzymuje wątek procesu
            System.out.println("Serwer zatrzymany.");
        }
    }
}

Jak użyć tego skryptu

  1. Skopiuj kod do pliku ProstySerwerHttp.java; nazwa pliku musi pokrywać się z nazwą klasy publicznej. Uruchom go poleceniem java ProstySerwerHttp.java. Program wybierze wolny port, wykona żądanie do własnego endpointu i sam się zakończy — połączenie z internetem nie jest potrzebne.
  2. W prawdziwej aplikacji podaj stały port, np. HttpServer serwer = utworz(8080), wywołaj serwer.start() i nie zatrzymuj go od razu. Endpoint będzie dostępny pod adresem http://127.0.0.1:8080/powitanie?imie=Ola. Zanim jednak wpuścisz tam więcej niż siebie, przeczytaj punkt 4 — domyślnie serwer obsługuje klientów pojedynczo.
  3. Nową trasę dodasz przez serwer.createContext("/status", Klasa::metoda). Metoda obsługi przyjmuje HttpExchange; odczytasz z niego metodę, URI, nagłówki i ciało żądania.
  4. Bez setExecutor serwer obsługuje wszystkie żądania po kolei w jednym wątku — jedno wolne żądanie każe czekać wszystkim pozostałym klientom. Jeżeli endpoint ma obsłużyć więcej niż jednego użytkownika naraz, ustaw przed start() własną pulę, np. serwer.setExecutor(Executors.newFixedThreadPool(8)), a przy zamykaniu zatrzymaj i serwer, i pulę — inaczej jej wątki utrzymają proces przy życiu.

Warto wiedzieć

  • Pakiet com.sun.net.httpserver jest dostarczany z JDK 17 w module jdk.httpserver, mimo prefiksu com.sun. Nie jest jednak pełnym serwerem produkcyjnym. Najważniejsze: bez setExecutor wszystkie żądania idą po kolei przez jeden wątek, więc jeden handler czekający dwie sekundy na bazę zatrzymuje cały ruch. Do tego nie dostaniesz routingu z parametrami ścieżki, walidacji, limitowania ruchu ani integracji z TLS i logami tak wygodnej jak we frameworku.
  • Przykład nasłuchuje wyłącznie na 127.0.0.1, więc nie jest widoczny z innych komputerów. Zmiana na 0.0.0.0 wystawia usługę na wszystkie interfejsy — rób to dopiero po dodaniu uwierzytelniania, walidacji, limitów rozmiaru żądań i reguł zapory.
  • Zawsze ustawiaj poprawny kod odpowiedzi i Content-Type. Kod 405 powinien zawierać nagłówek Allow, błędne dane klienta powinny dawać 400, brak zasobu 404, a nieobsłużony błąd serwera 500 — zwracanie 200 dla każdego przypadku utrudnia klientom poprawną obsługę. Skoro obsługujesz GET, obsłuż też HEAD: wysyłają go monitoring, health-checki i przeglądarki. W HttpServer podaj wtedy w sendResponseHeaders długość -1, bo przy podanej długości ciała JDK wypisuje ostrzeżenie na stderr.
  • Parametry URL są danymi niezaufanymi. Przed użyciem w HTML trzeba je zakodować jako tekst HTML, a przed zapytaniem SQL przekazać jako parametr przygotowanej instrukcji. Ten przykład zwraca imię jako text/plain, dlatego nie interpretuje ono znaczników HTML. Adres z połamanym zapisem procentowym (np. ?imie=%ZZ) odrzuci zresztą sam HttpServer kodem 400, zanim żądanie dojdzie do Twojej metody.
#http #httpclient #api #rest #html #blok tekstu

Podobne skrypty