Страницы

пятница, 19 августа 2016 г.

Node.js HTTP/2

На прошлой неделе среди разработчиков Node.js началось обсуждение реализации протокола HTTP/2. Ждать появления HTTP/2 в Node.js core еще очень долго, но уже сейчас можно затестить фичи этого протокола с помощью модуля node-spdy, который работает как с протоколом SPDY - предшественником HTTP/2, который кстати Google Chrome больше не поддерживает, так и с HTTP/2, обязательно поверх TLS. Расскажу как я тестил фичу по имени HTTP/2 Server Push.


Создаем каталог для статики с файлами следующего содержания:
- index.html
<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="UTF-8">
  <link href="style.css" rel="stylesheet">
  <title>HTTP/2</title>
</head>
 
<body>
  <h2>Hello, world!</h2>
  <img src="favicon.png" alt="Здесь могла бы быть ваша реклама">
  <script src="script.js"></script>
</body>
 
</html>

- style.css:
h2 {
  color: blue;
}
.italic {
  font-style: italic;
}      

- script.js:
document.addEventListener('DOMContentLoaded', function () {
  document.querySelector('h2').className = 'italic';
});

Пишем простой http-сервер для раздачи нашей статики:
- http.js
 
'use strict';
 
const http = require('http');
 
const fs = require('fs');
 
const routes = new Map([
  ['/', {
    head: {
      'Content-Type': 'text/html'
    },
    body: fs.readFileSync('./static/index.html')
  }],
  ['/style.css', {
    head: {
      'Content-Type': 'text/css'
    },
    body: fs.readFileSync('./static/style.css')
  }],
  ['/script.js', {
    head: {
      'Content-Type': 'application/javascript'
    },
    body: fs.readFileSync('./static/script.js')
  }]
]);
 
