netty docs 4x

Netty.docs: User guide for 4.x

 

User guide for 4.x

Did you know this page is automatically generated from a Github Wiki page? You can improve it by yourself here!

Preface

The Problem

Nowadays we use general purpose applications or libraries to communicate with each other. For example, we often use an HTTP client library to retrieve information from a web server and to invoke a remote procedure call via web services. However, a general purpose protocol or its implementation sometimes does not scale very well. It is like how we don't use a general purpose HTTP server to exchange huge files, e-mail messages, and near-realtime messages such as financial information and multiplayer game data. What's required is a highly optimized protocol implementation that is dedicated to a special purpose. For example, you might want to implement an HTTP server that is optimized for AJAX-based chat application, media streaming, or large file transfer. You could even want to design and implement a whole new protocol that is precisely tailored to your need. Another inevitable case is when you have to deal with a legacy proprietary protocol to ensure the interoperability with an old system. What matters in this case is how quickly we can implement that protocol while not sacrificing the stability and performance of the resulting application.

The Solution

The Netty project is an effort to provide an asynchronous event-driven network application framework and tooling for the rapid development of maintainable high-performance and high-scalability protocol servers and clients.

In other words, Netty is an NIO client server framework that enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server development.

'Quick and easy' does not mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences learned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.

Some users might already have found other network application frameworks that claim to have the same advantage, and you might want to ask what makes Netty so different from them. The answer is the philosophy it is built on. Netty is designed to give you the most comfortable experience both in terms of the API and the implementation from day one. It is not something tangible but you will realize that this philosophy will make your life much easier as you read this guide and play with Netty.

Getting Started

This chapter tours around the core constructs of Netty with simple examples to let you get started quickly. You will be able to write a client and a server on top of Netty right away when you are at the end of this chapter.

If you prefer a top-down approach in learning something, you might want to start from Chapter 2, Architectural Overview and get back here.

Before Getting Started

The minimum requirements to run the examples in this chapter are only two; the latest version of Netty and JDK 1.6 or above. The latest version of Netty is available in the project download page. To download the right version of JDK, please refer to your preferred JDK vendor's web site.

As you read, you might have more questions about the classes introduced in this chapter. Please refer to the API reference whenever you want to know more about them. All class names in this document are linked to the online API reference for your convenience. Also, please don't hesitate to contact the Netty project community and let us know if there's any incorrect information, errors in grammar or typos, and if you have any good ideas to help improve the documentation.

Writing a Discard Server

The most simplistic protocol in the world is not 'Hello, World!' but DISCARD. It's a protocol that discards any received data without any response.

To implement the DISCARD protocol, the only thing you need to do is to ignore all received data. Let us start straight from the handler implementation, which handles I/O events generated by Netty.

package io.netty.example.discard;

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Handles a server-side channel.
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}
  1. DiscardServerHandler extends ChannelInboundHandlerAdapter, which is an implementation of ChannelInboundHandlerChannelInboundHandler provides various event handler methods that you can override. For now, it is just enough to extend ChannelInboundHandlerAdapter rather than to implement the handler interface by yourself.
  2. We override the channelRead() event handler method here. This method is called with the received message, whenever new data is received from a client. In this example, the type of the received message is ByteBuf.
  3. To implement the DISCARD protocol, the handler has to ignore the received message. ByteBuf is a reference-counted object which has to be released explicitly via the release() method. Please keep in mind that it is the handler's responsibility to release any reference-counted object passed to the handler. Usually, channelRead() handler method is implemented like the following:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // Do something with msg
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
  1. The exceptionCaught() event handler method is called with a Throwable when an exception was raised by Netty due to an I/O error or by a handler implementation due to the exception thrown while processing events. In most cases, the caught exception should be logged and its associated channel should be closed here, although the implementation of this method can be different depending on what you want to do to deal with an exceptional situation. For example, you might want to send a response message with an error code before closing the connection.

So far so good. We have implemented the first half of the DISCARD server. What's left now is to write the main() method which starts the server with the DiscardServerHandler.

package io.netty.example.discard;
    
import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
    
/**
 * Discards any incoming data.
 */
public class DiscardServer {
    
    private int port;
    
    public DiscardServer(int port) {
        this.port = port;
    }
    
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
    
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)
    
            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new DiscardServer(port).run();
    }
}
  1. NioEventLoopGroup is a multithreaded event loop that handles I/O operation. Netty provides various EventLoopGroup implementations for different kind of transports. We are implementing a server-side application in this example, and therefore two NioEventLoopGroup will be used. The first one, often called 'boss', accepts an incoming connection. The second one, often called 'worker', handles the traffic of the accepted connection once the boss accepts the connection and registers the accepted connection to the worker. How many Threads are used and how they are mapped to the created Channels depends on the EventLoopGroup implementation and may be even configurable via a constructor.
  2. ServerBootstrap is a helper class that sets up a server. You can set up the server using a Channel directly. However, please note that this is a tedious process, and you do not need to do that in most cases.
  3. Here, we specify to use the NioServerSocketChannel class which is used to instantiate a new Channel to accept incoming connections.
  4. The handler specified here will always be evaluated by a newly accepted Channel. The ChannelInitializer is a special handler that is purposed to help a user configure a new Channel. It is most likely that you want to configure the ChannelPipeline of the new Channel by adding some handlers such as DiscardServerHandler to implement your network application. As the application gets complicated, it is likely that you will add more handlers to the pipeline and extract this anonymous class into a top-level class eventually.
  5. You can also set the parameters which are specific to the Channel implementation. We are writing a TCP/IP server, so we are allowed to set the socket options such as tcpNoDelay and keepAlive. Please refer to the apidocs of ChannelOption and the specific ChannelConfig implementations to get an overview about the supported ChannelOptions.
  6. Did you notice option() and childOption()option() is for the NioServerSocketChannel that accepts incoming connections. childOption() is for the Channels accepted by the parent ServerChannel, which is NioSocketChannel in this case.
  7. We are ready to go now. What's left is to bind to the port and to start the server. Here, we bind to the port 8080 of all NICs (network interface cards) in the machine. You can now call the bind() method as many times as you want (with different bind addresses.)

Congratulations! You've just finished your first server on top of Netty.

Looking into the Received Data

Now that we have written our first server, we need to test if it really works. The easiest way to test it is to use the telnet command. For example, you could enter telnet localhost 8080 in the command line and type something.

However, can we say that the server is working fine? We cannot really know that because it is a discard server. You will not get any response at all. To prove it is really working, let us modify the server to print what it has received.

We already know that channelRead() method is invoked whenever data is received. Let us put some code into the channelRead() method of the DiscardServerHandler:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    try {
        while (in.isReadable()) { // (1)
            System.out.print((char) in.readByte());
            System.out.flush();
        }
    } finally {
        ReferenceCountUtil.release(msg); // (2)
    }
}
  1. This inefficient loop can actually be simplified to: System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
  2. Alternatively, you could do in.release() here.

If you run the telnet command again, you will see the server prints what it has received.

The full source code of the discard server is located in the io.netty.example.discard package of the distribution.

Writing an Echo Server

So far, we have been consuming data without responding at all. A server, however, is usually supposed to respond to a request. Let us learn how to write a response message to a client by implementing the ECHO protocol, where any received data is sent back.

The only difference from the discard server we have implemented in the previous sections is that it sends the received data back instead of printing the received data out to the console. Therefore, it is enough again to modify the channelRead() method:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
  1. ChannelHandlerContext object provides various operations that enable you to trigger various I/O events and operations. Here, we invoke write(Object) to write the received message in verbatim. Please note that we did not release the received message unlike we did in the DISCARD example. It is because Netty releases it for you when it is written out to the wire.
  2. ctx.write(Object) does not make the message written out to the wire. It is buffered internally and then flushed out to the wire by ctx.flush(). Alternatively, you could call ctx.writeAndFlush(msg) for brevity.

If you run the telnet command again, you will see the server sends back whatever you have sent to it.

The full source code of the echo server is located in the io.netty.example.echo package of the distribution.

Writing a Time Server

The protocol to implement in this section is the TIME protocol. It is different from the previous examples in that it sends a message, which contains a 32-bit integer, without receiving any requests and closes the connection once the message is sent. In this example, you will learn how to construct and send a message, and to close the connection on completion.

Because we are going to ignore any received data but to send a message as soon as a connection is established, we cannot use the channelRead() method this time. Instead, we should override the channelActive() method. The following is the implementation:

package io.netty.example.time;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) { // (1)
        final ByteBuf time = ctx.alloc().buffer(4); // (2)
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        
        final ChannelFuture f = ctx.writeAndFlush(time); // (3)
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        }); // (4)
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. As explained, the channelActive() method will be invoked when a connection is established and ready to generate traffic. Let's write a 32-bit integer that represents the current time in this method.

  2. To send a new message, we need to allocate a new buffer which will contain the message. We are going to write a 32-bit integer, and therefore we need a ByteBuf whose capacity is at least 4 bytes. Get the current ByteBufAllocator via ChannelHandlerContext.alloc() and allocate a new buffer.

  3. As usual, we write the constructed message.

    But wait, where's the flip? Didn't we used to call java.nio.ByteBuffer.flip() before sending a message in NIO? ByteBuf does not have such a method because it has two pointers; one for read operations and the other for write operations. The writer index increases when you write something to a ByteBuf while the reader index does not change. The reader index and the writer index represents where the message starts and ends respectively.

    In contrast, NIO buffer does not provide a clean way to figure out where the message content starts and ends without calling the flip method. You will be in trouble when you forget to flip the buffer because nothing or incorrect data will be sent. Such an error does not happen in Netty because we have different pointer for different operation types. You will find it makes your life much easier as you get used to it -- a life without flipping out!

    Another point to note is that the ChannelHandlerContext.write() (and writeAndFlush()) method returns a ChannelFuture. A ChannelFuture represents an I/O operation which has not yet occurred. It means, any requested operation might not have been performed yet because all operations are asynchronous in Netty. For example, the following code might close the connection even before a message is sent:

    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();

    Therefore, you need to call the close() method after the ChannelFuture is complete, which was returned by the write() method, and it notifies its listeners when the write operation has been done. Please note that, close() also might not close the connection immediately, and it returns a ChannelFuture.

  4. How do we get notified when a write request is finished then? This is as simple as adding a ChannelFutureListener to the returned ChannelFuture. Here, we created a new anonymous ChannelFutureListener which closes the Channel when the operation is done.

    Alternatively, you could simplify the code using a pre-defined listener:

    f.addListener(ChannelFutureListener.CLOSE);

To test if our time server works as expected, you can use the UNIX rdate command:

$ rdate -o <port> -p <host>

where <port> is the port number you specified in the main() method and <host> is usually localhost.

Writing a Time Client

Unlike DISCARD and ECHO servers, we need a client for the TIME protocol because a human cannot translate a 32-bit binary data into a date on a calendar. In this section, we discuss how to make sure the server works correctly and learn how to write a client with Netty.

