10 июл. 2008 г.

Java Simple Http Server

Простенький HTTP сервер на Java.

Итак.. Недавно у нас возникла острая необходимость использовать простенький HTTP сервер в составе очередного Java приложения. Поиски готовых библиотек подобного типа не увенчались успехом - либо это были громоздкие штуковины с кучей ненужных нам библиотек, либо что-то уж совсем кривонаписанное.
Решили писать самостоятельно.
Уже было начали реализовывать довольно сложную систему потоков, обрабатывающих запросы, как вдруг наткнулись на одну замечательную вещь, имя которой com.sun.net.httpserver
Этот package был предусмотрительно добавлен в Java 1.6.
(Вот тут про него написано)

Вобщем вкратце суть в том, что теперь для того, чтобы написать простой HTTP сервер достаточно всего 3х классов с несложной структурой.


Исходный код подкатом.



1. SimpleHttpServer:

Главный класс. Инициализирует HttpServer.

package org.tosamoepalevo.simplehttpserver;

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ResourceBundle;
import java.util.concurrent.Executors;

public class SimpleHttpServer extends Thread {
private int port;
private ResourceBundle resourceBundle;
public static final String DEFAULT_SHARED_FOLDER = "shared";
public static final String DEFAULT_DEFAULT_FILE_NAME = "index.php";
public static final int DEFAULT_PORT = 80;

public SimpleHttpServer(ResourceBundle resourceBundle) {
//загружаем параметры из файла .properties (передаётся извне)
this.resourceBundle = resourceBundle;
if (resourceBundle != null) {
port = Integer.parseInt(resourceBundle.getString("simplehttpserver.port"));
} else {
port = DEFAULT_PORT;
}
}

public void run() {
try {
InetSocketAddress adress = new InetSocketAddress(port);
//создаём HttpServer
HttpServer httpServer = HttpServer.create(adress, 0);
//На любые запросы будет отвечать наш класс SimpleHttpHandler
httpServer.createContext("/", new SimpleHttpHandler(resourceBundle));
//Вот так просто и легко мы задаём пул потоков для обработки запросов
//Как вариант можно использовать Executors.newFixedThreadPool(max number of threads)
httpServer.setExecutor(Executors.newCachedThreadPool());
//Запускаем сервер
httpServer.start();
System.out.println("Server is listening on port " + port);
} catch (IOException e) {
e.printStackTrace();
}
}
}

2. SimpleHttpHandler:

Класс, отвечающий на запросы

package org.tosamoepalevo.simplehttpserver;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ResourceBundle;

public class SimpleHttpHandler implements HttpHandler {
private ResourceBundle resourceBundle;

public SimpleHttpHandler(ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
}

public void handle(HttpExchange exchange) throws IOException {
String requestMethod = exchange.getRequestMethod();
SimpleHttpServerFileStorage fileStorage;
DataOutputStream dos;
//отвечаем на GET запросы
if (requestMethod.equalsIgnoreCase("GET")) {
dos = new DataOutputStream(exchange.getResponseBody());
//Наш класс для работы с файловой системой
fileStorage = new SimpleHttpServerFileStorage(resourceBundle);
//пишем в сокет содержимое файла
fileStorage.writeFileContent(exchange, exchange.getRequestURI().toString(), dos);
dos.flush();
dos.close();
}
}
}

3. SimpleHttpServerFileStorage:

Класс для работы с файловой системой. На самом деле для пущего упрощения можно было реализовать его функциональность и в SimpleHttpHandler.

package org.tosamoepalevo.simplehttpserver;

import com.sun.net.httpserver.HttpExchange;

import java.io.*;
import java.util.ResourceBundle;

public class SimpleHttpServerFileStorage {
private String sharedFolder;
private String defaultFileName;
private byte[] byteBuffer;

public SimpleHttpServerFileStorage(ResourceBundle resourceBundle) {
if (resourceBundle != null) {
defaultFileName = resourceBundle.getString("simplehttpserver.defaultpage");
sharedFolder = resourceBundle.getString("simplehttpserver.sharedfolder");
} else {
defaultFileName = SimpleHttpServer.DEFAULT_DEFAULT_FILE_NAME;
sharedFolder = SimpleHttpServer.DEFAULT_SHARED_FOLDER;
}
byteBuffer = new byte[4096];
}

public void writeFileContent(HttpExchange exchange, String fileName, DataOutputStream dos) throws IOException {
System.out.println("GET " + fileName);
File file = getFileByName(fileName);
if (file.exists()) {
exchange.sendResponseHeaders(200, 0);
DataInputStream dis = new DataInputStream(new FileInputStream(file));
int length;
while ((length = dis.read(byteBuffer)) != -1) {
dos.write(byteBuffer, 0, length);
}
} else {
sendError(exchange, dos, 404, "'" + fileName + "' not found.");
}
}

private File getFileByName(String fileName) {
//на всякий случай проверяем, чтобы никто не захотел обратиться к папке уровнем выше
if (fileName.indexOf("../") != -1 || fileName.indexOf("..\\") != -1) {
fileName = "/";
}
int qIndex;
//и отсекаем параметры запроса от имени файла
if ((qIndex = fileName.indexOf("?")) != -1) {
fileName = fileName.substring(0, qIndex);
}
//и перед файлом слэш тоже лучше убрать
if (fileName.indexOf("/") == 0 || fileName.indexOf("\\") == 0) {
fileName = fileName.substring(1, fileName.length());
}
//ну и если после всего этого от имени файла ничего не осталось, вернём страницу по умолчанию
if (fileName.length() == 0) {
fileName = defaultFileName;
}
return new File(sharedFolder + "/" + fileName);
}

private void sendError(HttpExchange exchange, DataOutputStream dos, int errorCode, String message) throws IOException {
exchange.sendResponseHeaders(errorCode, 0);
dos.writeBytes("<h1>Error " + errorCode + "</h1><br>");
dos.writeBytes(message);
}

}


Ну и файл simplehttpserver.properties:
simplehttpserver.port=80
simplehttpserver.sharedfolder=shared
simplehttpserver.defaultpage=index.html


Вот и всё ☺

Протестировав вышенаписанное с помощью Apache Jakarta Jmeter было выявлено, что если 100 юзеров будут одновременно и непрерывно посылать запросы, сервер справится, а большего нам и не надо ☺


Исходники обработаны с помощью blogsource

4 комментария:

Xantorohara комментирует...

А как это решение будет вести себя на кластере с балансировкой нагрузки :-)

ТОСАМОЕПАЛЕВО комментирует...

Если поставить побольше юзеров в JMeter, устроив основательный лоад тестинг, и попробовать в это время открыть какую-либо страницу из браузера, страница с отличной от нуля вероятностью будет отображаться криво (например не будет некоторых картинок или css не подгрузится), т.к. некоторые запросы будут просто игнорироваться =)
Это происходит точно, если выставить пул потоков Executors.newFixedThreadPool(max number of threads) и max 'number of threads' будет меньше числа одновременных запросов.
С httpServer.setExecutor(Executors.newCachedThreadPool()) не проверял.

Анонимный комментирует...

а если 'max number of threads' поставить очень большой, то "симпл хттп сервер" съест все ресурсы :(

п.с. вам по прежнему не нужно больше 100 одновременных соединений?) может наталкивались на простое решение, но для большей загрузки?

ТОСАМОЕПАЛЕВО комментирует...

Этот сервер скорее для баловства, чем для реальных ситуаций =)