Pelayan HTTP dengan Netty

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melaksanakan pelayan casing atas sederhana melalui HTTP dengan Netty , kerangka tak segerak yang memberi kita fleksibiliti untuk mengembangkan aplikasi jaringan di Java.

2. Pelayan Bootstrapping

Sebelum memulakan, kita harus mengetahui konsep asas Netty, seperti saluran, pengendali, pengekod, dan penyahkod.

Di sini kita akan terus memasuki bootstrap pelayan, yang kebanyakannya sama dengan pelayan protokol sederhana:

public class HttpServer { private int port; private static Logger logger = LoggerFactory.getLogger(HttpServer.class); // constructor // main method, same as simple protocol server public void run() throws Exception { ... ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new HttpRequestDecoder()); p.addLast(new HttpResponseEncoder()); p.addLast(new CustomHttpServerHandler()); } }); ... } } 

Jadi, di sini hanya childHandler berbeza mengikut protokol yang ingin kita laksanakan , yang merupakan HTTP untuk kita.

Kami menambah tiga pengendali ke saluran paip pelayan:

  1. Netty's HttpResponseEncoder - untuk bersiri
  2. Netty's HttpRequestDecoder - untuk deserialisasi
  3. CustomHttpServerHandler kami sendiri - untuk menentukan tingkah laku pelayan kami

Mari lihat pengendali terakhir secara terperinci seterusnya.

3. CustomHttpServerHandler

Tugas pengendali khusus kami adalah memproses data masuk dan menghantar respons.

Mari memecahkannya untuk memahami cara kerjanya.

3.1. Struktur Penangan

CustomHttpServerHandler meluaskan SimpleChannelInboundHandler abstrak Netty dan melaksanakan kaedah kitaran hayatnya:

public class CustomHttpServerHandler extends SimpleChannelInboundHandler { private HttpRequest request; StringBuilder responseData = new StringBuilder(); @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) { // implementation to follow } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }

Seperti yang ditunjukkan oleh nama kaedah, channelReadComplete menyingkirkan konteks pengendali setelah mesej terakhir di saluran habis digunakan sehingga tersedia untuk mesej masuk berikutnya. Kaedah pengecualianCaught adalah untuk menangani pengecualian jika ada.

Setakat ini, semua yang kita lihat ialah kod plat boiler

Sekarang mari kita teruskan dengan perkara menarik, pelaksanaan channelRead0 .

3.2. Membaca Saluran

Kes penggunaan kami mudah, pelayan hanya akan mengubah parameter permintaan dan parameter pertanyaan, jika ada, menjadi huruf besar. Berhati-hati di sini untuk menunjukkan data permintaan sebagai tindak balas - kami melakukan ini hanya untuk tujuan demonstrasi, untuk memahami bagaimana kami dapat menggunakan Netty untuk melaksanakan pelayan HTTP.

Di sini, kami akan menggunakan mesej atau permintaan, dan menyiapkan responsnya seperti yang disarankan oleh protokol (perhatikan bahawa RequestUtils adalah sesuatu yang akan kami tulis sebentar lagi):

if (msg instanceof HttpRequest) { HttpRequest request = this.request = (HttpRequest) msg; if (HttpUtil.is100ContinueExpected(request)) { writeResponse(ctx); } responseData.setLength(0); responseData.append(RequestUtils.formatParams(request)); } responseData.append(RequestUtils.evaluateDecoderResult(request)); if (msg instanceof HttpContent) { HttpContent httpContent = (HttpContent) msg; responseData.append(RequestUtils.formatBody(httpContent)); responseData.append(RequestUtils.evaluateDecoderResult(request)); if (msg instanceof LastHttpContent) { LastHttpContent trailer = (LastHttpContent) msg; responseData.append(RequestUtils.prepareLastResponse(request, trailer)); writeResponse(ctx, trailer, responseData); } } 

Seperti yang dapat kita lihat, ketika saluran kita menerima HttpRequest , ia terlebih dahulu memeriksa apakah permintaan tersebut menjangkakan status 100 Lanjutkan. Sekiranya demikian, kami segera menulis semula dengan jawapan kosong dengan status TERUS :

private void writeResponse(ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); ctx.write(response); }

Setelah itu, pengendali menginisialisasi rentetan untuk dikirim sebagai respons dan menambahkan parameter permintaan permintaan untuk dikirim kembali sebagaimana adanya.

Mari sekarang tentukan format kaedah Param dan letakkan di kelas pembantu RequestUtils untuk melakukan itu:

StringBuilder formatParams(HttpRequest request) { StringBuilder responseData = new StringBuilder(); QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); Map
      
