Предыдущая статья оказалась достаточно объемной и в то же время не законченной. Привести пример HTTP клиента и не показать простейшего HTTP сервера, это как сказать "а" и забыть про "б". Пришло время все исправить. |
Реализация веб-сервера будет разбита на две части: первая (класс HttpServer) будет отвечать за прием сообщений от клиентов, вторая (класс ClientSession) за их обработку.
Код класса HttpServer представлен ниже:
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * Обрабатывает запросы от клиентов, возвращая файлы, указанные в url-path или * ответ с кодом 404, если такой файл не найден. * * @author brovko_rs */ public class HttpServer { /** * Первым аргументом может идти номер порта. */ public static void main(String[] args) { /* Если аргументы отсутствуют, порт принимает значение поумолчанию */ int port = DEFAULT_PORT; if (args.length > 0) { port = Integer.parseInt(args[0]); } /* Создаем серверный сокет на полученном порту */ ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); System.out.println("Server started on port: " + serverSocket.getLocalPort() + "\n"); } catch (IOException e) { System.out.println("Port " + port + " is blocked."); System.exit(-1); } /* * Если порт был свободен и сокет был успешно создан, можно переходить к * следующему шагу - ожиданию клинтов */ while (true) { try { Socket clientSocket = serverSocket.accept(); /* Для обработки запроса от каждого клиента создается * отдельный объект и отдельный поток */ ClientSession session = new ClientSession(clientSocket); new Thread(session).start(); } catch (IOException e) { System.out.println("Failed to establish connection."); System.out.println(e.getMessage()); System.exit(-1); } } } private static final int DEFAULT_PORT = 9999; }Приведенный код не должен вызвать затруднений. В нем создается серверный сокет, и при каждом новом подключении клиента его обработка делегируется очередному объекту ClientSession.
Полный код ClientSession приводится ниже, после идут небольшие пояснения к нему:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Date; /** * Обрабатывает запрос клиента. * * @author brovko_rs */ public class ClientSession implements Runnable { @Override public void run() { try { /* Получаем заголовок сообщения от клиента */ String header = readHeader(); System.out.println(header + "\n"); /* Получаем из заголовка указатель на интересующий ресурс */ String url = getURIFromHeader(header); System.out.println("Resource: " + url + "\n"); /* Отправляем содержимое ресурса клиенту */ int code = send(url); System.out.println("Result code: " + code + "\n"); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public ClientSession(Socket socket) throws IOException { this.socket = socket; initialize(); } private void initialize() throws IOException { /* Получаем поток ввода, в который помещаются сообщения от клиента */ in = socket.getInputStream(); /* Получаем поток вывода, для отправки сообщений клиенту */ out = socket.getOutputStream(); } /** * Считывает заголовок сообщения от клиента. * * @return строка с заголовком сообщения от клиента. * @throws IOException */ private String readHeader() throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder builder = new StringBuilder(); String ln = null; while (true) { ln = reader.readLine(); if (ln == null || ln.isEmpty()) { break; } builder.append(ln).append(System.getProperty("line.separator")); } return builder.toString(); } /** * Вытаскивает идентификатор запрашиваемого ресурса из заголовка сообщения * от клиента. * * @param header заголовок сообщения от клиента. * @return идентификатор ресурса. */ private String getURIFromHeader(String header) { int from = header.indexOf(" ") + 1; int to = header.indexOf(" ", from); String uri = header.substring(from, to); int paramIndex = uri.indexOf("?"); if (paramIndex != -1) { uri = uri.substring(0, paramIndex); } return DEFAULT_FILES_DIR + uri; } /** * Отправляет ответ клиенту. В качестве ответа отправляется http заголовок и * содержимое указанного ресурса. Если ресурс не указан, отправляется * перечень доступных ресурсов. * * @param url идентификатор запрашиваемого ресурса. * @return код ответа. 200 - если ресурс был найден, 404 - если нет. * @throws IOException */ private int send(String url) throws IOException { InputStream strm = HttpServer.class.getResourceAsStream(url); int code = (strm != null) ? 200 : 404; String header = getHeader(code); PrintStream answer = new PrintStream(out, true, "UTF-8"); answer.print(header); if (code == 200) { int count = 0; byte[] buffer = new byte[1024]; while ((count = strm.read(buffer)) != -1) { out.write(buffer, 0, count); } strm.close(); } return code; } /** * Возвращает http заголовок ответа. * * @param code код результата отправки. * @return http заголовок ответа. */ @SuppressWarnings("deprecation") private String getHeader(int code) { StringBuffer buffer = new StringBuffer(); buffer.append("HTTP/1.1 ").append(code).append(" ") .append(getAnswer(code)).append("\n"); buffer.append("Date: ").append(new Date().toGMTString()).append("\n"); buffer.append("Accept-Ranges: none\n"); buffer.append("\n"); return buffer.toString(); } /** * Возвращает комментарий к коду результата отправки. * * @param code код результата отправки. * @return комментарий к коду результата отправки. */ private String getAnswer(int code) { switch (code) { case 200: return "OK"; case 404: return "Not Found"; default: return "Internal Server Error"; } } private Socket socket; private InputStream in = null; private OutputStream out = null; private static final String DEFAULT_FILES_DIR = "/www"; }
Первое, что делает ClientSession - это получает содержимое запроса и выводит его в стандартный поток вывода. Т.к. данная реализация не предусматривает реакции на параметры запроса, то тело запроса не представляет интереса и чтение самого запроса ограничивается только его заголовком:
if (ln == null || ln.isEmpty()) { break; }Далее, из заголовка сообщения получается url запрашиваемого ресурса. В соответствии с протоколом http, url вытаскивается из первой строки заголовка как подстрока между первыми двумя пробелами. От url отрезаются параметры запроса (если они присутствуют).
private String getURIFromHeader(String header) { int from = header.indexOf(" ") + 1; int to = header.indexOf(" ", from); String uri = header.substring(from, to); int paramIndex = uri.indexOf("?"); if (paramIndex != -1) { uri = uri.substring(0, paramIndex); } return DEFAULT_FILES_DIR + uri; }
Для удобства, корневой директорией сервера считается папка www, но это всего лишь несущественная условность.
Для ответа клиенту формируется простейший http заголовок, указывающий код ответа. Доступны два кода: 200 - если запрашиваемый ресурс был найден и всеми любимый 404 - если ресурса не оказалось. Дополнительно в заголовке указывается время и тот факт, что сервер не поддерживает докачку файлов (оба поля не существенны и приводятся чисто формально).
private String getHeader(int code) { StringBuffer buffer = new StringBuffer(); buffer.append("HTTP/1.1 ").append(code).append(" ") .append(getAnswer(code)).append("\n"); buffer.append("Date: ").append(new Date().toGMTString()).append("\n"); buffer.append("Accept-Ranges: none\n"); buffer.append("\n"); return buffer.toString(); }
При отправке ответа ресурсы получаются несколько неординарным способом:
private int send(String url) throws IOException { InputStream strm = HttpServer.class.getResourceAsStream(url); int code = (strm != null) ? 200 : 404; String header = getHeader(code); PrintStream answer = new PrintStream(out, true, "UTF-8"); answer.print(header); if (code == 200) { int count = 0; byte[] buffer = new byte[1024]; while ((count = strm.read(buffer)) != -1) { out.write(buffer, 0, count); } strm.close(); } return code; }Это сделано для того, чтобы можно было уместить весь сервер в одном jar архиве.
Собранный проект вы можете взять здесь. Для запуска требуется java версии 6 и выше. Команда для запуска проекта: java -jar webserver.jar <номер порта>. Номер порта можно не указывать, тогда сервер будет запущен на 9999 порту:
java -jar webserver.jar Server started on port: 9999
Чтобы проверить работоспособность сервера, перейдите по адресу: http://localhost:9999/index.html В случае успеха должна открыться страница, которая лежит в папке www внутри jar архива с сервером. Для размещения собственных ресурсов на сервере просто добавьте их в папку www внутри архива и перейдите по ссылке http://localhost:9999/<путь>, где <путь> - это это путь к интересующему вас ресурсу относительно папки www внутри архива. Внимание! В <пути> в качестве разделителя необходимо использовать прямой слэш "/". Если путь будет содержать ссылку не на файл, а на директорию, будет выведено содержимое этой директории.
Содержимое, отправленное серверу можно увидеть в окне терминала из которого был запущен сервер. Помимо заголовка запроса будет выведен запрашиваемый адрес и код ответа сервера:
java -jar webserver.jar Server started on port: 9999 GET /index.html HTTP/1.1 Host: localhost:9999 User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:13.0) Gecko/20100101 Firefox/13.0.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Resource: /www/index.html Result code: 200
Для прекращения работы сервера воспользуйтесь стандартной комбинацией клавиш Ctrl+C
UPD: теперь доступен репозиторий, для цикла статей об http в java: https://github.com/programming086/java_http
Немає коментарів:
Дописати коментар