Статьи

Узел JS и серверный Java Script

Давайте начнем с самого начала. Имейте в виду, это может занять много времени …

Следующий фрагмент кода Java может быть использован для создания сервера, который получает запросы TCP / IP:

class Server implements Runnable {
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
Socket s = ss.accept();
s.getInputStream(); //read from this
s.getOutputStream(); //write to this
} catch (IOException ex) { /* ... */ }
}
}

Этот код выполняется до строки с ss.accept (), которая блокируется до получения входящего запроса. Затем метод accept возвращается, и у вас есть доступ к входным и выходным потокам для связи с клиентом.

Есть одна проблема с этим кодом. Подумайте о нескольких запросах, поступающих одновременно. Вы посвятили себя выполнению первого запроса перед следующим вызовом метода accept. Зачем? Потому что метод accept блокирует. Если вы решили, что прочитаете чанк из входного потока первого соединения, а затем проявите доброту к следующему соединению, примите его и обработаете свой первый чанк, прежде чем продолжить работу с исходным (первым) соединением, у вас возникнет проблема, потому что блоки метода приема. Если бы не было второго запроса, вы бы не смогли завершить первый запрос, потому что JVM блокирует этот метод accept. Таким образом, вы должны обработать входящий запрос полностью, прежде чем принять второй входящий запрос.

Это не так уж и плохо, потому что вы можете создать ServerSocket с дополнительным параметром, называемым backlog, который сообщает ему, сколько запросов стоит в очереди перед отказом от дальнейших соединений. Пока вы заняты обработкой первого запроса, последующие запросы просто ставятся в очередь.

Эта стратегия будет работать, хотя она не очень эффективна. Если у вас многоядерный процессор, вы будете работать только на одном ядре. Было бы лучше иметь больше потоков, чтобы можно было сбалансировать нагрузку между ядрами (обратите внимание, это зависит от JVM и ОС!).

Более типичный многопоточный сервер создается следующим образом:

class Server implements Runnable {
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
new Thread(new Handler(ss.accept())).start();
// one thread per socket connection every thread
// created this way will essentially block for I/O
} catch (IOException ex) { /* ... */ }
}
}

Приведенный выше код передает каждый входящий запрос новому потоку, позволяя основному потоку обрабатывать новые входящие запросы, а порожденные потоки обрабатывают отдельные запросы. Этот код также балансирует нагрузку на ядра ЦП, где JVM и ОС позволяют это. В идеале мы, вероятно, не будем создавать поток для каждого нового запроса, а просто передадим запрос исполнителю пула потоков (см. Пакет java.util.concurrent). С другой стороны, бывают моменты, когда требуется поток для запроса. Если диалог между сервером и клиентом длится дольше (а не простой HTTP-запрос, который обычно обслуживается в течение от миллисекунд до секунд), сокет может оставаться открытым. Примером того, когда это требуется, являются такие вещи, как серверы чата, VOIP или что-то еще, где требуется постоянный разговор. Но в таких ситуацияхприведенный выше код, даже если он многопоточный, имеет свои ограничения. Эти ограничения на самом деле из-за потоков! Рассмотрим следующий код:

public class MaxThreadTest {

static int numLive = 0;

public static void main(String[] args) {
while(true){
new Thread(new Runnable(){
public void run() {
numLive++;

System.out.println("running " + Thread.currentThread().getName() + " " + numLive);
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}

numLive--;
}
}).start();
}
}
}

Этот код создает кучу потоков, пока процесс не завершится. При размере кучи 64 МБ он падал (не хватало памяти) после 4000 потоков во время тестирования на моем ноутбуке с Windows XP Thinkpad. Я увеличил размер кучи до 256 МБ, и Eclipse потерпел крах в режиме отладки … Я запустил процесс из командной строки и смог открыть 5092 потока, но он был нестабильным и не отвечал. Интересно, что я увеличил размер кучи до 1 ГБ, а затем смог открыть только 2658 потоков … Это показывает, что я не очень понимаю ОС или JVM на этом уровне! В любом случае, если бы мы писали систему для обработки миллиона одновременных разговоров, нам, вероятно, потребовалось бы более двухсот серверов. Но теоретически мы могли бы сократить наши расходы до менее чем 10% от этого, потому что нам разрешено открывать чуть более 65 000 потоков на сервер (ну, скажем, 63,000 к тому времени мы учитываем все порты, используемые ОС и другими процессами). Теоретически мы могли бы получить только 16 серверов на миллион одновременных подключений.