        params = queryStringDecoder.parameters(); if (!params.isEmpty()) { for (Entry
       
         p : params.entrySet()) { String key = p.getKey(); List vals = p.getValue(); for (String val : vals) { responseData.append("Parameter: ").append(key.toUpperCase()).append(" = ") .append(val.toUpperCase()).append("\r\n"); } } responseData.append("\r\n"); } return responseData; }
       
      

Seterusnya, setelah menerima HttpContent , kami mengambil badan permintaan dan mengubahnya menjadi huruf besar :

StringBuilder formatBody(HttpContent httpContent) { StringBuilder responseData = new StringBuilder(); ByteBuf content = httpContent.content(); if (content.isReadable()) { responseData.append(content.toString(CharsetUtil.UTF_8).toUpperCase()) .append("\r\n"); } return responseData; }

Juga, jika HttpContent yang diterima adalah LastHttpContent , kami menambah mesej selamat tinggal dan header tertinggal, jika ada:

StringBuilder prepareLastResponse(HttpRequest request, LastHttpContent trailer) { StringBuilder responseData = new StringBuilder(); responseData.append("Good Bye!\r\n"); if (!trailer.trailingHeaders().isEmpty()) { responseData.append("\r\n"); for (CharSequence name : trailer.trailingHeaders().names()) { for (CharSequence value : trailer.trailingHeaders().getAll(name)) { responseData.append("P.S. Trailing Header: "); responseData.append(name).append(" = ").append(value).append("\r\n"); } } responseData.append("\r\n"); } return responseData; }

3.3. Menulis Respons

Sekarang data kami yang akan dikirim sudah siap, kami dapat menulis respons ke ChannelHandlerContext :

private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) { boolean keepAlive = HttpUtil.isKeepAlive(request); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, ((HttpObject) trailer).decoderResult().isSuccess() ? OK : BAD_REQUEST, Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8)); httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); if (keepAlive) { httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write(httpResponse); if (!keepAlive) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } }

Dalam kaedah ini, kami membuat FullHttpResponse dengan versi HTTP / 1.1, menambahkan data yang telah kami siapkan sebelumnya.

Sekiranya permintaan ingin tetap hidup, atau dengan kata lain, jika sambungan tidak akan ditutup, kami menetapkan tajuk sambungan respons sebagai tetap hidup . Jika tidak, kami menutup sambungan.

4. Menguji Pelayan

Untuk menguji pelayan kami, mari hantar beberapa arahan cURL dan lihat tindak balasnya.

Sudah tentu, kita perlu memulakan pelayan dengan menjalankan kelas HttpServer sebelum ini .

4.1. DAPATKAN Permintaan

Mari mula-mula memanggil pelayan, memberikan permintaan dengan kuki:

curl //127.0.0.1:8080?param1=one

Sebagai tindak balas, kami mendapat:

Parameter: PARAM1 = ONE Good Bye! 

Kita juga boleh menekan //127.0.0.1:8080?param1=one dari mana-mana penyemak imbas untuk melihat hasil yang sama.

4.2. Permintaan POST

Sebagai ujian kedua kami, mari hantar POST dengan kandungan sampel badan :

curl -d "sample content" -X POST //127.0.0.1:8080

Inilah tindak balasnya:

SAMPLE CONTENT Good Bye!

Kali ini, kerana permintaan kami berisi isi, pelayan menghantarnya kembali dengan huruf besar .

5. Kesimpulan

Dalam tutorial ini, kami melihat bagaimana menerapkan protokol HTTP, terutama pelayan HTTP menggunakan Netty.

HTTP / 2 di Netty menunjukkan pelaksanaan pelayan klien dari protokol HTTP / 2.

Seperti biasa, kod sumber tersedia di GitHub.