The biggest and only difference between a server and a client in Netty is that different Bootstrap and Channel implementations are used. Please take a look at the following code:

package io.netty.example.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            Bootstrap b = new Bootstrap(); // (1)
            b.group(workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            
            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
  1. Bootstrap is similar to ServerBootstrap except that it's for non-server channels such as a client-side or connectionless channel.
  2. If you specify only one EventLoopGroup, it will be used both as a boss group and as a worker group. The boss worker is not used for the client side though.
  3. Instead of NioServerSocketChannelNioSocketChannel is being used to create a client-side Channel.
  4. Note that we do not use childOption() here unlike we did with ServerBootstrap because the client-side SocketChannel does not have a parent.
  5. We should call the connect() method instead of the bind() method.

As you can see, it is not really different from the server-side code. What about the ChannelHandler implementation? It should receive a 32-bit integer from the server, translate it into a human-readable format, print the translated time, and close the connection:

package io.netty.example.time;

import java.util.Date;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg; // (1)
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. In TCP/IP, Netty reads the data sent from a peer into a ByteBuf.

It looks very simple and does not look any different from the server side example. However, this handler sometimes will refuse to work raising an IndexOutOfBoundsException. We discuss why this happens in the next section.

Dealing with a Stream-based Transport

One Small Caveat of Socket Buffer

In a stream-based transport such as TCP/IP, received data is stored into a socket receive buffer. Unfortunately, the buffer of a stream-based transport is not a queue of packets but a queue of bytes. It means, even if you sent two messages as two independent packets, an operating system will not treat them as two messages but as just a bunch of bytes. Therefore, there is no guarantee that what you read is exactly what your remote peer wrote. For example, let us assume that the TCP/IP stack of an operating system has received three packets:

Three packets received as they were sent

Because of this general property of a stream-based protocol, there's a high chance of reading them in the following fragmented form in your application:

Three packets split and merged into four buffers

Therefore, a receiving part, regardless it is server-side or client-side, should defrag the received data into one or more meaningful frames that could be easily understood by the application logic. In the case of the example above, the received data should be framed like the following:

Four buffers defragged into three

The First Solution

Now let us get back to the TIME client example. We have the same problem here. A 32-bit integer is a very small amount of data, and it is not likely to be fragmented often. However, the problem is that it can be fragmented, and the possibility of fragmentation will increase as the traffic increases.

The simplistic solution is to create an internal cumulative buffer and wait until all 4 bytes are received into the internal buffer. The following is the modified TimeClientHandler implementation that fixes the problem:

package io.netty.example.time;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    private ByteBuf buf;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        buf = ctx.alloc().buffer(4); // (1)
    }
    
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release(); // (1)
        buf = null;
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m); // (2)
        m.release();
        
        if (buf.readableBytes() >= 4) { // (3)
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. ChannelHandler has two life cycle listener methods: handlerAdded() and handlerRemoved(). You can perform an arbitrary (de)initialization task as long as it does not block for a long time.
  2. First, all received data should be cumulated into buf.
  3. And then, the handler must check if buf has enough data, 4 bytes in this example, and proceed to the actual business logic. Otherwise, Netty will call the channelRead() method again when more data arrives, and eventually all 4 bytes will be cumulated.

The Second Solution

Although the first solution has resolved the problem with the TIME client, the modified handler does not look that clean. Imagine a more complicated protocol which is composed of multiple fields such as a variable length field. Your ChannelInboundHandler implementation will become unmaintainable very quickly.

As you may have noticed, you can add more than one ChannelHandler to a ChannelPipeline, and therefore, you can split one monolithic ChannelHandler into multiple modular ones to reduce the complexity of your application. For example, you could split TimeClientHandler into two handlers:

  • TimeDecoder which deals with the fragmentation issue, and
  • the initial simple version of TimeClientHandler.

Fortunately, Netty provides an extensible class which helps you write the first one out of the box:

package io.netty.example.time;

public class TimeDecoder extends ByteToMessageDecoder { // (1)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
        if (in.readableBytes() < 4) {
            return; // (3)
        }
        
        out.add(in.readBytes(4)); // (4)
    }
}
  1. ByteToMessageDecoder is an implementation of ChannelInboundHandler which makes it easy to deal with the fragmentation issue.
  2. ByteToMessageDecoder calls the decode() method with an internally maintained cumulative buffer whenever new data is received.
  3. decode() can decide to add nothing to out when there is not enough data in the cumulative buffer. ByteToMessageDecoder will call decode() again when there is more data received.
  4. If decode() adds an object to out, it means the decoder decoded a message successfully. ByteToMessageDecoder will discard the read part of the cumulative buffer. Please remember that you don't need to decode multiple messages. ByteToMessageDecoder will keep calling the decode() method until it adds nothing to out.

Now that we have another handler to insert into the ChannelPipeline, we should modify the ChannelInitializer implementation in the TimeClient:

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

If you are an adventurous person, you might want to try the ReplayingDecoder which simplifies the decoder even more. You will need to consult the API reference for more information though.

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

Additionally, Netty provides out-of-the-box decoders which enables you to implement most protocols very easily and helps you avoid from ending up with a monolithic unmaintainable handler implementation. Please refer to the following packages for more detailed examples:

Speaking in POJO instead of ByteBuf

All the examples we have reviewed so far used a ByteBuf as a primary data structure of a protocol message. In this section, we will improve the TIME protocol client and server example to use a POJO instead of a ByteBuf.

The advantage of using a POJO in your ChannelHandlers is obvious; your handler becomes more maintainable and reusable by separating the code which extracts information from ByteBuf out from the handler. In the TIME client and server examples, we read only one 32-bit integer and it is not a major issue to use ByteBuf directly. However, you will find it is necessary to make the separation as you implement a real-world protocol.

First, let us define a new type called UnixTime.

package io.netty.example.time;

import java.util.Date;

public class UnixTime {

    private final long value;
    
    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }
    
    public UnixTime(long value) {
        this.value = value;
    }
        
    public long value() {
        return value;
    }
        
    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

We can now revise the TimeDecoder to produce a UnixTime instead of a ByteBuf.

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    if (in.readableBytes() < 4) {
        return;
    }

    out.add(new UnixTime(in.readUnsignedInt()));
}

With the updated decoder, the TimeClientHandler does not use ByteBuf anymore:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    UnixTime m = (UnixTime) msg;
    System.out.println(m);
    ctx.close();
}

Much simpler and elegant, right? The same technique can be applied on the server side. Let us update the TimeServerHandler first this time:

@Override
public void channelActive(ChannelHandlerContext ctx) {
    ChannelFuture f = ctx.writeAndFlush(new UnixTime());
    f.addListener(ChannelFutureListener.CLOSE);
}

Now, the only missing piece is an encoder, which is an implementation of ChannelOutboundHandler that translates a UnixTime back into a ByteBuf. It's much simpler than writing a decoder because there's no need to deal with packet fragmentation and assembly when encoding a message.

package io.netty.example.time;

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        ctx.write(encoded, promise); // (1)
    }
}
  1. There are quite a few important things in this single line.

    First, we pass the original ChannelPromise as-is so that Netty marks it as success or failure when the encoded data is actually written out to the wire.

    Second, we did not call ctx.flush(). There is a separate handler method void flush(ChannelHandlerContext ctx) which is purposed to override the flush() operation.

To simplify even further, you can make use of MessageToByteEncoder:

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

The last task left is to insert a TimeEncoder into the ChannelPipeline on the server side before the TimeServerHandler, and it is left as a trivial exercise.

Shutting Down Your Application

Shutting down a Netty application is usually as simple as shutting down all EventLoopGroups you created via shutdownGracefully(). It returns a Future that notifies you when the EventLoopGroup has been terminated completely and all Channels that belong to the group have been closed.

Summary

In this chapter, we had a quick tour of Netty with a demonstration on how to write a fully working network application on top of Netty.

There is more detailed information about Netty in the upcoming chapters. We also encourage you to review the Netty examples in the io.netty.example package.

Please also note that the community is always waiting for your questions and ideas to help you and keep improving Netty and its documentation based on your feedback.

4.x 用户指南

你知道这个页面是从 Github Wiki 页面自动生成的吗? 你可以在这里自行改进它 

前言

问题

如今,我们使用通用应用程序或库进行相互通信。例如,我们经常使用 HTTP 客户端库从 Web 服务器检索信息,并通过 Web 服务调用远程过程调用 (RPC)。然而,通用协议或其实现有时扩展性不佳。这就像我们不会使用通用 HTTP 服务器来交换大文件、电子邮件以及近实时消息(例如财务信息和多人游戏数据)。​​我们需要的是高度优化的专用协议实现。例如,您可能希望实现一个针对基于 AJAX 的聊天应用程序、媒体流或大文件传输进行优化的 HTTP 服务器。您甚至可能希望设计和实现一个完全根据您的需求定制的全新协议。另一个不可避免的情况是,您必须处理遗留的专有协议以确保与旧系统的互操作性。在这种情况下,重要的是我们如何快速实现该协议,同时不牺牲最终应用程序的稳定性和性能。

解决方案

Netty 项目致力于提供一个异步事件驱动的网络应用程序框架和工具,用于快速开发可维护的高性能和高可扩展性的协议服务器和客户端。

换句话说,Netty 是一个 NIO 客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化和精简了网络编程,例如 TCP 和 UDP 套接字服务器开发。

“快速简便”并不意味着最终的应用程序会面临可维护性或性能问题。Netty 的设计充分汲取了众多协议(例如 FTP、SMTP、HTTP 以及各种二进制和基于文本的遗留协议)的实现经验。因此,Netty 成功地找到了一种兼顾开发便捷性、性能、稳定性和灵活性的方法,并且丝毫不妥协。

有些用户可能已经找到了其他声称拥有相同优势的网络应用框架,你可能会想问 Netty 究竟有何不同?答案在于它所秉持的理念。Netty 的设计初衷是从 API 和实现方面,为您带来最舒适的体验。虽然这并非具体的东西,但随着你阅读本指南并实际使用 Netty,你会发现这种理念会让你的生活更加轻松。

入门

本章将通过一些简单示例讲解 Netty 的核心结构,帮助你快速上手。读完本章后,你将能够立即在 Netty 上编写客户端和服务器。

如果您喜欢自上而下地学习某些内容,您可能需要从第 2 章“架构概述”开始,然后再回到这里。

开始之前

运行本章示例的最低要求只有两个:最新版本的 Netty 和 JDK 1.6 或更高版本。最新版本的 Netty 可在项目下载页面获取。要下载合适的 JDK 版本,请访问您首选的 JDK 供应商的网站。