Способ сделать это — использовать неблокирующий ввод / вывод. Начиная с Java 1.4 (около 2002 года?), Пакет java.nio всегда был нам полезен. С его помощью вы можете создать систему, которая обрабатывает много одновременных входящих запросов, используя только один поток. Это работает примерно так: регистрация в ОС для получения событий, когда что-то происходит, например, когда принимается новый запрос или когда один из клиентов отправляет данные по проводам.

С помощью этого API мы можем создать сервер, который, к сожалению, немного сложнее, чем приведенный выше, но который обрабатывает множество сокетов из одного потока:

public class NonBlockingServer2 {

public static void main(String[] args) throws IOException {
System.out.println("Starting NIO server...");
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();

ByteBuffer buffer = ByteBuffer.allocate(512);

Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(30032));
server.configureBlocking(false);
SelectionKey serverkey = server.register(selector, SelectionKey.OP_ACCEPT);

boolean quit = false;
while(!quit) {
selector.select(); //blocks until something arrives, of type OP_ACCEPT
Set keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key == serverkey) {
if (key.isAcceptable()) {
SocketChannel client = server.accept();
if(client != null){ //can be null if theres no pending connection
client.configureBlocking(false);
SelectionKey clientkey = client.register(selector,
SelectionKey.OP_READ); //register for the read event
numConns++;
}
}
} else {
SocketChannel client = (SocketChannel) key.channel();
if (!key.isReadable()){
continue;
}
int bytesread = client.read(buffer);
if (bytesread == -1) {
//whens this happen?
key.cancel();
client.close();
continue;
}
buffer.flip();
String request = decoder.decode(buffer).toString();
buffer.clear();

if (request.trim().equals("quit")) {
client.write(encoder.encode(CharBuffer.wrap("Bye.")));
key.cancel();
client.close();
}else if (request.trim().equals("hello")) {
String id = UUID.randomUUID().toString();
key.attach(id);
String response = id + "\r";
client.write(encoder.encode(CharBuffer.wrap(response)));
}else if (request.trim().equals("time")) {
numTimeRequests++;
String response = "hi " + key.attachment() + " the time here is " + new Date() + "\r";
client.write(encoder.encode(CharBuffer.wrap(response)));
}
}
}
}
System.out.println("done");
}
}

Приведенный выше код основан на найденном здесь, Сокращая количество используемых потоков, и не блокируя, а полагаясь на ОС, которая сообщает нам, когда что-то не так, мы можем обрабатывать гораздо больше запросов. Я протестировал некоторый код, очень похожий на этот, чтобы увидеть, сколько соединений я смог обработать. Windows XP доказала свою высокую надежность, когда воспроизводимо и последовательно более 12 000 подключений приводят к синим экранам смерти! Пора переходить на Linux (Fedora Core). У меня не было проблем с созданием 64 000 клиентов, одновременно подключенных к моему серверу. Позвольте мне перефразировать … У меня не было проблем с тем, чтобы клиенты просто подключались и оставляли соединение открытым, но проблема в том, что сервер обрабатывал только 100 запросов в секунду. Теперь 100 запросов в секунду на веб-сервере, на оборудовании, которым был 5-летний дешевый ноутбук Dell, звучат для меня весьма впечатляюще. Но на сервере с 64,000 одновременных подключений, то есть каждый клиент делает запрос каждые десять минут! Не очень хорошо для приложения VOIP … Скорость соединения также снизилась с 3 миллисекунд при 500 одновременных соединениях до 100 миллисекунд при 60 000 одновременных соединений.

Так что, может быть, мне лучше дойти до сути этой публикации? Несколько дней назад я читал о Voxer и Node.js в The Register . У меня были трудности с этой статьей. Зачем кому-то хотеть построить фреймворк для Javascript на сервере? Я разработал много богатых клиентов, и у меня есть опыт, чтобы понять, как сделать богатую клиентскую разработку. Я также разработал множество богатых интернет-приложений (RIA), которые используют Javascript, и я могу только сказать, что это не самое лучшее. Я не какой-нибудь сценарист или хакер, который не знает, как разрабатывать код Javascript, и я хорошо понимаю проблемы разработки Javascript. И я разработал много и много кода на стороне сервера, в основном на Java, и я ценю то, что Java превосходит Javascript.

Мне кажется, что разработчики Node.js и те, кто следит за ним и использует его, не разбираются в разработке серверов. Хотя написание в Javascript может поначалу быть более быстрым, на мой взгляд, отсутствие инструментов и библиотек по сравнению с Java делает его неконкурентным.

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

