Лучший способ понять устройство и принцип работы чего-либо – сделать это что-то самому.

Заинтересовавшись однажды сетевыми технологиями и, среди прочего, серверами, я пришёл к мысли, что было бы неплохо написать одн такой сервер самому, используя 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[64*1024];
         
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 mimeType = “text/plain”;

if(ex > 0) { 

String format = path.substring(r); 

if(format.equalsIgnoreCase(".html")) 

mimeType = "text/html";

else if(format.equalsIgnoreCase(".jpeg"))
                    
mimeType = "image/jpeg";

else if(format.equalsIgnoreCase(".gif"))
                    
mimeType = "image/gif";

Формируем ответ клиенту:

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-сервера является одной из простейших, она может быть легко доработана путём добавления графического интерфейса пользователя, количества поддерживаемых расширений, ограничителя подключений и т.п. Одним словом – простор для творчества остаётся огромным. Дерзайте.

Комментарии (7)


  1. vedenin1980
    17.09.2015 10:25
    +1

    Вообще, я бы по советовал использовать что-то вроде org.eclipse.jetty.server, тогда все будет куда проще и мощнее:

    Server server = new Server(port);
    ServletContextHandler handler = new ServletContextHandler(server, "\server_path\");
    handler.addServlet(new ServletHolder(YouServlet1 /* ваш_класс_сервлета 1*/), "\servlet_path1\");
    handler.addServlet(new ServletHolder(YouServlet2 /* ваш_класс_сервлета 2*/), "\servlet_path2\");
    ...
    server.start();
    
    public class YouServlet extends HttpServlet {
    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String prm1 = req.getParameter("параметр1"); // получаем параметр url'a
    resp.setStatus(HttpStatus.OK_200); // отдаем статус
    resp.getWriter().println("печатаем текст html страницы результата"); 
    ...
    }
    

    И на localhost:port\server_path\servlet_path1\ появится результат


  1. dinikin
    17.09.2015 12:35
    +12

    Через ваш веб-сервер можно получить доступ к любому фалу в файловой системе, к которому имеет доступ пользователь, под которым запущен java процесс.
    Это отличный пример, как не надо писать веб-сервер.


  1. Borz
    17.09.2015 15:21
    +3

    для начинающих Java-программистов
    им лучше не начинать сразу вот так свои костыли сервера писать

    Однако большая часть Java-литературы
    потому как читать надо не по Java книги для этого


  1. uvarovalexander
    17.09.2015 17:31

    Суть статьи — не написать полноценный сервер, как было указано в начале, а описать принцип функционирования подобных приложений в самых общих чертах.


  1. Throwable
    21.09.2015 11:44

    Для автора и новичков: использование StringBuilder вместо конкатенации строк всецело приветствуется.



    1. vedenin1980
      21.09.2015 12:16

      Только в меру, так как компиляторы давно уже умеют автоматически менять конкатенации строк на StringBuilder в большинстве простых случаев (если нет сложных циклов и вызовы функций с конкатенацией строк в одну большую). Поэтому, если оптимизация кода действительно очень важна — обязательно используйте StringBuilder, если нет — по желанию.