阅读过程中,您可能会对本章介绍的类有更多疑问。如需了解更多信息,请参阅 API 参考。为方便起见,本文档中的所有类名均已链接到在线 API 参考。此外,如果您发现任何信息错误、语法或拼写错误,或者有任何好的想法可以帮助改进文档,请随时联系 Netty 项目社区并告知我们。

编写丢弃服务器

世界上最简单的协议不是“Hello, World!”而是DISCARD。它是一个丢弃所有接收到的数据而不做任何响应的协议。

要实现该DISCARD协议,您唯一需要做的就是忽略所有接收到的数据。让我们直接从处理程序的实现开始,该实现处理 Netty 生成的 I/O 事件。

package io.netty.example.discard;

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Handles a server-side channel.
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // Discard the received data silently.
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}
  1. DiscardServerHandlerextendsChannelInboundHandlerAdapter是 的一个实现ChannelInboundHandlerChannelInboundHandler它提供了各种可供您重写的事件处理方法。目前,只需扩展即可,ChannelInboundHandlerAdapter无需自行实现处理程序接口。
  2. 我们channelRead()在这里重写了事件处理程序方法。每当从客户端接收到新数据时,都会使用收到的消息调用此方法。在此示例中,收到的消息类型为ByteBuf
  3. 为了实现该DISCARD协议,处理程序必须忽略接收到的消息。 ByteBuf是一个引用计数对象,必须通过该release()方法显式释放。请记住,释放传递给处理程序的任何引用计数对象是处理程序的责任。通常,channelRead()处理程序方法的实现如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // Do something with msg
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
  1. 当 Netty 因 I/O 错误引发异常,或处理程序实现在处理事件时引发异常时,会使用 Throwable 调用事件处理程序方法exceptionCaught()。大多数情况下,捕获的异常应该被记录下来,并在此关闭其关联的通道,但此方法的实现可能会有所不同,具体取决于您想要如何处理异常情况。例如,您可能希望在关闭连接之前发送一条包含错误代码的响应消息。

到目前为止一切顺利。我们已经实现了DISCARD服务器的前半部分。现在剩下的就是编写main()使用 启动服务器的方法DiscardServerHandler

package io.netty.example.discard;
    
import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
    
/**
 * Discards any incoming data.
 */
public class DiscardServer {
    
    private int port;
    
    public DiscardServer(int port) {
        this.port = port;
    }
    
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
    
            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)
    
            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new DiscardServer(port).run();
    }
}
  1. NioEventLoopGroup是一个处理 I/O 操作的多线程事件循环。NettyEventLoopGroup为不同类型的传输提供了各种实现。在本例中,我们实现一个服务器端应用程序,因此NioEventLoopGroup将使用两个实现。第一个通常称为“boss”,用于接受传入连接。第二个通常称为“worker”,用于在“boss”接受连接并将接受的连接注册到 worker 后处理已接受连接的流量。使用多少个线程以及如何将它们映射到创建的Channel线程取决于实现EventLoopGroup,甚至可以通过构造函数进行配置。
  2. ServerBootstrap是一个用于设置服务器的辅助类。您可以直接使用 来设置服务器Channel。但请注意,这是一个繁琐的过程,大多数情况下您无需执行此操作。
  3. 在这里,我们指定使用NioServerSocketChannel用于实例化新类Channel来接受传入连接的类。
  4. 此处指定的处理程序将始终由新接受的 进行评估ChannelChannelInitializer是一个特殊的处理程序,旨在帮助用户配置新的Channel。您很可能希望通过添加一些处理程序(例如 )来配置ChannelPipeline新的,以实现您的网络应用程序。随着应用程序变得越来越复杂,您可能会向管道添加更多处理程序,并最终将此匿名类提取到顶级类中。ChannelDiscardServerHandler
  5. 您还可以设置特定于Channel实现的参数。我们正在编写一个 TCP/IP 服务器,因此我们可以设置套接字选项,例如tcpNoDelaykeepAlive。请参阅 apidocsChannelOption和具体ChannelConfig实现,以了解支持的 s 的概述ChannelOption
  6. 您是否注意到了option()childOption()? option()代表NioServerSocketChannel接受传入连接的 。childOption()代表Channel由父级 接受的ServerChannel,在本例中是NioSocketChannel
  7. 现在一切准备就绪。剩下的就是绑定端口并启动服务器。这里,我们将绑定到8080机器上所有 NIC(网卡)的端口。现在您可以bind()根据需要多次调用该方法(使用不同的绑定地址)。

恭喜!您刚刚完成了基于 Netty 的第一个服务器。

查看接收到的数据

现在我们已经编写好了第一个服务器,我们需要测试它是否真的能正常工作。最简单的测试方法是使用telnet命令。例如,你可以telnet localhost 8080在命令行中输入 telnet 并输入一些内容。

但是,我们能说服务器工作正常吗?我们无法确定,因为它是一个丢弃服务器。你不会收到任何响应。为了证明它确实正常工作,让我们修改服务器,让它打印接收到的内容。

我们已经知道,channelRead()只要收到数据,就会调用该方法。让我们在该channelRead()方法中添加一些代码DiscardServerHandler

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    try {
        while (in.isReadable()) { // (1)
            System.out.print((char) in.readByte());
            System.out.flush();
        }
    } finally {
        ReferenceCountUtil.release(msg); // (2)
    }
}
  1. 这个低效的循环实际上可以简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
  2. 或者,您也可以in.release()在这里进行。

如果再次运行telnet命令,您将看到服务器打印它收到的内容。

丢弃服务器的完整源代码位于io.netty.example.discard分发包中。

编写 Echo 服务器

到目前为止,我们一直在消费数据,但没有任何响应。然而,服务器通常应该响应请求。让我们学习如何通过实现ECHO协议来向客户端发送响应消息,该协议会将接收到的任何数据发送回客户端。

与前几节中实现的丢弃服务器唯一的区别是,它将接收到的数据发送回去,而不是将接收到的数据打印到控制台。因此,只需再次修改该channelRead()方法即可:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
  1. 对象ChannelHandlerContext提供各种操作,使您能够触发各种 I/O 事件和操作。在这里,我们调用write(Object)以逐字写入接收到的消息。请注意,与示例中不同,我们没有释放接收到的消息DISCARD。这是因为 Netty 在将消息写入网络时会为您释放它。
  2. ctx.write(Object)不会将消息写入网络。它会在内部进行缓冲,然后通过 刷新到网络。或者,为了简洁起见ctx.flush(),您也可以调用。ctx.writeAndFlush(msg)

如果您再次运行telnet命令,您将看到服务器发回您发送给它的任何内容。

回显服务器的完整源代码位于io.netty.example.echo发行版的包中。

编写时间服务器

本节要实现的协议是TIME协议。它与前面的示例不同,它发送一条包含 32 位整数的消息,而不接收任何请求,并在消息发送后关闭连接。在本例中,您将学习如何构造和发送消息,以及如何在完成后关闭连接。

因为我们要忽略任何接收到的数据,而是在连接建立后立即发送消息,所以channelRead()这次我们不能使用该方法。相反,我们应该重写该channelActive()方法。以下是实现:

package io.netty.example.time;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) { // (1)
        final ByteBuf time = ctx.alloc().buffer(4); // (2)
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        
        final ChannelFuture f = ctx.writeAndFlush(time); // (3)
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        }); // (4)
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 如上所述,channelActive()当连接建立并准备好生成流量时,该方法将被调用。让我们在此方法中写入一个表示当前时间的 32 位整数。

  2. 要发送新消息,我们需要分配一个新的缓冲区来保存该消息。我们将写入一个 32 位整数,因此需要一个ByteBuf容量至少为 4 字节的缓冲区。获取当前ByteBufAllocatorviaChannelHandlerContext.alloc()并分配一个新的缓冲区。

  3. 像往常一样,我们编写构造的消息。

    java.nio.ByteBuffer.flip()但是等等,翻转在哪里?我们以前在 NIO 中发送消息之前不是要调用吗?ByteBuf没有这样的方法,因为它有两个指针:一个用于读取操作,另一个用于写入操作。写入操作时,写入器索引会增加,而ByteBuf读取器索引则保持不变。读取器索引和写入器索引分别表示消息的起始和结束位置。

    相比之下,NIO 缓冲区不提供清晰的方法来识别消息内容的起始和结束位置,除非调用 flip 方法。如果您忘记翻转缓冲区,您将会遇到麻烦,因为将不会发送任何数据或发送错误的数据。Netty 不会发生这样的错误,因为我们为不同的操作类型提供了不同的指针。随着您逐渐习惯它,您会发现它可以让您的生活变得轻松很多——不再需要翻转!

    另一点需要注意的是,ChannelHandlerContext.write()(and writeAndFlush()) 方法返回一个ChannelFuture。 AChannelFuture表示尚未发生的 I/O 操作。这意味着,任何请求的操作可能尚未执行,因为 Netty 中的所有操作都是异步的。例如,以下代码甚至可能在消息发送之前就关闭连接:

    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();

    因此,您需要在方法返回完成close()后调用该方法,并在写入操作完成后通知其监听器。请注意,也可能不会立即关闭连接,并且它会返回一个。ChannelFuturewrite()close()ChannelFuture

  4. 那么,当写入请求完成时,我们如何收到通知呢?这很简单,只需ChannelFutureListener在返回的 中添加一个 即可ChannelFuture。在这里,我们创建了一个新的匿名函数,它会在操作完成后ChannelFutureListener关闭。Channel

    或者,您可以使用预定义的监听器简化代码:

    f.addListener(ChannelFutureListener.CLOSE);

要测试我们的时间服务器是否按预期工作,您可以使用 UNIXrdate命令:

$ rdate -o <port> -p <host>

其中<port>是您在方法中指定的端口号main()<host>通常是localhost

编写时间客户端

DISCARD与服务器不同ECHO,我们需要一个协议客户端,TIME因为人类无法将 32 位二进制数据转换为日历上的日期。在本节中,我们将讨论如何确保服务器正常工作,并学习如何使用 Netty 编写客户端。

Netty 中服务器和客户端之间最大且唯一的区别在于使用的实现方式不同BootstrapChannel请看以下代码:

