Server HTTP dengan Netty

1. Ikhtisar

Dalam tutorial ini, kita akan menerapkan server huruf besar sederhana melalui HTTP dengan Netty , kerangka kerja asinkron yang memberi kita fleksibilitas untuk mengembangkan aplikasi jaringan di Java.

2. Bootstrap Server

Sebelum kita mulai, kita harus mengetahui konsep dasar Netty, seperti saluran, handler, encoder, dan decoder.

Di sini kita akan langsung beralih ke bootstrap server, yang sebagian besar sama dengan server 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 yang berbeda sesuai protokol yang ingin kita terapkan , yaitu HTTP untuk kita.

Kami menambahkan tiga penangan ke pipeline server:

  1. Netty's HttpResponseEncoder - untuk serialisasi
  2. HttpRequestDecoder Netty - untuk deserialisasi
  3. CustomHttpServerHandler kita sendiri - untuk menentukan perilaku server kita

Selanjutnya mari kita lihat handler terakhir secara mendetail.

3. CustomHttpServerHandler

Tugas penangan khusus kita adalah memproses data masuk dan mengirim tanggapan.

Mari kita pecahkan untuk memahami cara kerjanya.

3.1. Struktur Penangan

CustomHttpServerHandler memperluas SimpleChannelInboundHandler abstrak Netty dan mengimplementasikan metode siklus hidupnya:

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 disarankan oleh nama metode, channelReadComplete menghapus konteks penangan setelah pesan terakhir di saluran digunakan sehingga tersedia untuk pesan masuk berikutnya. Metode exceptionCaught adalah untuk menangani pengecualian jika ada.

Sejauh ini, yang kita lihat hanyalah kode boilerplate.

Sekarang mari kita lanjutkan dengan hal-hal menarik, implementasi channelRead0 .

3.2. Membaca Channel

Kasus penggunaan kami sederhana, server hanya akan mengubah badan permintaan dan parameter kueri, jika ada, menjadi huruf besar. Perhatian di sini untuk merefleksikan data permintaan dalam tanggapan - kami melakukan ini hanya untuk tujuan demonstrasi, untuk memahami bagaimana kami dapat menggunakan Netty untuk mengimplementasikan server HTTP.

Di sini, kita akan menggunakan pesan atau permintaan, dan mengatur tanggapannya seperti yang direkomendasikan oleh protokol (perhatikan bahwa RequestUtils adalah sesuatu yang akan kita 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 bisa kita lihat, ketika saluran kita menerima HttpRequest , pertama kali memeriksa apakah permintaan mengharapkan status 100 Lanjutkan. Dalam hal ini, kami segera membalas dengan tanggapan kosong dengan status LANJUTKAN :

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

Setelah itu, penangan menginisialisasi string yang akan dikirim sebagai respons dan menambahkan parameter kueri permintaan ke dalamnya untuk dikirim kembali sebagaimana adanya.

Sekarang mari kita tentukan metode formatParams 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; }
       
      

Selanjutnya, saat menerima HttpContent , kami mengambil isi 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; }

Selain itu, jika HttpContent yang diterima adalah LastHttpContent , kita menambahkan pesan selamat tinggal dan header tambahan, 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 Tanggapan

Sekarang data kita yang akan dikirim siap, kita dapat menulis respon 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 metode ini, kami membuat FullHttpResponse dengan versi HTTP / 1.1, menambahkan data yang telah kami siapkan sebelumnya.

Jika permintaan akan tetap hidup, atau dengan kata lain, jika koneksi tidak akan ditutup, kami menetapkan header koneksi respons sebagai keep-hidup . Jika tidak, kami menutup koneksi.

4. Menguji Server

Untuk menguji server kami, mari kirim beberapa perintah cURL dan lihat tanggapannya.

Tentu saja, kita perlu memulai server dengan menjalankan kelas HttpServer sebelum ini .

4.1. DAPATKAN Permintaan

Pertama-tama mari kita panggil server, memberikan cookie dengan permintaan:

curl //127.0.0.1:8080?param1=one

Sebagai tanggapan, kami mendapatkan:

Parameter: PARAM1 = ONE Good Bye! 

Kami juga dapat menekan //127.0.0.1:8080?param1=one dari browser manapun untuk melihat hasil yang sama.

4.2. Permintaan POST

Sebagai pengujian kedua, mari kirim POST dengan konten sampel tubuh :

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

Berikut tanggapannya:

SAMPLE CONTENT Good Bye!

Kali ini, karena permintaan kami berisi body, server mengirimkannya kembali dalam huruf besar .

5. Kesimpulan

Dalam tutorial ini, kami melihat bagaimana mengimplementasikan protokol HTTP, khususnya server HTTP menggunakan Netty.

HTTP / 2 di Netty mendemonstrasikan implementasi klien-server dari protokol HTTP / 2.

Seperti biasa, kode sumber tersedia di GitHub.