Используйте Java Socket для ручной сборки http-сервера

Java

Оригинальная ссылка:Используйте Java Socket для ручной сборки http-сервера

Можно сказать, что для бэкенда Java предоставление http-сервиса является одним из основных навыков, но действительно ли вы понимаете HTTP-протокол? Вы знаете, как вручную создать http-сервер? Как нижний уровень tomcat поддерживает http-сервисы? Что такое знаменитый сервлет и как его использовать?

При изучении Java программирование сокетов является неизбежной главой; хотя в реальных бизнес-проектах возможность использования этого в основном равна 0, этот пост в блоге в основном расскажет, как использовать сокет для реализации простой функции http-сервера, предоставляя общий запрос на получение/публикацию. поддержка и понимание протокола http в процессе

I. HTTP-сервер с 0 на 1

Поскольку наша цель — построить http-сервер с помощью сокетов, нам сначала нужно подтвердить два пункта: один — как использовать сокет; другой — как устроен протокол http и как анализировать данные; далее будет объяснено их соответственно.

1. Основы программирования сокетов

В основном мы используем ServerSocket для привязки портов и предоставления услуг TCP.Основная позиция относительно проста.Общая процедура выглядит следующим образом.

  • Создайте объект ServerSocket и привяжите порт прослушивания
  • Слушайте запросы клиентов с помощью метода accept().
  • После установления соединения прочитайте информацию о запросе, отправленную клиентом через входной поток.
  • Отправлять информацию о местном акценте клиенту через выходной поток
  • Закрыть связанные ресурсы

Соответствующий псевдокод выглядит следующим образом:

ServerSocket serverSocket = new ServerSocket(port, ip)
serverSocket.accept();
// 接收请求数据
socket.getInputStream();

// 返回数据给请求方
out = socket.getOutputStream()
out.print(xxx)
out.flush();;

// 关闭连接
socket.close()

2. HTTP-протокол

ServerSocket над нами использует протокол TCP, а сам протокол HTTP является слоем поверх протокола TCP.Для нас, чтобы создать http-сервер, есть только два момента, на которые нужно обратить внимание.

  • Как парсить запрошенные данные по протоколу http
  • Как вернуть данные по протоколу http

Поэтому нам нужно знать спецификацию формата данных

сообщение запроса

request headers

ответное сообщение

respones headers

Две вышеприведенные картинки сначала имеют интуитивное изображение, а затем начинают акцентировать внимание на ключевых моментах.

Будь то сообщение запроса или соответствующее сообщение, его можно разделить на три части, что значительно упрощает нашу последующую обработку.

  • первая строка: строка состояния
  • Вторая строка до первой пустой строки: заголовок (заголовок запроса/соответствующий заголовок)
  • Осталось: тело

3. Дизайн http-сервера

Далее перейдем к делу.Создаем http сервер на основе сокета.В принципе проблем с использованием сокета нет.Надо обратить особое внимание на следующие моменты

  • Разобрать данные запроса
  • Инкапсулировать возвращаемый результат

а. Запросить анализ данных

Мы получаем все данные из сокета, а затем разбираем их в соответствующий HTTP-запрос. Сначала мы определяем объект Request и храним внутри некоторую базовую информацию HTTP. Следующим шагом является вылов всех данных в сокете и инкапсуляция их в виде объект запроса

@Data
public static class Request {
    /**
     * 请求方法 GET/POST/PUT/DELETE/OPTION...
     */
    private String method;
    /**
     * 请求的uri
     */
    private String uri;
    /**
     * http版本
     */
    private String version;

    /**
     * 请求头
     */
    private Map<String, String> headers;

    /**
     * 请求参数相关
     */
    private String message;
}

Согласно предыдущему введению протокола http, процесс синтаксического анализа выглядит следующим образом, давайте сначала посмотрим на процесс синтаксического анализа строки запроса.

строка запроса, содержит три основных элемента: метод запроса + URI + http-версия, разделенные пробелами, поэтому код парсинга выглядит следующим образом

/**
 * 根据标准的http协议,解析请求行
 *
 * @param reader
 * @param request
 */
private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
    String[] strs = StringUtils.split(reader.readLine(), " ");
    assert strs.length == 3;
    request.setMethod(strs[0]);
    request.setUri(strs[1]);
    request.setVersion(strs[2]);
}

Парсинг заголовков запросов, все данные со второй строки до первой пустой строки являются заголовком запроса, формат заголовка запроса также относительно ясен, т.к.key:value, конкретная реализация выглядит следующим образом

/**
 * 根据标准http协议,解析请求头
 *
 * @param reader
 * @param request
 * @throws IOException
 */
private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
    Map<String, String> headers = new HashMap<>(16);
    String line = reader.readLine();
    String[] kv;
    while (!"".equals(line)) {
        kv = StringUtils.split(line, ":");
        assert kv.length == 2;
        headers.put(kv[0].trim(), kv[1].trim());
        line = reader.readLine();
    }

    request.setHeaders(headers);
}

Наконец, анализ текста., этот кусок нужно отметить, что текст может быть пустым или могут быть данные, когда есть данные, как мы вынесем все данные?

Сначала посмотрите на конкретную реализацию следующим образом

/**
 * 根据标注http协议,解析正文
 *
 * @param reader
 * @param request
 * @throws IOException
 */
private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
    int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));
    if (contentLen == 0) {
        // 表示没有message,直接返回
        // 如get/options请求就没有message
        return;
    }

    char[] message = new char[contentLen];
    reader.read(message);
    request.setMessage(new String(message));
}