package io.netty.example.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        try {
            Bootstrap b = new Bootstrap(); // (1)
            b.group(workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            
            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
  1. Bootstrap与之类似,ServerBootstrap只不过它适用于非服务器通道,例如客户端或无连接通道。
  2. 如果只指定一个EventLoopGroup,它将同时用作 boss 组和 worker 组。不过,boss worker 组不用于客户端。
  3. 而不是NioServerSocketChannelNioSocketChannel正在用于创建客户端Channel
  4. 请注意,我们在这里不使用,这childOption()与我们所做的不同,ServerBootstrap因为客户端SocketChannel没有父级。
  5. 我们应该调用connect()方法而不是bind()方法。

如你所见,它与服务器端代码并​​没有什么不同。那么ChannelHandler实现呢?它应该从服务器接收一个 32 位整数,将其转换为人类可读的格式,打印转换后的时间,然后关闭连接:

package io.netty.example.time;

import java.util.Date;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg; // (1)
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 在 TCP/IP 中,Netty 将从对等方发送的数据读取到ByteBuf.

它看起来非常简单,与服务器端示例没有任何区别。然而,这个处理程序有时会拒绝工作并引发IndexOutOfBoundsException。我们将在下一节讨论为什么会发生这种情况。

处理基于流的传输

关于套接字缓冲区的一个小警告

在基于流的传输(例如 TCP/IP)中,接收到的数据会存储在套接字接收缓冲区中。然而,基于流的传输的缓冲区并非数据包队列,而​​是字节队列。这意味着,即使您将两条消息作为两个独立的数据包发送,操作系统也不会将它们视为两条消息,而只是将其视为一堆字节。因此,无法保证您读取的内容与远程对等体写入的内容完全一致。例如,假设操作系统的 TCP/IP 协议栈已收到三个数据包:

发送时收到三个数据包

由于基于流的协议的这种一般属性,很有可能在您的应用程序中以以下碎片形式读取它们:

三个数据包被拆分并合并到四个缓冲区

因此,无论是服务器端还是客户端,接收方都应该将接收到的数据整理成一个或多个有意义的帧,以便应用程序逻辑能够轻松理解。在上述示例中,接收到的数据应该采用如下格式:

四个缓冲区被整理成三个

第一个解决方案

现在让我们回到TIME客户端示例。这里我们遇到了同样的问题。32 位整数的数据量非常小,不太可能经常出现碎片。然而,问题在于它很容易出现碎片,而且随着流量的增加,碎片的可能性也会随之增加。

最简单的解决方案是创建一个内部累积缓冲区,并等待所有 4 个字节都接收到内部缓冲区中。以下是TimeClientHandler修复该问题的修改后的实现:

package io.netty.example.time;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    private ByteBuf buf;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        buf = ctx.alloc().buffer(4); // (1)
    }
    
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release(); // (1)
        buf = null;
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m); // (2)
        m.release();
        
        if (buf.readableBytes() >= 4) { // (3)
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. AChannelHandler有两个生命周期监听器方法:handlerAdded()handlerRemoved()。您可以执行任意初始化(反初始化)任务,只要它不会长时间阻塞即可。
  2. 首先,所有接收到的数据都应累积到 中buf
  3. 然后,处理程序必须检查是否buf有足够的数据(本例中为 4 个字节),然后继续执行实际的业务逻辑。否则,Netty 会channelRead()在更多数据到达时再次调用该方法,最终累计所有 4 个字节。

第二种解决方案

虽然第一个方案解决了客户端的问题TIME,但修改后的处理程序看起来并不那么简洁。想象一下一个更复杂的协议,它由多个字段组成,例如可变长度字段。你的ChannelInboundHandler实现很快就会变得难以维护。

ChannelHandler你可能已经注意到,你可以向 中添加多个ChannelPipeline,因此,你可以将一个整体拆分ChannelHandler成多个模块化组件,以降低应用程序的复杂性。例如,你可以拆分TimeClientHandler成两个处理程序:

  • TimeDecoder处理碎片化问题,
  • 的初始简单版本TimeClientHandler

幸运的是,Netty 提供了一个可扩展的类,可以帮助您开箱即用地编写第一个类:

package io.netty.example.time;

public class TimeDecoder extends ByteToMessageDecoder { // (1)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
        if (in.readableBytes() < 4) {
            return; // (3)
        }
        
        out.add(in.readBytes(4)); // (4)
    }
}
  1. ByteToMessageDecoderChannelInboundHandler是一种可以轻松处理碎片问题的实现。
  2. ByteToMessageDecoderdecode()每当收到新数据时,都会使用内部维护的累积缓冲区调用该方法。
  3. decode()out当累积缓冲区中没有足够的数据时, 可以决定不添加任何内容。当收到更多数据时ByteToMessageDecoder将再次调用。decode()
  4. 如果decode()将一个对象添加到out,则表示解码器已成功解码一条消息。 ByteToMessageDecoder将丢弃累积缓冲区的已读取部分。请记住,您不需要解码多条消息。ByteToMessageDecoder将继续调用该decode()方法,直到它不再向 中添加任何内容out

现在我们有另一个处理程序要插入到中ChannelPipeline,我们应该修改ChannelInitializer中的实现TimeClient

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

如果您喜欢冒险,不妨尝试一下,ReplayingDecoder它可以进一步简化解码器。不过,您需要查阅 API 参考以获取更多信息。

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

此外,Netty 提供了开箱即用的解码器,让您能够非常轻松地实现大多数协议,并帮助您避免最终得到一个难以维护的单片处理程序实现。请参阅以下软件包以获取更详细的示例:

用 POJO 代替ByteBuf

到目前为止,我们回顾的所有示例都使用ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进TIME协议客户端和服务器示例,使用 POJO 代替ByteBuf

在 s中使用 POJO 的优势ChannelHandler显而易见;通过将提取信息的代码与ByteBuf处理程序分离,您的处理程序将变得更易于维护和复用。在TIME客户端和服务器示例中,我们只读取一个 32 位整数,直接使用它并不是什么大问题ByteBuf。但是,在实现实际协议时,您会发现进行分离是必要的。

首先,让我们定义一个名为的新类型UnixTime

package io.netty.example.time;

import java.util.Date;

public class UnixTime {

    private final long value;
    
    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }
    
    public UnixTime(long value) {
        this.value = value;
    }
        
    public long value() {
        return value;
    }
        
    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

现在我们可以修改TimeDecoder来生成UnixTime而不是ByteBuf

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    if (in.readableBytes() < 4) {
        return;
    }

    out.add(new UnixTime(in.readUnsignedInt()));
}

使用更新的解码器,TimeClientHandler不再使用ByteBuf

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    UnixTime m = (UnixTime) msg;
    System.out.println(m);
    ctx.close();
}

是不是简洁优雅多了?同样的技术也可以应用在服务器端。TimeServerHandler这次我们来更新第一个代码:

@Override
public void channelActive(ChannelHandlerContext ctx) {
    ChannelFuture f = ctx.writeAndFlush(new UnixTime());
    f.addListener(ChannelFutureListener.CLOSE);
}

现在,唯一缺少的部分就是一个编码器,它是ChannelOutboundHandler将 转换UnixTime回的实现ByteBuf。这比编写解码器简单得多,因为在编码消息时无需处理数据包的碎片和组装。

package io.netty.example.time;

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        ctx.write(encoded, promise); // (1)
    }
}
  1. 这句话里包含了很多重要的事情。

    首先,我们按原样传递原始数据ChannelPromise,以便当编码数据实际写入线路时,Netty 将其标记为成功或失败。

    其次,我们没有调用ctx.flush()。有一个单独的处理程序方法void flush(ChannelHandlerContext ctx)旨在覆盖该flush()操作。

为了进一步简化,您可以使用MessageToByteEncoder

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

剩下的最后一个任务是在服务器端的 之前TimeEncoder插入,这只是一个简单的练习。ChannelPipelineTimeServerHandler

关闭你的应用程序

关闭 Netty 应用程序通常很简单,只需关闭EventLoopGroup通过 创建的所有 即可shutdownGracefully()。它会返回一个Future,通知您EventLoopGroup已完全终止,并且Channel属于该组的所有 都已关闭。

概括

在本章中,我们快速了解了 Netty,并演示了如何在 Netty 上编写一个功能齐全的网络应用程序。

在接下来的章节中,我们将详细介绍 Netty。我们也鼓励您查看io.netty.example软件包中的 Netty 示例。

另请注意,社区始终等待您的问题和想法来帮助您,并根据您的反馈不断改进 Netty 及其文档。

 

Netty 项目 3.x 用户指南

Netty 项目 3.x 用户指南

经过验证的快速网络应用程序开发方法


前言

1. 问题

如今,我们使用通用应用程序或库来相互通信。例如,我们经常使用 HTTP 客户端库从 Web 服务器检索信息,并通过 Web 服务调用远程过程调用 (RPC)。

然而,通用协议或其实现有时扩展性不佳。这就像我们不会使用通用 HTTP 服务器来交换大文件、电子邮件以及近实时消息(例如财务信息和多人游戏数据)。​​我们需要的是高度优化的协议实现,专用于特定用途。例如,您可能希望实现一个针对基于 AJAX 的聊天应用程序、媒体流或大文件传输进行优化的 HTTP 服务器。您甚至可能希望设计和实现一个全新的协议,以完全满足您的需求。

另一个不可避免的情况是,为了确保与旧系统的互操作性,我们必须处理遗留的专有协议。在这种情况下,关键在于我们能够如何快速地实现该协议,同时又不牺牲最终应用程序的稳定性和性能。

2.解决方案

Netty 项目致力于提供一个异步事件驱动的网络应用程序框架和工具,用于快速开发可维护的高性能·高可扩展性协议服务器和客户端。

换句话说,Netty 是一个 NIO 客户端服务器框架,它能够快速轻松地开发网络应用程序(例如协议服务器和客户端)。它极大地简化了网络编程(例如 TCP 和 UDP 套接字服务器开发)。

“快速简便”并不意味着最终的应用程序会面临可维护性或性能问题。Netty 的设计充分汲取了众多协议(例如 FTP、SMTP、HTTP 以及各种二进制和基于文本的遗留协议)的实现经验。因此,Netty 成功地找到了一种兼顾开发便捷性、性能、稳定性和灵活性的方法,并且丝毫不妥协。

有些用户可能已经找到了其他声称拥有相同优势的网络应用框架,你可能会想问 Netty 究竟有何不同?答案在于它所秉持的理念。Netty 的设计初衷是从 API 和实现方面,为您带来最舒适的体验。虽然这并非具体的东西,但随着你阅读本指南并实际使用 Netty,你会发现这种理念会让你的工作更加轻松。

第 1 章 入门

本章将通过一些简单示例讲解 Netty 的核心结构,帮助你快速上手。读完本章后,你将能够立即在 Netty 上编写客户端和服务器。

如果您喜欢自上而下地学习某些东西,您可能需要从第 2 章“架构概述”开始,然后再回到这里。

1. 开始之前

运行本章介绍的示例的最低要求只有两个:最新版本的 Netty 和 JDK 1.5 或更高版本。最新版本的 Netty 可在 项目下载页面获取。要下载合适的 JDK 版本,请访问您首选的 JDK 供应商的网站。

阅读过程中,您可能会对本章介绍的类有更多疑问。如需了解更多信息,请参阅 API 参考。为方便起见,本文档中的所有类名均已链接到在线 API 参考。此外,如果您发现任何信息错误、语法错误、拼写错误,或者您有改进文档的好主意,请随时 联系 Netty 项目社区并告知我们。

2. 编写丢弃服务器

世界上最简单的协议不是“Hello, World!”而是 DISCARD。它是一个丢弃所有接收到的数据而不进行任何响应的协议。