Может быть, поэтому я никогда не работал при запуске!

В заключение давайте рассмотрим несколько других моментов. Прежде чем кто-то скажет, что производительность моего примера сервера была плохой, потому что это просто Java, которая работает медленно, позвольте мне прокомментировать. Прежде всего, Java всегда будет быстрее, чем Javascript. Во-вторых, используя top для мониторинга сервера, я заметил, что 50% процессорного времени тратится на то, чтобы ОС определяла, какие события генерировать, а не обрабатывает эти запросы Java.

На указанном выше сервере все работает в одном потоке. Для повышения производительности после поступления запроса его можно передать пулу потоков для ответа. Это поможет сбалансировать нагрузку между несколькими ядрами, что, безусловно, необходимо для подготовки работы сервера.

Пока я в этом, вот цитата с домашней страницы Node JS:

«Но как насчет параллельности нескольких процессоров? Разве потоки не нужны для масштабирования программ на многоядерные компьютеры? Процессы необходимы для масштабирования до многоядерных компьютеров, а не потоков с общим доступом к памяти. Основой масштабируемых систем являются быстрые сети и не — блокирующий дизайн — остальное — передача сообщений. В будущих версиях Node сможет раскошелиться на новые процессы (используя API Web Workers), которые хорошо вписываются в текущий дизайн ».

На самом деле, я не уверен … Java на Linux может распределять потоки по ядрам, поэтому отдельные процессы на самом деле не требуются. И вышеприведенное утверждение просто доказывает, что Node JS не подходит для создания действительно профессиональных систем — я имею в виду, нет поддержки потоков ?!

Итак, в интересах завершения, вот клиентское приложение, которое я использовал для подключения к серверу:

public class Client {

private static final int NUM_CLIENTS = 3000;

static Timer serverCallingTimer = new Timer("servercaller", false);

static Random random = new Random();

/**
* this client is asynchronous, because it does not wait for a full response before
* opening the next socket.
*/
public static void main(String[] args) throws UnknownHostException, IOException, InterruptedException {

final InetSocketAddress endpoint = new InetSocketAddress("192.168.1.103", 30032);
System.out.println(new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()) + " Starting async client");

long start = System.nanoTime();
for(int i = 0; i < NUM_CLIENTS; i++){
startConversation(endpoint);
}

System.out.println(new SimpleDateFormat("HH:mm:ss.SSS")
.format(new Date())
+ "Done, averaging "
+ ((System.nanoTime() - start) / 1000000.0 / NUM_CLIENTS)
+ "ms per call");
}

protected static void startConversation(InetSocketAddress endpoint) throws IOException {
final Socket s = new Socket();
s.connect(endpoint, 0/*no timeout*/);
s.getOutputStream().write(("hello\r").getBytes("UTF-8")); //protocol dictates \r is end of command
s.getOutputStream().flush();

//read response
String str = readResponse(s);
System.out.println("New Client: Session ID " + str);

//send a request at regular intervals, keeping the same socket! eg VOIP
//we cannot use this thread, its the main one which created the socket
//simply create another task to be carried out by the scheduler at a later time

//the interval below is 4 minutes, otherwise the server gets REALLY slow handling
//so many requests. This is equivalent to ~260 reqs/sec

serverCallingTimer.scheduleAtFixedRate(
new ConversationContainer(s, str),
random.nextInt(240000/*in the next 4 mins*/),
240000L/*every 4 mins*/);
}

private static String readResponse(Socket s) throws IOException {
InputStream is = s.getInputStream();
int curr = -1;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((curr = is.read()) != -1){
if(curr == 13) break; //protocol dictates a new line is the end of a response
baos.write(curr);
}
return baos.toString("UTF-8");
}

private static class ConversationContainer extends TimerTask {
Socket s;
String id;
public ConversationContainer(Socket s, String id){
this.s = s;
this.id = id;
}

@Override
public void run() {
try {
s.getOutputStream().write("time\r".getBytes("UTF-8")); //protocol dictates \r is end of command
s.getOutputStream().flush();

String response = readResponse(s);

if(random.nextInt(1000) % 1000 == 0){
//we dont want to log everything, because it will kill our server!
System.out.println(id + " - server time is '" + response + "'");
}

} catch (Exception e) {
e.printStackTrace();
}
}
}
}

 

 

От http://blog.maxant.co.uk/pebble/2011/03/05/1299360960000.html