Обратите внимание на мою позу использования выше, в первую очередь по заголовку запросаContent-TypeЗначение , чтобы получить размер данных тела, поэтому способ, которым мы его получаем, заключается в создании такого большогоchar[]Чтобы прочитать все данные в потоке, если наш массив меньше фактического, то он не будет прочитан, если большой, то в массиве будут какие-то пустые данные;

Наконец, инкапсулируйте приведенный выше анализ, завершите парсинг запроса

/**
 * http的请求可以分为三部分
 *
 * 第一行为请求行: 即 方法 + URI + 版本
 * 第二部分到一个空行为止,表示请求头
 * 空行
 * 第三部分为接下来所有的,表示发送的内容,message-body;其长度由请求头中的 Content-Length 决定
 *
 * 几个实例如下
 *
 * @param reqStream
 * @return
 */
public static Request parse2request(InputStream reqStream) throws IOException {
    BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));
    Request httpRequest = new Request();
    decodeRequestLine(httpReader, httpRequest);
    decodeRequestHeader(httpReader, httpRequest);
    decodeRequestMessage(httpReader, httpRequest);
    return httpRequest;
}

б. Задача запроса HttpTask

Для каждого запроса назначается отдельная задача для поддержки параллелизма.Для ServerSocket при получении запроса создается задача HttpTask для реализации http связи

Так что же делает эта httptask?

  • Извлекать данные из запросов
  • Отвечать на запросы
  • Обернуть результат и вернуться
public class HttpTask implements Runnable {
    private Socket socket;

    public HttpTask(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        if (socket == null) {
            throw new IllegalArgumentException("socket can't be null.");
        }

        try {
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter out = new PrintWriter(outputStream);

            HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());
            try {
                // 根据请求结果进行响应,省略返回
                String result = ...;
                String httpRes = HttpMessageParser.buildResponse(httpRequest, result);
                out.print(httpRes);
            } catch (Exception e) {
                String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());
                out.print(httpRes);
            }
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Для инкапсуляции результата запроса дайте простую демонстрацию

@Data
public static class Response {
    private String version;
    private int code;
    private String status;

    private Map<String, String> headers;

    private String message;
}

public static String buildResponse(Request request, String response) {
    Response httpResponse = new Response();
    httpResponse.setCode(200);
    httpResponse.setStatus("ok");
    httpResponse.setVersion(request.getVersion());

    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");
    headers.put("Content-Length", String.valueOf(response.getBytes().length));
    httpResponse.setHeaders(headers);

    httpResponse.setMessage(response);

    StringBuilder builder = new StringBuilder();
    buildResponseLine(httpResponse, builder);
    buildResponseHeaders(httpResponse, builder);
    buildResponseMessage(httpResponse, builder);
    return builder.toString();
}


private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
    stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ")
            .append(response.getStatus()).append("\n");
}

private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
    for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
        stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
    }
    stringBuilder.append("\n");
}

private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
    stringBuilder.append(response.getMessage());
}

C. Настройка службы http

Предыдущие сделали в основном все, что нужно сделать, а остальное просто, создайтеServerSocket, привязываем порт для получения запроса, запускаем этот http-сервис в пуле потоков

public class BasicHttpServer {
    private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();
    private static ExecutorService taskExecutor;
    private static int PORT = 8999;

    static void startHttpServer() {
        int nThreads = Runtime.getRuntime().availableProcessors();
        taskExecutor =
                new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),
                        new ThreadPoolExecutor.DiscardPolicy());

        while (true) {
            try {
                ServerSocket serverSocket = new ServerSocket(PORT);
                bootstrapExecutor.submit(new ServerThread(serverSocket));
                break;
            } catch (Exception e) {
                try {
                    //重试
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        bootstrapExecutor.shutdown();
    }

    private static class ServerThread implements Runnable {

        private ServerSocket serverSocket;

        public ServerThread(ServerSocket s) throws IOException {
            this.serverSocket = s;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Socket socket = this.serverSocket.accept();
                    HttpTask eventTask = new HttpTask(socket);
                    taskExecutor.submit(eventTask);
                } catch (Exception e) {
                    e.printStackTrace();
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }
}

На данный момент http-сервер на основе сокетов в основном построен, и затем вы можете его протестировать.

4. Тест

Делать этот сервер, в основном, на базе проектаquick-fixСозданный, этот проект предназначен в основном для решения приложения доступа к внутренним службам и исправления данных, мы тестируем на основе этого проекта.

Заполненный почтовый запрос выглядит следующим образом

2.gif

Далее давайте рассмотрим ситуацию, когда выводится заголовок возврата.

2.gif

II. Другое

0. Исходный код проекта

  • quick-fix
  • Соответствующий код:
    • com.git.hui.fix.core.endpoint.BasicHttpServer
    • com.git.hui.fix.core.endpoint.HttpMessageParser
    • com.git.hui.fix.core.endpoint.HttpTask

1. Серый блог:liuyueyi.github.io/hexblog

Серый личный блог, записывающий все посты блога по учебе и работе, приглашаю всех в гости

2. Заявление

Это не так хорошо, как письмо веры.Контент уже написан, и это чисто из семьи.Из-за ограниченных личных возможностей неизбежно будут упущения и ошибки.Если вы найдете ошибки или лучше предложения, вы можете критиковать и исправлять их. Спасибо

3. Сканируйте внимание

серый блог

QrCode

планета знаний

goals