要实现 DISCARD 协议,你唯一需要做的就是忽略所有接收到的数据。让我们直接从处理程序的实现开始,它处理 Netty 生成的 I/O 事件。

  1 包 org.jboss.netty.example.discard;
   2公共类 DiscardServerHandler 扩展了{ 
    SimpleChannelHandler  4 @Override  6 public void messageReceived( ctx, e) { ChannelHandlerContextMessageEvent  }  8 @Override 10 public void exceptionCaught( ctx,e) { ChannelHandlerContextExceptionEvent  e.getCause().printStackTrace(); 12 ch = e.getChannel(); 14 ch.close();  } 16 } Channel

1  

DiscardServerHandlerextends SimpleChannelHandler是 的一个实现 ChannelHandler。 SimpleChannelHandler它提供了各种可供您重写的事件处理方法。目前,只需扩展即可,SimpleChannelHandler无需自行实现处理程序接口。

2  

我们messageReceived在这里重写了事件处理程序方法。每当从客户端接收到新数据时,都会使用包含接收到的数据的 来调用此方法MessageEvent。在此示例中,我们忽略接收到的数据,不执行任何操作以实现 DISCARD 协议。

3  

exceptionCaught当 Netty 因 I/O 错误引发异常,或处理程序实现在处理事件时抛出异常时,会使用 调用事件处理程序方法ExceptionEvent。大多数情况下,捕获的异常应该被记录下来,并且其关联的通道应该在这里关闭,尽管此方法的实现可能因您想要处理异常情况的方式而异。例如,您可能希望在关闭连接之前发送一条包含错误代码的响应消息。

到目前为止一切顺利。我们已经实现了 DISCARD 服务器的前半部分。现在剩下的就是编写main使用 启动服务器的方法DiscardServerHandler

  1 包 org.jboss.netty.example.discard;
   2导入 java.net.InetSocketAddress;
   4导入 java.util.concurrent.Executors;
   6公共类 DiscardServer {
   8    公共静态 void main(String[] args) 抛出异常 {
 factory =
 10             new 
         
     
             ChannelFactory NioServerSocketChannelFactory(  Executors.newCachedThreadPool(), 12 Executors.newCachedThreadPool()); 14 bootstrap = new ServerBootstrapServerBootstrap(工厂); 16 bootstrap.setPipelineFactory(new (){ ChannelPipelineFactory  公共ChannelPipelinegetPipeline() { 18  返回Channels.pipeline(new DiscardServerHandler());  } 20 }); 22 bootstrap.setOption("child.tcpNoDelay", true);   bootstrap.setOption("child.keepAlive", true); 24 bootstrap.bind(new InetSocketAddress(8080));  26  } }

4  

ChannelFactory是一个创建和管理Channels 及其相关资源的工厂。它处理所有 I/O 请求并执行 I/O 操作以生成ChannelEvents。Netty 提供了多种 ChannelFactory实现。在本例中,我们实现了一个服务器端应用程序,因此 NioServerSocketChannelFactory使用了 s。另外需要注意的是,它本身不会创建 I/O 线程。它应该从您在构造函数中指定的线程池中获取线程,并且它可以让您更好地控制在应用程序运行的环境中(例如带有安全管理器的应用服务器)如​​何管理线程。

5  

ServerBootstrap是一个用于设置服务器的辅助类。您可以直接使用 来设置服务器Channel。但请注意,这是一个繁琐的过程,大多数情况下您无需执行此操作。

6  

在这里,我们配置了ChannelPipelineFactory。每当服务器接受新连接时,ChannelPipeline指定的 都会创建一个新的ChannelPipelineFactory。新的管道包含DiscardServerHandler。随着应用程序变得越来越复杂,您可能会向管道添加更多处理程序,并最终将这个匿名类提取到顶级类中。

7  

您还可以设置特定于Channel 实现的参数。我们正在编写一个 TCP/IP 服务器,因此我们可以设置套接字选项,例如tcpNoDelay和 keepAlive。请注意, "child."所有选项都添加了前缀 。这意味着这些选项将应用于已接受的Channel,而不是 的选项ServerSocketChannel。您可以执行以下操作来设置 的选项ServerSocketChannel

bootstrap.setOption(“reuseAddress”,true);

 

8  

现在一切准备就绪。剩下的就是绑定端口并启动服务器。这里,我们将绑定到8080 机器上所有 NIC(网卡)的端口。现在您可以bind根据需要多次调用该方法(使用不同的绑定地址)。

恭喜!您刚刚完成了基于 Netty 的第一个服务器。

3. 查看接收到的数据

现在我们已经编写好了第一个服务器,我们需要测试它是否真的能正常工作。最简单的测试方法是使用telnet 命令。例如,你可以在命令行中 输入“ telnet localhost 8080 ”,然后输入一些内容。

但是,我们能说服务器工作正常吗?我们无法确定,因为它是一个丢弃服务器。你不会收到任何响应。为了证明它确实正常工作,让我们修改服务器,让它打印接收到的内容。

我们已经知道,MessageEvent每当接收到数据时都会生成,并且messageReceived处理程序方法将被调用。让我们将一些代码放入 messageReceived的方法 中DiscardServerHandler

  1  @Override
   2  public void messageReceived( ChannelHandlerContextctx, MessageEvente) {  ChannelBufferbuf = (ChannelBuffer) e.getMessage();  4  while(buf.readable()) {  System.out.println((char) buf.readByte());  6 System.out.flush();  }  8 }

9  

可以安全地假设套接字传输中的消息类型始终是 ChannelBuffer。 ChannelBuffer是 Netty 中存储字节序列的基本数据结构。它与 NIO 类似 ByteBuffer,但更易于使用且更灵活。例如,Netty 允许您创建一个 ChannelBuffer组合多个 s 的复合结构ChannelBuffer,从而减少不必要的内存复制次数。

虽然它与 NIOByteBuffer非常相似,但强烈建议您参考 API 参考。学习如何ChannelBuffer正确使用是顺利使用 Netty 的关键一步。

如果您再次运行telnet命令,您将看到服务器打印已收到的内容。

丢弃服务器的完整源代码位于 org.jboss.netty.example.discard分发包中。

4. 编写 Echo 服务器

到目前为止,我们一直在消费数据,但没有任何响应。然而,服务器通常应该响应请求。让我们学习如何通过实现 ECHO协议向客户端发送响应消息,该协议会将接收到的任何数据发送回去。

与前几节中实现的丢弃服务器唯一的区别是,它将接收到的数据发送回去,而不是将接收到的数据打印到控制台。因此,只需再次修改该messageReceived方法即可:

  1  @Override
   2  public void messageReceived( ChannelHandlerContextctx, MessageEvente) {  Channelch = e.getChannel();  4  ch.写入(e.getMessage()); }

10  

对象ChannelEvent具有对其关联 的引用Channel。这里,返回的Channel表示接收 的连接MessageEvent。我们可以获取Channel并调用 write方法将一些内容写回远程对等体。

如果您再次运行telnet命令,您将看到服务器发回您发送给它的任何内容。

回显服务器的完整源代码位于 org.jboss.netty.example.echo发行版的包中。

5.编写时间服务器

本节要实现的协议是 TIME协议。它与前面的示例不同,它发送一条包含 32 位整数的消息,而不接收任何请求,并且一旦消息发送完成就会断开连接。在本例中,您将学习如何构造和发送消息,以及如何在完成后关闭连接。

因为我们要忽略任何接收到的数据,而是在连接建立后立即发送消息,所以 messageReceived这次我们不能使用该方法。相反,我们应该重写该channelConnected方法。以下是实现:

  1 包 org.jboss.netty.example.time;
   2公共类 TimeServerHandler 扩展了{
   4     @Override
   6    公共 void channelConnected( ctx, e) { 
    SimpleChannelHandler ChannelHandlerContextChannelStateEvent Channelch = e.getChannel();  8时间 = .buffer(4); ChannelBufferChannelBuffers 10  time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L)); 12 f = ch.write(time); ChannelFuture 14  f.addListener(new ChannelFutureListener(){  公共 void operationComplete( ChannelFuturefuture) { 16 ch = future.getChannel();  ch.close(); 18 }  }); 20 } 22 @Override  公共 void exceptionCaught( ctx,e) { 24 e.getCause().printStackTrace();  e.getChannel().close(); 26 } } Channel ChannelHandlerContextExceptionEvent

11  

如上所述,channelConnected当连接建立时,该方法将被调用。我们在这里写入一个 32 位整数,以秒为单位表示当前时间。

12  

要发送新消息,我们需要分配一个新的缓冲区来保存消息。我们将写入一个 32 位整数,因此需要一个ChannelBuffer容量为 4字节的缓冲区。ChannelBuffers辅助类用于分配新的缓冲区。除了 buffer方法之外,ChannelBuffers还提供了许多与 相关的实用方法ChannelBuffer。有关更多信息,请参阅 API 参考。

另一方面,使用静态导入是一个好主意 ChannelBuffers

  1 导入静态 org.jboss.netty.buffer。. ChannelBuffers*;  2  ...  dynamicBuf = dynamicBuffer(256);  4 plainBuf = buffer(1024); ChannelBuffer ChannelBuffer

 

十三  

像往常一样,我们编写构造的消息。

但是等等,那个方法在哪儿?我们以前在 NIO 中发送消息之前 flip不是要调用吗?它没有这个方法,因为它有两个指针:一个用于读取操作,另一个用于写入操作。写入操作时,写入器索引会增加,而读取器索引则保持不变。读取器索引和写入器索引分别表示消息的起始和结束位置。 ByteBuffer.flip()ChannelBufferChannelBuffer

相比之下,NIO 缓冲区不提供一种清晰的方法来在不调用该 flip方法的情况下确定消息内容的起始和结束位置。如果您忘记翻转缓冲区,您将会遇到麻烦,因为将不会发送任何数据或不正确的数据。Netty 不会发生这样的错误,因为我们为不同的操作类型使用了不同的指针。随着您逐渐习惯它,您会发现它可以让您的生活变得轻松很多——不再需要翻转!

另一点需要注意的是,该write 方法返回一个ChannelFuture。 AChannelFuture表示尚未发生的 I/O 操作。这意味着,任何请求的操作可能尚未执行,因为 Netty 中的所有操作都是异步的。例如,以下代码甚至可能在消息发送之前就关闭连接:

  1 ch = ...;
   2 ch.写入(消息);
 ch.关闭(); Channel

因此,您需要在该方法返回的 close 之后调用方法来通知您写入操作已完成。请注意, 也可能不会立即关闭连接,它会返回一个 。 ChannelFuturewritecloseChannelFuture

14  

那么,当写入请求完成时,我们如何收到通知呢?这很简单,只需ChannelFutureListener在返回的 中 添加一个 即可ChannelFuture。在这里,我们创建了一个新的匿名函数,它会在操作完成后 ChannelFutureListener 关闭。Channel

