Главная > Программирование Пример простейшего серверного приложения на JavaПрограмма UploadServer — пример простейшего серверного приложения на языке Java. Приводится исходный код с подробными объяснениями работы каждого фрагмента. Представляет интерес для тех, кто только начинает осваивать программирование на языке Java. Описание программыПрограмма UploadServer — простейший узкоспециализированный web-сервер, предназначенный для закачки файлов на компьютер, на котором он запущен. Он был написан мною на языке Java, причем я преследовал две цели: сделать программу, которой удобно перекидывать файлы в локальной (и не только) сети без установки и настройки громоздкого софта, а также сделать простой, но функциональный и богатый разными «фичами» пример серверного приложения на Java. Порядок работы с программой не менее прост, чем она сама. Вы запускаете программу, указав в командной строке номер порта (он должен быть разрешен в фаерволе для доступа снаружи), после чего с компьютера, где есть файлы для загрузки, заходите на свой компьютер обычным web-браузером. В появившейся форме выбираете нужный файл кнопкой "Browse..." и нажимаете кнопку "Upload". После успешной загрузки файла будет отображена страница с указанием размера и MD5-суммы загруженного файла. Для работы программы требуется Java Runtime Environment версии не ниже 1.4 (я проверял на 1.6, но если верить документации, все необходимые функции появились не позже 1.4). Скачать последнюю версию платформу можно с официального сайта Java. Если вы планируете не только смотреть на программу, но и модифицировать ее, то вам потребуется еще и Java-компилятор, который вместе с прочими полезными в разработке утилитами входит в состав Java Development Kit (JDK), который можно скачать там же. Скачать программуПрограмма распространяется совершенно бесплатно, ее текст может быть полностью или частично использован в любых приложениях при условии сохранения моего имени и ссылки на данный сайт в исходных текстах. Я не несу никакой ответственности за любое использование программы и не даю никаких гарантий относительно ее работоспособности, применимости в конкретной ситуации или отсутствия в ней ошибок. Программа поставляется в виде архива содержащего исходный текст (файл UploadServer.java) и откомпилированный класс (файл UploadServer.class) Программа UploadServer (архив Zip, 5 k)Для запуска программы наберите в каталоге с откомпилированным файлом UploadServer.class следующую строку: java -cp . UploadServer <номер порта>
Разбор исходного текста программыЕсли вас интересует только применение программы, то дальше можно не читать — скачали и пользуйтесь на здоровье. Если же вы хотите на этом простом примере разобраться в создании сетевых приложений на Java, дальше я подробно, практически построчно разберу устройство программы. Сразу предупреждаю, что я не претендую на правильность стиля программирования, оптимальный выбор алгоритмов и т.п. Просто моя собственная практика обучения программированию говорит о том, что один из наиболее продуктивных путей, это изучение чужих программ и параллельное написание своих, сперва по образу и подобию, а затем полностью самостоятельно. За каждым блоком кода следует короткий комментарий о его назначении. Сведения о программе, об авторе, а также лицензионное соглашение.
/**
UploadServer program - simple file upload web-server
Author: Denis Volkov (c) 2007
For information and support visit http://www.denvo.ru
This program is FREEWARE, you are free to use any part of code
as well as whole program in your development provided that you
remain original copyrights and site name in your source.
The product is distributed "as is". The author of the Product will
not be liable for any consequences, loss of profit or any other kind
of loss, that may occur while using the product itself and/or together
with any other software.
*/
Хорошим тоном считается вставлять эти строки в каждый файл исходного кода, хотя в случае больших проектов лицензию удобнее вынести в отдельный файл, а в исходниках оставить только упоминание об этом файле. Импорт используемых классов
import java.net.*;
import java.io.*;
import java.security.*;
import java.util.*;
import java.util.regex.*;
import java.nio.charset.*;
Программа на языке Java состоит из классов, которые группируются в пакеты, образуя иерархическую структуру наподобие файловой системы. При использовании классов из других пакетов, их необходимо импортировать с помощью директивы import. В качестве параметра можно указывать имя конкретного класса или звездочку после имени пакета, что будет означать импорт всех классов из пакета. Заголовок класса и определение переменных
public class UploadServer extends Thread
{
private final static String httpHeader = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html\r\n"
+ "\r\n";
private final static String uploadFormString = httpHeader
+ "<form method=\"POST\" enctype=\"multipart/form-data\">\r\n"
+ "<input type=\"file\" name=\"upload_file\" size=100>\r\n"
+ "<input type=\"submit\" value=\"Upload\">\r\n"
+ "</form>\r\n\r\n";
private final Charset streamCharset = Charset.forName("ISO-8859-1");
private ServerSocket serverSocket;
private Socket clientSocket;
Описание класса начинается с его заголовка, в котором указывается имя класса, тип доступа к нему, имя базового
класса при наследовании, имена интерфейсов, реализуемых классом. В нашем случае описывается публичный класс (чтобы его
можно было указывать в качестве стартового), который унаследован от класса Thread, поэтому является потоком. Конструктор класса
UploadServer(int port) throws Exception
{
serverSocket = new ServerSocket(port);
System.err.println("Server ready @" + port);
}
Конструктор — специальная функция, вызываемая при создании объекта. Здесь конструктор получает в качестве аргумента номер порта, на котором будет создан сервер, создает серверный сокет, слушающий указанный порт, и печатает сообщение об успешном запуске. Функция, с которой начинается исполнение потока
public void run()
{
while(true)
{
try
{
clientSocket = serverSocket.accept();
System.err.println("Client connection accepted from "
+ clientSocket.getInetAddress().toString());
// Read and process data from socket
processConnection();
}
catch(Exception e)
{
System.err.println("Exception in run's main loop");
e.printStackTrace();
}
// Close socket
try
{
clientSocket.close();
}
catch(Exception e) { }
System.err.println("Client connection closed");
}
}
Функция run() вызывается из базового класса Thread в созданном новом потоке. В ней создается бесконечный цикл
приема соединений от клиентов. В начале вызывается функция accept серверного сокета, которая ожидает входящего соединения
и возвращает сокет для созданного соединения. Адрес клиента, возвращенный функцией getInetAddress(), используется в
выдаваемом диагностическом сообщении, затем вызывается processConnection() для приема и обработки данных от клиентов. Когда
последняя возвратит управление, соединение с клиентом завершается и программа переходит к ожиданию нового соединения. Функция обработки соединения с клиентом — определение переменных
private void processConnection() throws Exception
{
InputStream inStream = clientSocket.getInputStream();
BufferedReader in = new BufferedReader(
new InputStreamReader(inStream, streamCharset));
OutputStream out = clientSocket.getOutputStream();
Вначале создаются объекты для обмена данными с клиентом. Для чтения данных, отправляемых клиентом, сокет
предоставляет поток, унаследованный от класса InputStream (мы не знаем, какой будет класс у реального объекта, но нам
это и не нужно, т.к. InputStream предоставляет нам необходимый абстрактный интерфейс для чтения данных). Чтобы читать
данные из сокета построчно, мы создаем цепочку читателей: первый InputStreamReader читает байты из потока и преобразует
их в символы, используя streamCharset, второй BufferedReader озволяет читать строчку вместо посимвольного чтения с
проверкой конца строки. Используемый Charset ISO-8859-1 производит перевод байтов в символы и наоборот «1 в
1», поэтому его можно использовать для двоичных файлов без риска искажения данных. Чтение заголовка клиентского запроса
// Read requiest header
String headerLine, firstLine = null;
Map headerData = new TreeMap();
Pattern headerPattern = Pattern.compile("([^\\:]+)\\:(.*)");
while((headerLine = in.readLine()).length() > 0)
{
if(firstLine == null)
firstLine = headerLine;
else
{
Matcher m = headerPattern.matcher(headerLine);
if(m.matches())
{
headerData.put(m.group(1).trim(), m.group(2).trim());
}
}
System.out.println("HEADER: " + headerLine);
}
Заголовок запроса в протоколе
HTTP состоит из первой строки формата Обработка запроса типа "GET"
// Process first line of request
if(firstLine.startsWith("GET"))
{
System.out.println("Send upload form");
// Show upload form
out.write(uploadFormString.getBytes());
}
Если первая строка запроса начинается со строки "GET", клиенту просто отправляется заранее сформированный ответ с HTML-документом, содержащим форму для отправки файла. Обработка запроса типа "POST", определение переменных
else if(firstLine.startsWith("POST"))
{
// Get body info
int contentLength = Integer.parseInt((String)(headerData.get("Content-Length")));
String contentType = (String)(headerData.get("Content-Type"));
String boundary = "\r\n--"
+ contentType.substring(contentType.indexOf("boundary=") + 9)
+ "--";
System.out.println("File upload, reading body: " + contentLength + " bytes");
// Prepare to reading
Pattern fileNamePattern = Pattern.compile("filename=\"([^\"]+)\"");
OutputStreamWriter writer = null;
MessageDigest digestMD5 = MessageDigest.getInstance("MD5");
String fileName = null;
String prevBuffer = "";
char[] buffer = new char[16 << 10];
int totalLength = contentLength;
В случае, если тип запроса клиента начинается со строки "POST", мы предполагаем, что была отправлена форма с загружаемым файлом. Вначале по параметру Content-Length из заголовка определяется длина передаваемых данных. Далее, поскольку данные будут передаваться клиентом в формате MIME, необходимо получить разделитель из параметра Content-Type и сформировать из него строку-разделитель для определения конца передаваемого файла (приложение А второй части описания формата MIME ). Для поиска оригинального имени файла (на клиентской машине) формируется регулярное выражение в переменной fileNamePattern. Для подсчета контрольной суммы принятого файла создается объект MessageDigest с использованием алгоритма хэширования MD5. Также определяются переменные для записывателя (writer), имени файла, создается буфер для чтения файла buffer. Цикл чтения тела запроса, обработка заголовка формы
// Reading loop
while(contentLength > 0)
{
if(writer == null)
{
// Read strings
String bodyLine = in.readLine();
contentLength -= bodyLine.length() + 2;
// Find name of file
if(bodyLine.length() > 0)
{
Matcher m = fileNamePattern.matcher(bodyLine);
if(m.find())
{
fileName = m.group(1);
}
}
else if(fileName != null)
{
OutputStream stream = new FileOutputStream(fileName);
if(digestMD5 != null)
stream = new DigestOutputStream(stream, digestMD5);
writer = new OutputStreamWriter(stream, streamCharset);
}
else
throw new RuntimeException("Name of uploaded file not found");
}
При отправке формы по HTTP в формате multipart/form-data для каждого поля file в форме создается блок, разделенный
строками-ограничителями, который состоит из текстовых полей с именем файла, а также отделенным от них пустой строкой
блоком двоичных данных самого файла. В данной части кода мы читаем текстовые поля и ищем в них имя файла с помощью
регулярного выражения. Поскольку в данном случае регулярное выражение охватывает не всю строку, а только ее часть, для
проверки наличия подстроки используется метод find() вместо метода matches() в разборе заголовка запроса. Чтение данных отправленного файла и запись их на диск
else
{
// Read data from stream
int readLength = Math.min(contentLength, buffer.length);
readLength = in.read(buffer, 0, readLength);
if(readLength < 0)
break;
contentLength -= readLength;
// Find boundary string
String curBuffer = new String(buffer, 0, readLength);
String bothBuffers = prevBuffer + curBuffer;
int boundaryPos = bothBuffers.indexOf(boundary);
if(boundaryPos == -1)
{
writer.write(prevBuffer, 0, prevBuffer.length());
prevBuffer = curBuffer;
}
else
{
writer.write(bothBuffers, 0, boundaryPos);
break;
}
}
// Write stats
System.out.print("Read: " + (totalLength - contentLength)
+ " Remains: " + contentLength + " Total: " + totalLength
+ " bytes \r");
}
После того, как были приняты текстовые строки из тела сообщения и создан записыватель, в каждой итерации цикла
происходит чтение данных в буфер и поиск среди прочитанных данных сигнатуры конца данных файла, сформированной ранее.
Поскольку сигнатура может встретиться на границе двух прочитанных порций данных, используется двойная буферизация с
использованием строки prevBuffer: сигнатура ищется в строке bothBuffers, составленной из предыдущей и новой порций
прочитанных данных, и в случае, если она не найдена, в файл записываются данные из предыдущей порции. Если же сигнатура
найдена, в файл записывается остаток составного буфера до сигнатуры и цикл завершается. Завершение приема файла, отправка ответа клиенту
System.out.println("Done ");
writer.close();
// Finalize digest calculation
byte[] md5Sum = digestMD5.digest();
StringBuffer md5SumString = new StringBuffer();
for(int n = 0; n < md5Sum.length; ++ n)
md5SumString.append(printByte(md5Sum[n]));
// Output client info
String answer = httpHeader + "<p><b>Upload completed, " + totalLength
+ " bytes, MD5 sum: " + md5SumString.toString() + "</b>"
+ "<p><a href=\"/\">Next file</a>\r\n\r\n";
out.write(answer.getBytes());
}
}
После завершения цикла чтения закрывается записыватель и выводится диагностическое сообщение. Затем вычисляется контрольная сумма MD5, полученный массив байт преобразуется в шестнадцатеричную строку и формируется ответ клиенту об успешной загрузке файла. Служебная функция перевода байта в шестнадцатеричную строку
private static String printByte(byte b)
{
int bi = ((int)b) & 0xFF;
if(bi < 16)
return "0" + Integer.toHexString(bi);
else
return Integer.toHexString(bi);
}
Функция используется для печати MD5-суммы в ответе клиенту. Она получает байт, преобразует его в целое с отбрасыванием старших байтов (поскольку байт — знаковый тип, то в случае отрицательных значений у целого старшие байты будут иметь значение 0xFF). Затем используется статический метод toHexString класса Integer, который преобразует целое в шестнадцатеричную строку. Этот метод печатает только значащие цифры, поэтому для чисел, меньших 16, необходимо дополнить полученную строку символом "0" слева. Функция main
public static void main(String[] args)
{
try
{
if(args.length < 1)
{
System.out.println("Usage: UploadServer <port>");
return;
}
UploadServer server = new UploadServer(Integer.parseInt(args[0]));
server.start();
}
catch(Exception e)
{
System.err.println("Error in main");
e.printStackTrace();
}
}
}
Исполнение программы на языке Java начинается с функции main класса, который указан в качестве стартового в
командной строке. Это статическая функция, она вызывается, когда ни одного объекта данного класса еще не создано. Функция
должна быть описана именно так, как в данном примере, иначе при запуске программы возникнет ошибка
java.lang.NoSuchMethodError (не найден метод). Полезные ссылки
Материал взят с сайта: http://www.denvo.ru/pub/programming/upload-server.html Главная > Программирование |