http.createServer((req, res) => {
  console.log('req.url:', req.url);
  const route = routes.get(req.url);
  if (route) {
    res.writeHead(200, route.head);
    res.end(route.body);
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000, (err) => {
  if (err) throw err;
  console.log('Listen on port 3000');
});
 
Запускаем сервер, идем по адресу http://localhost:3000, получаем:

"Реклама" появится ближе к финалу.

Консоль сервера выглядит следующим образом:

Вкладка Network в DevTools выглядит примерно так:


Создаем ключи для https:
В коде сервера меняем http на https:
- https.js
 
'use strict';
 
const https = require('https');
 
const fs = require('fs');
 
const opts = {
  key: fs.readFileSync('cert/dummy.pem'),
  cert: fs.readFileSync('cert/dummy.crt')
};
 
const routes = new Map([
  ['/', {
    head: {
      'Content-Type': 'text/html'
    },
    body: fs.readFileSync('./static/index.html')
  }],
  ['/style.css', {
    head: {
      'Content-Type': 'text/css'
    },
    body: fs.readFileSync('./static/style.css')
  }],
  ['/script.js', {
    head: {
      'Content-Type': 'application/javascript'
    },
    body: fs.readFileSync('./static/script.js')
  }]
]);
 
https.createServer(opts, (req, res) => {
  console.log('req.url:', req.url);
  const route = routes.get(req.url);
  if (route) {
    res.writeHead(200, route.head);
    res.end(route.body);
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000, (err) => {
  if (err) throw err;
  console.log('Listen on port 3000');
});
 

Запускаем сервер, идем по адресу https://localhost:3000, не обращаем внимание на предупреждения браузера, получаем ту же самую страничку. На сервере и на клиенте ничего особенно не изменилось:


Устанавливаем node-spdy:

В коде сервера меняем https на spdy:
- spdy.js
 
'use strict';
 
const spdy = require('spdy');
 
const fs = require('fs');
 
const opts = {
  key: fs.readFileSync('cert/dummy.pem'),
  cert: fs.readFileSync('cert/dummy.crt')
};
 
const routes = new Map([
  ['/', {
    head: {
      'Content-Type': 'text/html'
    },
    body: fs.readFileSync('./static/index.html')
  }],
  ['/style.css', {
    head: {
      'Content-Type': 'text/css'
    },
    body: fs.readFileSync('./static/style.css')
  }],
  ['/script.js', {
    head: {
      'Content-Type': 'application/javascript'
    },
    body: fs.readFileSync('./static/script.js')
  }]
]);
 
spdy.createServer(opts, (req, res) => {
  console.log('req.url:', req.url);
  const route = routes.get(req.url);
  if (route) {
    res.writeHead(200, route.head);
    res.end(route.body);
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000, (err) => {
  if (err) throw err;
  console.log('Listen on port 3000');
});
 
Запускаем сервер, обновляем страничку в браузере:


HTTP/2 in action!

Попробуем HTTP/2 Server Push:
spdy-push.js
 
'use strict';
 
const spdy = require('spdy');
 
const fs = require('fs');
 
const opts = {
  key: fs.readFileSync('cert/dummy.pem'),
  cert: fs.readFileSync('cert/dummy.crt')
};
 
const routes = new Map([
  ['/', {
    head: {
      'Content-Type': 'text/html'
    },
    body: fs.readFileSync('./static/index.html')
  }],
  ['/style.css', {
    head: {
      'Content-Type': 'text/css'
    },
    body: fs.readFileSync('./static/style.css')
  }],
  ['/script.js', {
    head: {
      'Content-Type': 'application/javascript'
    },
    body: fs.readFileSync('./static/script.js')
  }]
]);
 
const onError = (err) => console.error(err);
 
spdy.createServer(opts, (req, res) => {
  console.log('req.url:', req.url);
  const route = routes.get(req.url);
  if (route) {
    if (req.url === '/' && typeof res.push === 'function') {
      const style = routes.get('/style.css');
      res.push('/style.css', {
        response: style.head
      })
        .on('error', onError)
        .end(style.body);
      const script = routes.get('/script.js');
      res.push('/script.js', {
        response: script.head
      })
        .on('error', onError)
        .end(script.body);
    }
    res.writeHead(200, route.head);
    res.end(route.body);
  }
  else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000, (err) => {
  if (err) throw err;
  console.log('Listen on port 3000');
});
 
Запускаем сервер, обновляем страничку в браузере:


Bingo!

Финальный номер - стримминг "рекламы":
spdy-push-pipe.js
 
'use strict';
 
const spdy = require('spdy');
 
const fs = require('fs');
 
const opts = {
  key: fs.readFileSync('cert/dummy.pem'),
  cert: fs.readFileSync('cert/dummy.crt')
};
 
const routes = new Map([
  ['/', {
    head: {
      'Content-Type': 'text/html'
    },
    body: fs.readFileSync('./static/index.html')
  }],
  ['/style.css', {
    head: {
      'Content-Type': 'text/css'
    },
    body: fs.readFileSync('./static/style.css')
  }],
  ['/script.js', {
    head: {
      'Content-Type': 'application/javascript'
    },
    body: fs.readFileSync('./static/script.js')
  }]
]);
 
const https = require('https');
const PassThrough = require('stream').PassThrough;
const request = (uri) => {
  const pass = new PassThrough();
  https.request(uri, (res) => {
    pass.emit('response', res);
    res.on('error', (err) => pass.emit('error', err));
    res.pipe(pass);
  })
    .on('error', (err) => pass.emit('error', err))
    .end();
  return pass;
};
 
const onError = (err) => console.error(err);
 
spdy.createServer(opts, (req, res) => {
  console.log('req.url:', req.url);
  const route = routes.get(req.url);
  if (route) {
    if (req.url === '/' && typeof res.push === 'function') {
      const rs = request('https://nodejs.org/static/favicon.png');
      const ws = res.push('/favicon.png', {
        response: {
          'Content-Type': 'image/x-icon'
        }
      });
      rs.on('error', onError);
      ws.on('error', onError);
      rs.pipe(ws);
 
      const style = routes.get('/style.css');
      res.push('/style.css', {
        response: style.head
      })
        .on('error', onError)
        .end(style.body);
      const script = routes.get('/script.js');
      res.push('/script.js', {
        response: script.head
      })
        .on('error', onError)
        .end(script.body);
    }
    res.writeHead(200, route.head);
    res.end(route.body);
  }
  else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000, (err) => {
  if (err) throw err;
  console.log('Listen on port 3000');
});
 
Запускаем сервер, обновляем страничку в браузере:



Номер удался, бурные аплодисменты переходящие в овации.

В браузере не поддерживающем HTTP/2 (я попробовал в IE11) тоже все нормально, разумеется без "рекламы":


Вот как-то так.