或者,您可以使用预定义的监听器简化代码:

f.添加监听器(ChannelFutureListener.关闭);

 

要测试我们的时间服务器是否按预期工作,您可以使用 UNIX rdate命令:

$ rdate -o <端口> -p <主机>
其中 port 是您在方法中指定的端口号main(),而 host 通常是localhost

 

6. 编写时间客户端

与 DISCARD 和 ECHO 服务器不同,我们需要一个支持 TIME 协议的客户端,因为人类无法将 32 位二进制数据转换为日历上的日期。在本节中,我们将讨论如何确保服务器正常工作,并学习如何使用 Netty 编写客户端。

Netty 中服务器和客户端之间最大且唯一的区别就是,两者是不同的,Bootstrap并且ChannelFactory是必需的。请看下面的代码:

  1 包 org.jboss.netty.example.time;
   2导入 java.net.InetSocketAddress;
   4导入 java.util.concurrent.Executors;
   6公共类 TimeClient {
   8    公共静态 void main(String[] args) 抛出异常 {
         String host = args[0];
 10         int port = Integer.parseInt(args[1]);
 12 factory =
             new 
         
     
          
         ChannelFactory NioClientSocketChannelFactory( 14  Executors.newCachedThreadPool(),  Executors.newCachedThreadPool()); 16 bootstrap = new ClientBootstrapClientBootstrap(16)(工厂); 18 bootstrap.setPipelineFactory(new (){ 20 public getPipeline(){  return .pipeline(new TimeClientHandler()); 22 }  }); 24 bootstrap.setOption(“tcpNoDelay” ChannelPipelineFactory ChannelPipeline Channels (17),true); 26  bootstrap.setOption(“keepAlive”,true); 28 bootstrap.connect (18)(新 InetSocketAddress(主机,端口));  } 30 }

15  

NioClientSocketChannelFactory,而不是NioServerSocketChannelFactory 用于创建客户端Channel

(16)  

ClientBootstrap是 的客户端对应部分ServerBootstrap

(17)  

请注意,没有"child."前缀。客户端SocketChannel没有父级。

(18)  

我们应该调用connect方法而不是bind方法。

可以看到,它与服务器端启动并没有什么区别。具体ChannelHandler实现是怎样的呢?它应该从服务器接收一个 32 位整数,将其转换为人类可读的格式,打印转换后的时间,然后关闭连接:

  1 包 org.jboss.netty.example.time;
   2导入 java.util.Date;
   4公共类 TimeClientHandler 扩展了{
   6     @Override
   8     public void messageReceived( ctx, e) {
 buf = ( ) e.getMessage();
 10         long currentTimeMillis = buf.readInt() * 1000L;
         System.out.println(new Date(currentTimeMillis));
 12         e.getChannel().close();
     }
 14     @Override
 16     public void exceptionCaught( ctx, e) {
         e.getCause().printStackTrace();
 18         e.getChannel().close();
     }
 20 } 
     
    SimpleChannelHandler ChannelHandlerContextMessageEvent ChannelBufferChannelBuffer ChannelHandlerContextExceptionEvent

它看起来非常简单,与服务器端示例没有任何区别。然而,这个处理程序有时会拒绝工作并引发 IndexOutOfBoundsException。我们将在下一节讨论为什么会发生这种情况。

7. 处理基于流的传输

7.1. 关于套接字缓冲区的一个小警告

在基于流的传输(例如 TCP/IP)中,接收到的数据会存储在套接字接收缓冲区中。然而,基于流的传输的缓冲区并非数据包队列,而​​是字节队列。这意味着,即使您将两条消息作为两个独立的数据包发送,操作系统也不会将它们视为两条消息,而只是将其视为一堆字节。因此,无法保证您读取的内容与远程对等体写入的内容完全一致。例如,假设操作系统的 TCP/IP 协议栈已收到三个数据包:

  1  +-----+-----+-----+
   2  | ABC |防御| GHI |
 +-----+-----+-----+    

由于基于流的协议的这种一般属性,很有可能在您的应用程序中以以下碎片形式读取它们:

  1  +----+-------+---+---+
   2  | AB | CDEFG | H | 我|
 +----+-------+---+---+    

因此,无论是服务器端还是客户端,接收方都应该将接收到的数据整理成一个或多个有意义的 ,以便应用程序逻辑能够轻松理解。在上述示例中,接收到的数据应该采用如下格式:

  1  +-----+-----+-----+
   2  | ABC |防御| GHI |
 +-----+-----+-----+    

7.2. 第一个解决方案

现在让我们回到 TIME 客户端的例子。这里我们遇到了同样的问题。32 位整数的数据量非常小,不太可能经常出现碎片。然而,问题在于它很 容易出现碎片,而且随着流量的增加,碎片的可能性也会随之增加。

最简单的解决方案是创建一个内部累积缓冲区,并等待所有 4 个字节都接收到内部缓冲区中。以下是TimeClientHandler 修复该问题的修改后的实现:

  1 包 org.jboss.netty.example.time;
   2导入静态 org.jboss.netty.buffer。.*;
   4导入 java.util.Date;
   6公共类 TimeClientHandler 扩展了{
   8    私有最终buf = dynamicBuffer(); 
    ChannelBuffers SimpleChannelHandler ChannelBuffer(19) 10 @Override 12 public void messageReceived( ctx, e) { m = ( ) e.getMessage(); 14 buf.writeBytes(m); ChannelHandlerContextMessageEvent ChannelBufferChannelBuffer (20) 16 if(buf.readableBytes()>=4){ (21)  long currentTimeMillis = buf.readInt() * 1000L; 18 System.out.println(new Date(currentTimeMillis));  e.getChannel().close(); 20 }  } 22 @Override 24 public void exceptionCaught( ctx, e) {  e.getCause().printStackTrace(); 26 e.getChannel().close();  } 28 } ChannelHandlerContextExceptionEvent

(19)  

动态缓冲区 是一种ChannelBuffer可以根据需求增加容量的缓冲区。当你不知道消息的长度时,它非常有用。

(20)  

首先,所有接收到的数据都应累积到 中 buf

(21)  

然后,处理程序必须检查是否buf有足够的数据(本例中为 4 个字节),然后继续执行实际的业务逻辑。否则,Netty 会 messageReceived在更多数据到达时再次调用该方法,最终累计所有 4 个字节。

7.3. 第二种解决方案

虽然第一个方案解决了 TIME 客户端的问题,但修改后的处理程序看起来并不那么简洁。想象一下一个更复杂的协议,它由多个字段组成,例如可变长度字段。你的ChannelHandler实现很快就会变得难以维护。

ChannelHandler你可能已经注意到,你可以向 中 添加多个ChannelPipeline,因此,你可以将一个整体拆分 ChannelHandler成多个模块化组件,以降低应用程序的复杂性。例如,你可以拆分 TimeClientHandler成两个处理程序:

  • TimeDecoder处理碎片化问题,

  • 的初始简单版本TimeClientHandler

 

幸运的是,Netty 提供了一个可扩展的类,可以帮助您开箱即用地编写第一个类:

  1 包 org.jboss.netty.example.time;
   2公共类 TimeDecoder 扩展 
    FrameDecoder(22){  4 @Override  6 受保护的对象解码( ctx,通道,缓冲区) ChannelHandlerContextChannelChannelBuffer(23){  8 if (buffer.readableBytes() < 4) { 10 返回 null; (24)  } 12 返回缓冲区.readBytes(4); (25) 14  } }

(22)  

FrameDecoder是一种ChannelHandler可以轻松处理碎片问题的实现。

(23)  

FrameDecoderdecode每当收到新数据时,都会使用内部维护的累积缓冲区 调用方法。

(24)  

如果null返回,则表示数据还不够。 FrameDecoder当有足够数据时将再次调用。

(25)  

null如果返回 非,则表示该decode方法已成功解码一条消息。 FrameDecoder将丢弃其内部累积缓冲区的已读取部分。请记住,您不需要解码多条消息。 FrameDecoder将继续调用该decoder方法,直到返回 null

现在我们有另一个处理程序要插入到中ChannelPipeline,我们应该修改ChannelPipelineFactory中的实现 TimeClient

  1          bootstrap.setPipelineFactory(new ChannelPipelineFactory() {  2  public ChannelPipelinegetPipeline() {  return .pipeline(  4 new TimeDecoder(),  new TimeClientHandler());  6 }  }); Channels

如果您喜欢冒险,不妨尝试一下, ReplayingDecoder它可以进一步简化解码器。不过,您需要查阅 API 参考以获取更多信息。

  1 包 org.jboss.netty.example.time;
   2公共类 TimeDecoder 扩展< > {
   4     @Override
   6    受保护的对象解码(
 ctx,通道,
   8缓冲区,状态){
 10        返回缓冲区.readBytes(4);
     }
 12 } 
    ReplayingDecoderVoidEnum ChannelHandlerContextChannel ChannelBufferVoidEnum

此外,Netty 提供了开箱即用的解码器,让您能够非常轻松地实现大多数协议,并帮助您避免最终得到一个难以维护的单片处理程序实现。请参阅以下软件包以获取更详细的示例:

  • org.jboss.netty.example.factorial对于二进制协议,以及

  • org.jboss.netty.example.telnet用于基于文本行的协议。

 

8. 使用 POJO 代替 ChannelBuffer

到目前为止,我们回顾的所有示例都使用ChannelBuffer作为协议消息的主要数据结构。在本节中,我们将改进 TIME 协议客户端和服务器示例,使用 POJO代替 ChannelBuffer

在你的处理程序中使用 POJO 的优势ChannelHandler显而易见;通过将提取信息的代码与ChannelBuffer处理程序分离,你的处理程序将变得更易于维护和复用。在 TIME 客户端和服务器示例中,我们只读取一个 32 位整数,直接使用它并不是什么大问题ChannelBuffer。然而,当你实现实际的协议时,你会发现进行分离是必要的。

首先,让我们定义一个名为的新类型UnixTime

  1 包 org.jboss.netty.example.time;
   2导入 java.util.Date;
   4公共类 UnixTime {
   6    私有最终 int 值;
   8    公共 UnixTime(int 值) {
         this.value = 值;
 10     }
 12    公共 int getValue() {
        返回值;
 14     }
 16     @Override
    公共 String toString() {
 18        返回新的 Date(value * 1000L).toString();
     }
 20 } 
     
             
              
              
           

现在我们可以修改TimeDecoder为返回 aUnixTime而不是 a ChannelBuffer

  1  @Override
   2  protected Object decoder (
 ctx, channel, buffer) {
   4     if (buffer.readableBytes() < 4) {
         return null;
   6     }
   8     return new UnixTime(buffer.readInt());            ChannelHandlerContextChannelChannelBuffer (26) }

(26)  

