Оригинальная ссылка:Используйте 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
Поэтому нам нужно знать спецификацию формата данных
сообщение запроса
ответное сообщение
Две вышеприведенные картинки сначала имеют интуитивное изображение, а затем начинают акцентировать внимание на ключевых моментах.
Будь то сообщение запроса или соответствующее сообщение, его можно разделить на три части, что значительно упрощает нашу последующую обработку.
- первая строка: строка состояния
- Вторая строка до первой пустой строки: заголовок (заголовок запроса/соответствующий заголовок)
- Осталось: тело
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Созданный, этот проект предназначен в основном для решения приложения доступа к внутренним службам и исправления данных, мы тестируем на основе этого проекта.
Заполненный почтовый запрос выглядит следующим образом
Далее давайте рассмотрим ситуацию, когда выводится заголовок возврата.
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. Заявление
Это не так хорошо, как письмо веры.Контент уже написан, и это чисто из семьи.Из-за ограниченных личных возможностей неизбежно будут упущения и ошибки.Если вы найдете ошибки или лучше предложения, вы можете критиковать и исправлять их. Спасибо
- Адрес вейбо:Маленький серый блог
- QQ: серо-серый / 3302797840
3. Сканируйте внимание
серый блог
планета знаний