Предыдущая статья оказалась достаточно объемной и в то же время не законченной. Привести пример HTTP клиента и не показать простейшего HTTP сервера, это как сказать "а" и забыть про "б". Пришло время все исправить. |
Представление о том, как получить сообщение от клиента и отправить ему ответ, вы можете почерпнуть из первой статьи цикла. Чтобы развить пример tcp сервера и вырастить из него http сервер, надо подружить его с http протоколом. Естественно, поддержка всего протокола сервером - задача не самая простая и очень далеко выходящая за рамки одной статьи. Поэтому приводимая реализация веб-сервера будет ограничена возможностью отдавать точно указанный контент и выводить содержимое запроса в консоль. Практическое применение такому серверу найти не просто, но для экспериментов, при изучении http и особенно ajax, он может пригодиться.
Реализация веб-сервера будет разбита на две части: первая (класс HttpServer) будет отвечать за прием сообщений от клиентов, вторая (класс ClientSession) за их обработку.
Код класса HttpServer представлен ниже:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Обрабатывает запросы от клиентов, возвращая файлы, указанные
* в url-path или ответ с кодом 404, если такой файл не найден.
*
*/
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 приводится ниже, после идут небольшие пояснения к нему:
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;
/**
* Обрабатывает запрос клиента.
*/
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 + 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 заголовок ответа.
*/
private String getHeader(int code) {
StringBuilder buffer = new StringBuilder();
buffer.append("HTTP/1.1 " + code + " " + getAnswer(code) + "\n");
buffer.append("Date: " + new Date().toGMTString() + "\n");
buffer.append("Accept-Ranges: none\n");
buffer.append("Content-Type: " + contentType + "\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;
}
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 " + code + " " + getAnswer(code) + "\n");
buffer.append("Date: " + new Date().toGMTString() + "\n");
buffer.append("Accept-Ranges: none\n");
buffer.append("\n");
return buffer.toString();
}
При отправке ответа ресурсы получаются несколько неординарным способом:
private int send(String url) throws IOException {
// Здесь можно было бы ожидать работу с классом File...
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;
}
Собранный проект вы можете взять здесь . Для запуска требуется 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: исправлена ошибка из-за которой страница открывалась как текстовый файл с исходным кодом. В заголовок проставляется тип содержимого Content-Type.
11 комментариев:
Добрый день, вижу не саму страницу, а код в странице
http://joxi.ru/4Ake4M5cv7lNmq
Проблема в том, что в методе getHeader не проставляется content-type:
buffer.append("Content-Type: text/html\n");
Добрый день
Что нужно добавить, чтобы у меня работал css и javascript, когда сервер ответит клиенту?
Добрый =)
Все дело в правильном Content-Type. Лучше всего, почитать какие значения для этого заголовка допустимы. К примеру на вики: https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_MIME-%D1%82%D0%B8%D0%BF%D0%BE%D0%B2#text
Здравствуйте, сервер запускается но я не могу подключиться к страницам, браузеры говорят что не существует такой страницы и в консоль постоянно выводится 404. Пробовал запускать ваш и проект, а так же собирал свой. Никак не могу исправить. В чем я не прав?
увы, не могу Вам точно ответить. Но раз вы получаете 404, то сервер все-таки работает, но не может найти страниц. Попробуйте открыть изображение по адресу http://localhost:9999/index_files/webserver.png Получилось?
Та же самая проблема
Спасибо за замечания! Ошибка исправлена
Отправить комментарий