FrameDecoderReplayingDecoder允许返回任何类型的对象。如果它们被限制只能返回 a ChannelBuffer,我们就必须插入另一个ChannelHandler 将 a 转换ChannelBuffer为 a 的 函数UnixTime

使用更新的解码器,TimeClientHandler 不再使用ChannelBuffer

  1  @Override
   2  public void messageReceived( ChannelHandlerContextctx, MessageEvente) {  UnixTime m = (UnixTime) e.getMessage();  4 System.out.println(m);  e.getChannel().close();  6 }

是不是简洁优雅多了?同样的技术也可以应用在服务器端。 TimeServerHandler这次我们来更新第一个代码:

  1  @Override
   2  public void channelConnected( ChannelHandlerContextctx, ChannelStateEvente) {  UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);  4 f = e.getChannel().write(time);  f.addListener( .CLOSE);  6 } ChannelFuture ChannelFutureListener

现在,唯一缺少的部分就是一个编码器,它是 ChannelHandler将 转换UnixTime回的实现ChannelBuffer。这比编写解码器简单得多,因为在编码消息时无需处理数据包的碎片和组装。

  1 包 org.jboss.netty.example.time;
   2导入静态 org.jboss.netty.buffer。.*;
   4公共类 TimeEncoder 扩展了{
   6    公共 void writeRequested( ctx,     
    ChannelBuffers SimpleChannelHandler ChannelHandlerContextMessageEvent(27)e){  8  UnixTime time =(UnixTime)e.getMessage(); 10 buf = buffer(4);  buf.writeInt(time.getValue()); 12 .write(ctx,e.getFuture(),buf); ChannelBuffer Channels(28) 14  } }

(27)  

编码器重写了该writeRequested 方法以拦截写入请求。请注意, MessageEvent此处的参数与 中指定的类型相同,messageReceived但它们的解释不同。 根据事件流动的方向, AChannelEvent可以是 上游事件,也可以是下游MessageEvent事件。例如,当调用 时, a 可以是上游事件messageReceived,而当调用 时, a 可以是下游事件writeRequested。请参阅 API 参考,了解更多关于上游事件和下游事件之间的区别。

(28)  

将 POJO 转换为 后ChannelBuffer,您应该将新的缓冲区转发到ChannelDownstreamHandler中的前一个ChannelPipeline。 Channels提供了各种辅助方法来生成并发送ChannelEvent。在此示例中, 方法创建一个新的 并将其发送到 中的前一个。 Channels.write(...)MessageEventChannelDownstreamHandlerChannelPipeline

另一方面,使用静态导入是一个好主意 Channels

  1 导入静态 org.jboss.netty.channel。Channels.*;  2  ... pipeline = pipeline();  4 write(ctx,e.getFuture(),buf); fireChannelDisconnected(ctx); ChannelPipeline

 

剩下的最后一个任务是在服务器端插入一个TimeEncoder ,ChannelPipeline这只是一个简单的练习。

9.关闭你的应用程序

如果您运行了TimeClient,您一定注意到应用程序并没有退出,而是继续运行,什么也没做。从完整的堆栈跟踪中,您还会发现有几个 I/O 线程正在运行。要关闭 I/O 线程并让应用程序正常退出,您需要释放 分配的资源ChannelFactory

典型网络应用的关机过程由以下三个步骤组成:

  1. 如果有的话,关闭所有服务器套接字,

  2. 如果有的话,关闭所有非服务器套接字(即客户端套接字和接受套接字),并且

  3. 释放所使用的所有资源ChannelFactory

 

要将上述三个步骤应用于TimeClient, TimeClient.main()可以通过关闭唯一的一个客户端连接并释放 使用的所有资源来正常关闭自身ChannelFactory

  1 包 org.jboss.netty.example.time;
   2公共类 TimeClient {
   4    公共静态 void main(String[] args) 抛出异常 {
         ...
   6工厂 = ...;
引导程序 = ...;
   8         ...
未来 
                  ChannelFactory ClientBootstrap ChannelFuture(29)= bootstrap.connect(...); 10  future.awaitUninterruptibly();(30)  如果(!future.isSuccess()){ 12 future.getCause()。printStackTrace(); (31)  } 14 未来.getChannel().getCloseFuture().awaitUninterruptibly(); (32)  工厂.释放外部资源(); (33) 16  } }

(29)  

connect方法ClientBootstrap 返回一个ChannelFuture用于在连接尝试成功或失败时发出通知的 。它还包含一个Channel与连接尝试关联的 的引用。

(30)  

等待返回ChannelFuture以确定连接尝试是否成功。

(31)  

如果失败,我们会打印失败的原因以了解失败的原因。如果连接尝试既没有成功也没有取消,getCause()则方法ChannelFuture将返回失败的原因。

(32)  

closeFuture 现在连接尝试已经结束,我们需要等待 的 ,直到连接 关闭Channel。每个Channel都有自己的 ,closeFuture 以便您收到通知并在关闭时执行某些操作。

即使连接尝试失败,closeFuture 也会收到通知,因为Channel当连接尝试失败时会自动关闭。

(33)  

此时所有连接都已关闭。剩下的唯一任务就是释放 正在使用的资源ChannelFactory。只需调用其releaseExternalResources() 方法即可。所有资源(包括 NIOSelector和线程池)都将自动关闭并终止。

关闭客户端非常简单,但关闭服务器呢?您需要解除与端口的绑定,并关闭所有已打开的已接受连接。为此,您需要一个数据结构来跟踪活动连接列表,而这并非易事。幸运的是,有一个解决方案,即ChannelGroup

ChannelGroup是 Java 集合 API 的一个特殊扩展,它表示一组打开的Channels。如果Channel将 a 添加到 a 中 ChannelGroup,并且添加的 sChannel已关闭,则已关闭的sChannel 会自动从其中删除ChannelGroup。您还可以对同一组中的所有 s 执行操作Channel。例如,您可以在关闭服务器时 关闭Channela 中的所有 s 。ChannelGroup

为了跟踪打开的套接字,您需要修改 以 TimeServerHandler将新的打开添加Channel到全局ChannelGroupTimeServer.allChannels

  1  @Override
   2  public void channelOpen( ChannelHandlerContextctx, ChannelStateEvente) {  TimeServer.allChannels.add(e.getChannel()); (34)  4  }

(34)  

是的,ChannelGroup是线程安全的。

现在所有活动列表Channel都会自动维护,关闭服务器就像关闭客户端一样简单:

  1 包 org.jboss.netty.example.time;
   2公共类 TimeServer {
   4    静态最终allChannels = new ("time-server" 
     
    ChannelGroupDefaultChannelGroup(35));  6 public static void main(String[] args) throws Exception {  8 ... factory = ...; 10 bootstrap = ...;  ... 12 channel ChannelFactory ServerBootstrap Channel(36)= bootstrap.bind(...);  allChannels.添加(通道); (37) 14  waitForShutdownCommand();(38) 未来=所有通道.关闭(); ChannelGroupFuture(39) 16  未来.awaitUninterruptibly();  factory.releaseExternalResources(); 18 } }

(35)  

DefaultChannelGroup需要将组的名称作为构造函数参数。组名称仅用于区分各个组。

(36)  

bind方法返回一个与指定本地地址绑定的ServerBootstrap 服务器端,调用返回的该方法将解除与绑定的本地地址的绑定。 Channelclose()ChannelChannel

(37)  

任何类型的Channels 都可以添加到 a 中,无论它是在服务器端、客户端还是已接受的 s。因此,当服务器关闭时,您可以一次性 ChannelGroup关闭绑定Channel以及已接受的s。Channel

(38)  

waitForShutdownCommand()是一个虚构的等待关闭信号的方法。你可以等待来自特权客户端或 JVM 关闭钩子的消息。

(39)  

您可以在同一个 中的所有通道上执行相同的操作 ChannelGroup。在这种情况下,我们关闭所有通道,这意味着绑定的服务器端Channel将解除绑定,并且所有已接受的连接将异步关闭。为了通知所有连接何时成功关闭,它返回一个 ,ChannelGroupFuture 其作用与 类似ChannelFuture

10.总结

在本章中,我们快速了解了 Netty,并演示了如何在 Netty 上编写一个功能齐全的网络应用程序。

在接下来的章节中,我们将详细介绍 Netty。我们也建议您查看 org.jboss.netty.example 包中的 Netty 示例。

还请注意, 社区始终在等待您的问题和想法来帮助您,并根据您的反馈不断改进 Netty。

第 2 章 架构概述

Netty架构图

在本章中,我们将研究 Netty 提供了哪些核心功能,以及它们如何在核心之上构成完整的网络应用程序开发堆栈。阅读本章时,请记住这张图。

另外请记住,很多详细的文档都在 javadoc 中。请点击类名和包名的链接。

1.丰富的Buffer数据结构

Netty 使用自己的缓冲区 API 而不是 NIOByteBuffer 来表示字节序列。这种方法比使用 有显著的优势ByteBuffer。Netty 的新缓冲区类型 ChannelBuffer从设计之初就致力于解决 的问题ByteBuffer,并满足网络应用程序开发人员的日常需求。以下是一些很酷的功能:

  • 如果需要的话,您可以定义自己的缓冲区类型。

  • 透明零拷贝是通过内置复合缓冲区类型实现的。

  • 开箱即用地提供动态缓冲区类型,其容量可以根据需要进行扩展,就像 一样StringBuffer

  • 没必要flip()再打电话了。

  • 它通常比 更快ByteBuffer

 

欲了解更多信息,请参阅 org.jboss.netty.buffer包装说明

1.1. 合并和切片 ChannelBuffers

在通信层之间传输数据时,通常需要对数据进行合并或切片。例如,如果有效载荷被拆分成多个包,则通常需要将其合并以进行解码。

传统上,将来自多个包的数据复制到新的字节缓冲区中进行组合。

Netty 支持零拷贝方法,通过ChannelBuffer“指向”所需的缓冲区,从而无需执行拷贝。

合并和切片 ChannelBuffers

2. 通用异步I/O API

Java 中的传统 I/O API 为不同的传输类型提供了不同的类型和方法。例如, java.net.Socket和 java.net.DatagramSocket没有任何通用的超类型,因此它们执行套接字 I/O 的方式截然不同。

这种不匹配使得将网络应用程序从一种传输协议移植到另一种传输协议变得繁琐而困难。当您需要支持其他传输协议时,传输协议之间缺乏可移植性就会成为一个问题,因为这通常需要重写应用程序的网络层。从逻辑上讲,许多协议可以在多种传输协议上运行,例如 TCP/IP、UDP/IP、SCTP 和串行端口通信。

更糟糕的是,Java 的新 I/O(NIO)API 与旧的阻塞 I/O(OIO)API 存在不兼容性,并且在下一个版本 NIO.2(AIO)中仍将如此。由于所有这些 API 在设计和性能特性上都各不相同,因此您通常不得不在开始实现阶段之前就确定应用程序将依赖哪个 API。

