Лучший способ понять устройство и принцип работы чего-либо – сделать это что-то самому.
Заинтересовавшись однажды сетевыми технологиями и, среди прочего, серверами, я пришёл к мысли, что было бы неплохо написать одн такой сервер самому, используя Java.
Однако большая часть Java-литературы, что попадалась мне на глаза, если и объясняла нечто по теме, то лишь в самых общих чертах, не идя далее обмена текстовыми сообщениями между клиент-серверными приложениями. В интернете подобная информация встречалась не намного чаще, в основном в виде крупиц знаний на обучающийх сайтах или отрывочных сведений от пользователей разнообразных форумов.
Таким образом, собрав эти знания воедино и написав таки удобоваримое серверное прилоение, спобоное обрабатывать браузерные запросы, я решил сделать выжимку из подобных знаний и поэтапно описать процесс создания простейшего web-сервера на Java.
Надеюсь, в этой статье найдутся полезные знания для начинающих Java-программистов и других людей, изучающих связанные технологии.
Итак, поскольку программа предполагает простейшие функции сервера, она будет состоять из одного класса без графического интерфейса. Этот класс (Server) наследует поток и имеет одно поле – сокет:
Class Server extends Thread {
Socket s;
}
В главном методе создаём новый ServerSocket и задаём для него порт (в данном случае использован порт 1025) и в бесконечном цикле ожидаем соединения с клиентом. При наличии соединения мы создаем новый поток, передавая ему соответствующий сокет. В случае неудачи выводим сообщение об ошибке:
Try {
ServerSocket server = new ServerSocket(1025);
while(true) {
new Server(server.accept());
}
}
catch(Exception e) {
System.out.println("Error: " + e);
}
Для того, чтобы сделать возможным создание нового потока подобным образом мы, естественно, должны описать для него соответствующий конструктор. В конструкторе мы маркируем поток как демон и здесь же запускаем:
Public Server(Socket socket) {
this.socket = socket;
setDaemon(true);
start();
}
Далее описываем функционал потока в методе run(): в первую очередь, создаем из сокета поток исходящих и входящих данных:
Public void run() {
try {
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
Для считывания входящих данных мы будем применять буфер, представляющий из себя байт-массив определёной размерности. Создание подобного буфера не является обязательным, т.к. возможно принимать входящие данные и другими способами, не лимитируя количество принимаемой информации – но для простейшего web-сервера, описанного здесь, вполне достаточно 64 кбайт для получения необходимых данных от клиента.
Помимо байт-массива, мы также создаем int переменную, которая будет хранить в себе количество реально принятых буфером байт. Это необходимо для того, чтобы в последствии особыми образом создать строку клиентского запроса из полученных данных:
Byte buffer = new byte;
int bytes = input.read(buffer);
String request = new String(buf, 0, r);
В строке request будет содержаться http-запрос от клиента. Среди прочей информации, содержащейся в данном запросе, нас в данный момент интересует адрес запрашиваемого файла и его расширение.
Не вдаваясь в подробности структуры http-запроса скажу лишь, что нужная нам информация будет находиться в первой строчке данного запроса приблизительно в таком виде:
GET /index.html HTTP/1.1
В данном примере запрашивается страница на сервере по адресу
/index.html
Страница с таким адресом выдается на большинстве серверов по умолчанию. Наша задача – с помощью собственноручно написанного метода getPath() вычленить этот адрес из запроса. Существует множество вариантов подобного метода и здесь их приводить нет смысла. Ключевой момент здесь состоит в том, что получив путь до нужного файла и записав его в строковую переменную path, мы можем попробовать создать на основе этих данных файл и, в случае успеха, вернуть этот файл, а в случае неудачи – вернуть специфическое сообщение об ошибке:
String path = getPath(request);
File file = new File(path);
Проверяем, является ли данный файл дирекорией. Если такой файл существует и является директорией, то мы возвращаем упомянутый выше файл по умолчанию – index.html:
Boolean exists = !file.exists();
if(!exists)
if(file.isDirectory())
if(path.lastIndexOf(""+File.separator) == path.length()-1) {
path = path + "index.html";
}
else {
path = path + File.separator + "index.html";
file = new File(path);
exists = !file.exists();
}
Если файла по указанному адресу не существует, то мы создаем http-ответ в строке response с указанием того, что файл не найден, добавляя в нужном порядке следующие заголовки:
If(exists){
String response = "HTTP/1.1 404 Not Found\n";
response +="Date: " + new Date() + "\n";
response +="Content-Type: text/plain\n";
response +="Connection: close\n";
response +="Server: Server\n";
response +="Pragma: no-cache\n\n";
response += "File " + path + " Not Found!";
После формирования строки response мы отправляем их клиенту и закрываем соединение:
Output.write(response.getBytes());
socket.close();
return;
}
Если же файл существует, то перед формированием ответа необходимо выяснить его расширение и, следовательно, MIME-тип. Для начала мы выясним индекс точки, стоящей перед расширением файла и сохраним его в int-переменную.
Int ex = path.lastIndexOf(".");
Затем вычленим расширение файла, стоящее после неё. Список возможным MIME-типов можно расширить, но в данном случае буде использовать всего по одной из форм 3-х форматов: html, jpeg и gif. По умолчанию будем использовать MIME-тип для текста:
String response = "HTTP/1.1 200 OK\n";
response += "Last-Modified: " + new Date(file.lastModified())) + "\n";
response += "Content-Length: " + file.length() + "\n";
response += "Content-Type: " + mimeType + "\n";
response +="Connection: close\n";
В конце заголовков обязательно должно быть две пустые строки, иначе ответ не будет корректны образом обработан клиентом.
Response += "Server: Server\n\n";
output.write(response.getBytes());
Для отправки самого файла можно использовать следующую конструкцию:
FileInputStream fis = new FileInputStream(path);
int write = 1;
while(write > 0) {
write = fis.read(buffer);
if(write > 0) output.write(buffer, 0, write);
}
fis.close();
socket.close();
}
Наконец, завершаем блок try-catch, указанный в начале.
Catch(Exception e) {
e.printStackTrace();
} }
Поскольку, как уже было сказано, данная реализация web-сервера является одной из простейших, она может быть легко доработана путём добавления графического интерфейса пользователя, количества поддерживаемых расширений, ограничителя подключений и т.п. Одним словом – простор для творчества остаётся огромным. Дерзайте.
Вы можете помочь и перевести немного средств на развитие сайта
Как вы наверное уже знаете - L2jServer сменил политику обновлений своих кодов и теперь обновления выходят примерно один раз в месяц, но тем не менее, код остается открытым и доступным для всех желающих изучить и установить Freya.
→
L2Teon проделал почти 150 корректировок кода с момента выхода прошлой ревизии java сервера Lineage 2 Interlude. Благодаря этому была повышена безопасность и отказоустойчивость сервера. Обновлена защита заточки предметов, исправлен спавн-лист. Исправлен баг с питомцами. Отключена загрузка AI с ядра, теперь только с «\data\scripts\ai\». Исправлены умения: Curse of Doom, Anchor, Mirage. Исправлен баг со складом (не верно указаны данные FloodProtector). Исправлен баг с исчезновением питомца после смерти. Исправлены умения Spell Force и Batle Force. Закрыта возможность чрезмерной заточки предметов. Исправление записей таблицы NPC благодаря которому нет ошибок в консоли.
→
Разработка Java сервера Lineage 2 Gracia Epilogue или просто Plus продолжается, и сегодня для владельцев серверов на базе L2Open-Team доступно следующее обновление. Добавлена опция отвечающая за шанс поднятия уровня Soul Crystal. Исправлен третий этаж в Steel Citadel, точнее возможность на него перейти. Добавлено восстановление маны Шаманом Орков при ударах. Исправлено получение магической поддержки новичкам. Исправление невидимости персонажей. Были добавлены семена манора по Lineage 2 Gracia Epilogue. Добавлен новый квест Pailaka Injured Dragon (необходим тест). В движке реализовано умение "Семь Стрел (Seven Arrow)". Добавлены Эвенты: Последний Герой и Захват Базы. Администратору доступен телепорт ко всем Рейд Боссам Gracia.
→
В очередной раз вышло обновление сборки сервера Lineage 2 Epilogue от команды L2jServer с ревизией 4309 у ядра и 7529 у датапака. Исправлено отображение окна действий при совершении трансформации. Возможность активации кэширования всех имен существующих персонажей. Исправление проблемы с отключением авто-сосок на одетом оружии. Добавлена настройка дальности вещания событий с кораблями. Доступна новая опция включения Чемпионов для спавна на карту. Реализация клановых дирижаблей, исправление пути полета в квестах. Добавлена поддержка более точного поиска двойных окон. Был исправлена ошибка Геодвига из-за которой выводилось "Can"t see target". В транспортных средствах теперь невозможно производить торговлю. Дроплист проверяется при загрузке сервера на предмет "левых" предметов.
Есть ли способ создать очень простой HTTP-сервер (поддерживающий только GET/POST) в Java, используя только API Java SE, без написания кода для ручного анализа HTTP-запросов и отформатирования HTTP-ответов вручную? Java SE API прекрасно инкапсулирует функциональность HTTP-клиента в HttpURLConnection, но существует ли аналоговый для HTTP-сервер функционал?
Просто, чтобы быть ясным, проблема, с которой я сталкиваюсь с множеством примеров ServerSocket, которые я видел в Интернете, заключается в том, что они выполняют собственный алгоритм синтаксического анализа/отклика запросов и обработку ошибок, что является утомительным, подверженным ошибкам и вряд ли быть всеобъемлющим, и я стараюсь избегать этого по этим причинам.
В качестве примера ручной HTTP-манипуляции, которую я пытаюсь избежать:
18
ответов
Начиная с Java SE 6 в Sun Oracle JRE имеется встроенный HTTP-сервер. В сводке пакета com.sun.net.httpserver описаны участвующие классы и приведены примеры.
Вот начальный пример, скопированный
из их документов (тем не менее, всем, кто пытается его редактировать, потому что это ужасный кусок кода, пожалуйста, не копируйте, а не мой, более того, вы никогда не должны редактировать цитаты, если они не изменились. в первоисточнике). Вы можете просто скопировать и запустить его на Java 6+.
package com.stackoverflow.q3732109;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class Test {
public static void main(String args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/test", new MyHandler());
server.setExecutor(null); // creates a default executor
server.start();
}
static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange t) throws IOException {
String response = "This is the response";
t.sendResponseHeaders(200, response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
}
Следует отметить, что часть response.length() в их примере плохая, это должна была быть response.getBytes().length . Даже в этом getBytes() метод getBytes() должен явно указывать кодировку, которую вы затем указываете в заголовке ответа. Увы, хотя и вводящий в заблуждение начинающих, в конце концов, это всего лишь базовый пример.
Выполните его и перейдите по адресу http://localhost: 8000/test, и вы увидите следующий ответ:
Что касается использования com.sun.* , Обратите внимание, что это, в отличие от того, что думают некоторые разработчики, абсолютно не запрещено общеизвестными часто задаваемыми вопросами. Почему разработчики не должны писать программы, называющие "солнечные" пакеты . Этот sun.misc.BASE64Encoder часто задаваемых вопросов касается пакета sun.* (Такого как sun.misc.BASE64Encoder) для внутреннего использования Oracle JRE (который, таким образом, уничтожит ваше приложение при запуске его на другом JRE), а не пакет com.sun.* . Sun/Oracle также просто разрабатывает программное обеспечение поверх API Java SE, как и любая другая компания, такая как Apache и так далее. Использование com.sun.* рекомендуется (но не запрещено), когда это касается реализации
определенного Java API, такого как GlassFish (Java EE impl), Mojarra (JSF impl), Джерси (JAX-RS impl) и т.д.,
Решение com.sun.net.httpserver не переносится через JRE. Лучше использовать официальный API веб-сервисов в javax.xml.ws для загрузки минимального HTTP-сервера...
Import java.io._
import javax.xml.ws._
import javax.xml.ws.http._
import javax.xml.transform._
import javax.xml.transform.stream._
@WebServiceProvider
@ServiceMode(value=Service.Mode.PAYLOAD)
class P extends Provider {
def invoke(source: Source) = new StreamSource(new StringReader("
Hello There!
"));
}
val address = "http://127.0.0.1:8080/"
Endpoint.create(HTTPBinding.HTTP_BINDING, new P()).publish(address)
println("Service running at "+address)
println("Type +[C] to quit!")
Thread.sleep(Long.MaxValue)
EDIT: это действительно работает! Вышеприведенный код выглядит как Groovy или что-то в этом роде. Вот перевод на Java, который я тестировал:
Import java.io.*;
import javax.xml.ws.*;
import javax.xml.ws.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
@WebServiceProvider
@ServiceMode(value = Service.Mode.PAYLOAD)
public class Server implements Provider
Данная страница jsp получает извне объект user и с помощью синтаксиса EL выводит значения его свойств. Стоит обратить внимание, что
здесь идет обращение к переменным name и age, хотя они являются приватными.
В папке Java Resources/src
в файле HelloServlet.java
определен сервлет
HelloServlet:
Import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
User tom = new User("Tom", 25);
request.setAttribute("user", tom);
getServletContext()
.getRequestDispatcher("/user.jsp")
.forward(request, response);
}
}
Сервлет создает объект User. Для передачи его на страницу user.jsp устанавливается атрибут "user" через
вызов request.setAttribute("user", tom) . Далее происходит перенаправление на страницу user.jsp. И,
таким образом, страница получит данные из сервлета.