例如,您可能想从 OIO 开始,因为您要服务的客户端数量非常少,而且使用 OIO 编写套接字服务器比使用 NIO 容易得多。但是,当您的业务呈指数级增长,并且您的服务器需要同时服务数万个客户端时,您就会遇到麻烦。您也可以从 NIO 开始,但这样做可能会因为 NIO Selector API 的复杂性而大大增加开发时间,从而阻碍快速开发。

Netty 有一个通用的异步 I/O 接口,称为Channel,它抽象出了点对点通信所需的所有操作。也就是说,一旦你在一种 Netty 传输协议上编写了应用程序,它就可以在其他 Netty 传输协议上运行。Netty 通过一个通用 API 提供了许多必要的传输协议:

  • 基于 NIO 的 TCP/IP 传输(参见org.jboss.netty.channel.socket.nio),

  • 基于 OIO 的 TCP/IP 传输(参见org.jboss.netty.channel.socket.oio),

  • 基于 OIO 的 UDP/IP 传输,以及

  • 当地交通(参见org.jboss.netty.channel.local)。

从一种传输方式切换到另一种传输方式通常只需要进行几行更改,例如选择不同的ChannelFactory 实现方式。

 

此外,您甚至可以利用尚未编写的新传输协议(例如串行端口通信传输协议),只需替换几行构造函数调用即可。此外,您还可以通过扩展核心 API 来编写自己的传输协议。

3.基于拦截器链模式的事件模型

对于事件驱动型应用程序来说,定义明确且可扩展的事件模型至关重要。Netty 拥有一个定义明确的事件模型,专注于 I/O。它还允许您在不破坏现有代码的情况下实现自己的事件类型,因为每个事件类型都通过严格的类型层次结构进行区分。这是 Netty 与其他框架的另一个区别。许多 NIO 框架没有事件模型的概念,或者只有非常有限的概念。即使它们提供扩展,当您尝试添加自定义事件类型时,它们也经常会破坏现有代码。

A由中的 s ChannelEvent列表处理。管道实现了 拦截过滤器 模式的高级形式,使用户能够完全控制事件的处理方式以及管道中处理程序之间的交互方式。例如,您可以定义从套接字读取数据时要执行的操作: ChannelHandlerChannelPipeline

  1  public class MyReadHandler implements SimpleChannelHandler{  2  public void messageReceived( ChannelHandlerContextctx, MessageEventevt) {  Object message = evt.getMessage();  4 // 对收到的消息执行某些操作。  ...  6 // 并将事件转发到下一个处理程序。  8 ctx.sendUpstream(evt);  } 10 }

您还可以定义处理程序收到写入请求时要执行的操作:

  1  public class MyWriteHandler implements SimpleChannelHandler{  2  public void writeRequested( ChannelHandlerContextctx, MessageEventevt) {  Object message = evt.getMessage();  4 // 对要写入的消息执行某些操作。  ...  6 // 并将事件转发到下一个处理程序。  8 ctx.sendDownstream(evt);  } 10 }

有关事件模型的更多信息,请参阅ChannelEvent和的API文档ChannelPipeline

4. 高级组件助力更快速的开发

除了上述已经能够实现所有类型网络应用程序的核心组件之外,Netty 还提供了一组高级功能来进一步加速开发页面。

4.1. 编解码器框架

正如第 8 节“使用 POJO 而非 ChannelBuffer 进行通信” 中所述,将协议编解码器与业务逻辑分离始终是一个好主意。然而,从头实现这个想法会遇到一些复杂问题。您必须处理消息碎片。有些协议是多层的(即构建在其他较低层协议之上)。有些协议过于复杂,无法在单个状态机中实现。

因此,一个好的网络应用程序框架应该提供一个可扩展、可重用、可单元测试和多层的编解码器框架,以生成可维护的用户编解码器。

Netty 提供了许多基本和高级编解码器来解决您在编写协议编解码器时遇到的大多数问题,无论它是简单的还是复杂的、二进制的还是文本的 - 无论如何。

4.2. SSL/TLS 支持

与传统的阻塞 I/O 不同,在 NIO 中支持 SSL 并非易事。您不能简单地包装一个流来加密或解密数据,而必须使用javax.net.ssl.SSLEngine。 SSLEngine它是一个状态机,其复杂程度与 SSL 本身相当。您必须管理所有可能的状态,例如密码套件和加密密钥协商(或重新协商)、证书交换和验证。此外,SSLEngine它甚至不像人们所期望的那样完全线程安全。

在 Netty 中,SslHandler它处理了所有复杂的细节和陷阱SSLEngine。您只需配置并将SslHandler其插入到您的 中即可。它还允许您 轻松 ChannelPipeline实现 StartTLS等高级功能。

4.3. HTTP 实现

HTTP 无疑是互联网上最流行的协议。目前已经有很多 HTTP 实现,例如 Servlet 容器。那么,为什么 Netty 的核心是 HTTP 呢?

Netty 的 HTTP 支持与现有的 HTTP 库截然不同。它让您能够完全控制 HTTP 消息在底层的交换方式。由于 Netty 本质上是 HTTP 编解码器和 HTTP 消息类的组合,因此不存在诸如强制线程模型之类的限制。也就是说,您可以编写自己的 HTTP 客户端或服务器,使其完全按照您的需求运行。您可以完全控制 HTTP 规范中的所有内容,包括线程模型、连接生命周期和分块编码。

由于其高度可定制的特性,您可以编写一个非常高效的 HTTP 服务器,例如:

  • 需要持久连接和服务器推送技术的聊天服务器(例如Comet

  • 媒体流服务器需要保持连接打开,直到整个媒体流传输完毕(例如 2 小时的视频)

  • 允许上传大文件而没有内存压力的文件服务器(例如,每个请求上传 1GB)

  • 可扩展的混搭客户端,可异步连接到数万个第三方 Web 服务

 

4.4. WebSockets 实现

WebSockets允许通过单个传输控制协议 (TCP) 套接字建立双向全双工通信通道。它旨在允许 Web 浏览器和 Web 服务器之间进行数据流传输。

WebSocket 协议已被 IETF 标准化为RFC 6455

Netty 实现了 RFC 6455 以及该规范的一些旧版本。请参阅 org.jboss.netty.handler.codec.http.websocketx包及其相关 示例

4.5. Google Protocol Buffer 集成

Google 协议缓冲区 (IPB) 是快速实现高效且可随时间演进的二进制协议的理想解决方案。借助ProtobufEncoder和 ProtobufDecoder,您可以将 Google 协议缓冲区编译器 (protoc) 生成的消息类转换为 Netty 编解码器。请查看 “LocalTime”示例,该示例展示了如何轻松地从示例协议定义 创建高性能二进制协议客户端和服务器 。

5.总结

本章我们从功能角度回顾了 Netty 的整体架构。Netty 拥有简洁而强大的架构。它由三个组件组成——缓冲区、通道和事件模型——所有高级功能都构建在这三个核心组件之上。一旦您理解了这三个组件如何协同工作,理解本章简要介绍的更高级功能应该不难。

您可能仍对整体架构以及各项功能如何协同工作存有疑问。如果是,欢迎 与我们联系,我们将共同改进本指南。

常见问题

此常见问题解答是StackOverflow 中的问题和答案的摘要。

1.什么时候可以写入下行数据?

只要您有对 Channel(或 ChannelHandlerContext)的引用,您就可以从任何地方、任何线程调用 Channel.write()(或 Channels.write())。

当您通过调用 Channel.write() 或调用 ChannelHandlerContext.sendDownstream(MessageEvent) 触发 writeRequested 事件时,将调用 writeRequested()。

参见 讨论

2.如何将阻塞应用程序代码与非阻塞 NioServerSocketChannelFactory 结合起来?

NioServerSocketChannelFactory使用老板线程和工作线程。

主线程负责接收传入的连接,而工作线程负责对相关通道执行非阻塞读写操作。线程池中默认的工作线程数为可用处理器数量的 2 * 。

如果您的应用程序的处理程序阻塞(例如(从数据库读取)或占用大量 CPU),则工作线程池可能会耗尽,性能也会下降。

我们建议您在另一个线程池中实现阻塞应用程序代码。您可以通过在通道管道中添加 OrderedMemoryAwareThreadPoolExecutor 来实现,该线程池位于您的处理程序之前,或者您也可以实现自己的线程池。

  1  public static void main(String[] args) throws Exception {
   2          OrderedMemoryAwareThreadPoolExecutor eventExecutor =
             new OrderedMemoryAwareThreadPoolExecutor(
   4                     5, 1000000, 10000000, 100,
                     TimeUnit.MILLISECONDS);
   6         ServerBootstrap bootstrap = new ServerBootstrap(
   8                 new NioServerSocketChannelFactory(
                         Executors.newCachedThreadPool(),
 10                         Executors.newCachedThreadPool()));
 12         sb.setPipelineFactory(new MyPipelineFactory(eventExecutor));
         sb.bind(socketAddress);
 14         // 其他代码
16         return;               
 18     }
 20     public class MyPipelineFactory implements ChannelPipelineFactory {
     @Override
 22     public ChannelPipeline getPipeline() throws Exception {
         // 创建一个默认的管道实现。
 24         ChannelPipeline pipeline = pipeline();
 26         pipeline.addLast("decoder", new HttpRequestDecoder());
         pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
 28         pipeline.addLast("encoder", new HttpResponseEncoder());
         pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());
 30         // 在阻塞处理程序之前插入 OrderedMemoryAwareThreadPoolExecutor
 32         pipeline.addLast("pipelineExecutor", new ExecutionHandler(_pipelineExecutor));
 34         // MyHandler 包含阻塞代码
        pipeline.addLast("handler", new MyHandler());
 36         return pipeline;
 38     }
 40     public class MyHandler extends SimpleChannelUpstreamHandler {
         // 您的阻塞应用程序代码
42 }                              
               
              
     
             
               
                   
                 
              
             
      

3. 鉴于事件可能同时发生,我是否需要同步我的处理程序代码?

ChannelUpstreamHandler将被同一个线程(即 I/O 线程)顺序调用,因此处理程序不必担心在前一个上游事件完成之前被新的上游事件调用​​。

但是,下游事件可能由多个线程同时触发。如果您ChannelDownstreamHandler 访问共享资源或存储状态信息,则可能需要适当的同步。

参见 讨论

4.如何在同一个 Channel 中的处理程序之间传递数据?

使用 ChannelLocal。

  1   2     // 声明
    public static final ChannelLocal<int> data = new ChannelLocal<int>();
   4     // 设置
  6     data.set(e.getChannel(), 1);
   8     // 获取
    int a = data.get(e.getChannel()); 
      
         
     

参见 讨论

 

posted @ 2025-09-17 10:58  CharyGao  阅读(6)  评论(0)    收藏  举报