A preliminary netty
As a NIO client server framework, Netty can quickly and easily build network applications, such as protocol servers and clients. Netty absorbs the experience of implementing protocols such as FTP, SMTP, and HTTP to ensure the robustness and maintainability of programs on the basis of ease of use and agility.
When we first learned Java network programming, we all opened a socket port, and then called the accept() method to block the connection, and then kept reading data. As we learn more later, we’ll try reading data in a non-blocking way and non-blocking I/O using the Selector Selector.
As the business grows, tens of thousands of concurrent transactions are no longer impossible. For higher throughput and scalable performance, robust and easy client development framework has become the pursuit of developers, and Netty perfectly meets the needs of people. It encapsulates Java’s complex underlying apis and exposes them in an easy-to-use way, enabling netty to focus more on the development of business logic rather than the trivial underlying architecture.
The following are the core components of Netty, which will be detailed in a follow-up note:
-
Channel.
A channel is a core concept of Java NIO and represents an operational connection to an entity (such as a network connection, file I/O operation).
-
The callback.
A callback is actually a method, and when a method is called, its specified (or bound) callback method is also called.
-
The Future.
A Future is a placeholder for an asynchronous operation whose corresponding Future object is invoked when the asynchronous operation completes. Netty’s Future implementation, ChannelFuture, allows an asynchronous operation to register multiple ChannelFutureListener instances. Each netty outbound I/O returns a ChannelFuture instance.
-
Events and handlers.
Netty notifies us of a change in the state of an operation through events such as:
- Connection activation or deactivation (inbound event).
- Data reads (inbound events).
- User events (inbound events).
- Error events (inbound events).
- Open or close a connection to a remote node (outbound event).
- Write data to the socket (outbound event).
Netty has many implementations for ChannelHandler, and you can also customize the implementation. Each event is distributed to a method in the corresponding ChannelHandler class.
Note: Inbound and outbound are relative to ChannelHandler. Inbound is inbound and outbound is outbound.
Code sample
Here is a simple netty use:
Inbound processor implementation
@ChannelHandler.Sharable // Identifies a channelHandler that can be safely called by multiple channels.
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
// This method is called when there is an inbound message
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf)msg;
System.out.println("Server receives message :" + buffer.toString(CharsetUtil.UTF_8));
// Write the received message to the sender without flushing the outbound message.
ctx.write(buffer);
}
@Override
// channelRead is triggered when it finishes consuming read data
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// Flush pending messages to the remote node and close the channel
ChannelFuture channelFuture = ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
@Override
// Handle exceptions during read operations
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// Prints the exception stack and closes the channelcause.printStackTrace(); ctx.close(); }}Copy the code
Note: Netty server handler processing is in the form of a chain of responsibility. By default, channelHandler forwards method calls to it to the next channelHandler in the chain. If the exceptionCaught() method is not implemented somewhere in the chain, the received exception is passed to the end of the channelPipeline and logged.
Server implementation
public class NettyServer {
public static void main(String[] args) {
NettyServer nettyServer = new NettyServer();
nettyServer.start(8888);
}
public void start(int port){
// Process TCP connection requests
EventLoopGroup bossGroup = new NioEventLoopGroup();
// Handle I/O events
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
// Used to boot and bind the server
ServerBootstrap bootstrap = new ServerBootstrap();
// Add the above thread group to the bootstrap
bootstrap.group(bossGroup,workGroup)
// Set the channel to be asynchronous
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
// Since NettyServerHandler is marked @sharable, the same instance can be used
socketChannel.pipeline().addLast(new NettyServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG,200)
.childOption(ChannelOption.SO_KEEPALIVE,true);
// Asynchronously bind the server, calling sync() to block and wait until the bind is complete.
ChannelFuture future = bootstrap.bind(port).sync();
// Get closeFuture for the channel and block until it completes.
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{ bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); }}}Copy the code
Outbound processor implementation
@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer(Netty "active", CharsetUtil.UTF_8));
}
// Records received message stores
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Client receives message:" + msg.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}Copy the code
SimpleChannelInboundHandler and ChannelInboundHandler:
Clients use SimpleChannelInboundHandler asynchronous operations because it didn’t need to be considered when channelRead0 method after the execution, SimpleChannelInboundHandler will release the memory ByteBuffer to save this message.
The server uses ChannelInboundHandler because it needs to send messages to the client. The ctx.write() method is asynchronous and may not return after the channelRead() method has executed. So ChannelInboundHandler is used to avoid this. The channelReadComplete method is fired when channelRead() has finished consuming the read data, flushing output to the channel.
Client-side implementation
public class NettyClient {
public static void main(String[] args) {
NettyClient nettyClient = new NettyClient();
nettyClient.connect("localhost".8888);
}
public void connect(String hostname,int port) {
// Process TCP connection requests
EventLoopGroup group = new NioEventLoopGroup();
try {
// Used to boot and bind the server
Bootstrap bootstrap = new Bootstrap();
// Add the above thread group to the bootstrap
bootstrap.group(group)
// Set the channel to be asynchronous
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline()
.addLast(new NettyClientHandler());
}
})
.option(ChannelOption.TCP_NODELAY,true);
// Connect to the remote node, block and wait until the connection is complete.
ChannelFuture future = bootstrap.connect(hostname, port).sync();
// block until the channel is closed.
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Close the thread pool and release all resources.group.shutdownGracefully(); }}}Copy the code
conclusion
As you can see from the code examples above, the Netty server and client are built much the same, implementing the processor and binding it.
Server-side development:
- Create and implement processor logic.
- The application expands as required
ChannelHandler
. - Called for different types of events
ChannelHandler
.
- The application expands as required
- create
ServerBootstrap
Instance to boot and bind the server. - Create and assign one
NioEventLoopGroup
Instance for request processing. - Create and assign one
NioEventLoopGroup
Instance for event handling. - Specifies the local to which the server is bound
InetSocketAddress
. - Initialize each new one with a processor instance
channel
. - call
ServerBootstrap.bind()
Method to bind the server.
Client development:
- Create and implement processor logic.
- The application expands as required
ChannelHandler
. - Called for different types of events
ChannelHandler
.
- The application expands as required
- Create a Bootstrap instance to initialize the client.
- Create and assign one
NioEventLoopGroup
Instance for event handling. - Create for the server
InetSocketAddress
Instance. - When a connection is established, one
Handler
Will be installed to thechannel
theChannelPipline
On. - call
Bootstrap.connect()
Method to connect to a remote node.
Earlier, we took a look at Netty, including its core content, simple use. We will continue to look at the main components and design concepts of Netty.
Netty component design
The Channel interface
We know that the core components of NetTY include channels, an important concept in Java that represents a connection to an entity operation. Netty abstracts several operations as channels, including:
- A Socket operation.
- Multithreading.
- Asynchronous notification.
We know that the basic I/O operations in the network (connection establishment, data reading, data writing) are dependent on the interface provided by the underlying network transport, which is represented by the Socket class in Java. Netty further encapsulates this class and greatly reduces the complexity of the Socket class. It provides a series of implementations based on the Channel interface:
- EmbeddedChannel
- EpollDatagramChannel
- LocalServerChannel
- KQueueDatagramChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
As shown in the figure, each Channel is assigned a ChannelPipline and a ChannelConfig, which contains all the configuration for the Channel and supports hot updates. The default ChannelConfig is normally created when a Channel instance is created:
// NioServerSocketChannel constructor
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
Copy the code
Netty provides a ChannelOption class that defines all the parameter types supported by ChannelConfig, which can be used as follows:
NioServerSocketChannel channel = new NioServerSocketChannel();
ServerSocketChannelConfig config = channel.config();
config.setOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
// For each ChannelOption option, Netty also provides a corresponding method, such as the above setting can be used instead
// config.setAllocator(PooledByteBufAllocator.DEFAULT);
// Set the channel
bootstrap.channel(channel.getClass());
Copy the code
The Comparable interface is implemented to ensure that channels are sequential. So if two different channels have the same hash value, an error will be thrown.
Channel also provides other methods, the main ones are:
The method name | describe |
---|---|
eventLoop | Return the corresponding EventLoop |
pipeline | Returns the corresponding ChannelPipline |
localAddress | Return the local SocketAddress |
remoteAddress | Return the remote SocketAddress |
write | Write data to the remote node, and this data will be passed to ChannelPipline and queued until it is flushed |
flush | Flush the previously written data to the remote node |
writeFlush | Write data and flush it to the remote node |
isActive | Checks whether the Channel is active |
Netty supported transport
NIO
NIO implements all asynchronous I/O operations with a Selector, which runs on a thread that checks for state changes and reacts to them. The Selector patterns by Java. Nio. Channels. SelectionKey definition:
The name of the | describe |
---|---|
OP_ACCEPT | Get notified when you accept a connection and create a Channel |
OP_CONNECT | Get notified when a connection is established |
OP_READ | Get notified when data is available for reading |
OP_WRITE | Gets notification when a Channel’s send buffer is writable |
Epoll
Epoll is a high-performance, extensible I/O event notification feature from Linux. Netty provides an API for Epoll on Linux.
OIO
OIO (Old IO) is the old blocking I/O that can be used to transition during project migration through the normal transport API.
Local
It is a local transport provided by Netty for asynchronous communication between clients and servers running in the same JVM.
Embedded
Embedded transports make it possible to embed a set of ChannelHandlers inside other ChannelHandlers as helper classes, extending the functionality of a Single ChannelHandler without modifying the internal code. The key to Embedded transport is the Channel implementation of the EmbeddChannel.
The lifecycle of a Channel
A Channel defines a set of state modes associated with ChannelInboundHandler:
state | describe |
---|---|
ChannelUnregistered | A Channel has been created but is not registered with EventLoop |
ChannelRegistered | Channels are registered with EventLoop |
ChannelActive | A Channel is active and can receive and send data |
ChannelInactive | The Channel is inactive |
Unit testing based on EmbeddedChannel
EmbeddedChannel is provided by netty specifically to improve unit testing for ChannelHandler. You can use it to simulate send and request messages to test the functional implementation of the corresponding ChannelHandler.
EmbeddedChannel provides the following commonly used apis:
API | describe |
---|---|
writeInbound | Write an inbound message to the EmbeddedChannel. Returns true if the data can be read from the EmbeddedChannel via readInbound(); |
readInbound | Read the inbound message from the EmbeddedChannel. Any return traverses the entire ChannelPipeline. If the read is not ready, this method returns NULL; |
writeOutbound | Write an outbound message to an EmbeddedChannel. Returns true if the data can be read from the EmbeddedChannel through readOutbound(); |
readOutbound | Read the outbound message from the EmbeddedChannel. Any return traverses the entire ChannelPipeline. If the read is not ready, this method returns NULL; |
Finish | If data can be read from the inbound or outbound, the flag EmbeddedChannel completes and returns. This also calls the closed method of the EmbeddedChannel; |
@Test
public void testFramesDecoded(a){
ByteBuf buf= Unpooled.buffer();
for (int i=0; i<9; i++){ buf.writeByte(i); } ByteBuf input=buf.duplicate(); EmbeddedChannel channel=new EmbeddedChannel(
new FixedLengthFrameDecoder(3)); assertTrue(channel.writeInbound(input.retain())); assertTrue(channel.finish());// Read the message
ByteBuf read=channel.readInbound();
assertEquals(buf.readSlice(3),read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
assertNull(channel.readInbound());
buf.release();
}
Copy the code
EventLoop interface
EventLoop defines netTY’s core abstraction for handling what happens during the lifetime of a connection.
From the picture above, we can see that:
- An EventLoopGroup contains one or more Eventloops.
- An EventLoopGroup can be bound to only one Thread during its lifetime.
- All I/O events processed by EventLoop will be processed on its proprietary Thread.
- A Channel can register with only one EventLoop during its lifetime.
- An EventLoop may be assigned to one or more channels.
Netty uses EventLoop to handle request tasks in connection. EventLoop uses two basic apis: networking and concurrent programming. The io.netty.util.concurrent package is built on the JUC package to provide an executor for a thread. The IO. Util. channel package classes extend these classes and interfaces to interact with channel events.
The task scheduling of netty extended JUC SecheduledExecutorService, because netty timing task can fit into a EventLoop execution queue, don’t need to switch from the thread like JUC, so reduces the performance overhead:
ctx.channel().eventLoop().schedule(new Runnable() { // Create a task processing thread @Override public void run(a) { System.out.println(EventLoop Task Scheduling); }},60, TimeUnit.MICROSECONDS); // Specify the scheduling period Copy the code
ChannelFuture interface
Netty provides ChannelFuture interface is used to as a placeholder of asynchronous invocation, ChannelFuture. The addListener () method to register a ChannelFutureListener, in order to be notified when an operation to complete.
ChannelHandler interface
The ChannelHandler interface becomes the container for application logic that handles inbound and outbound data.
Netty provides a number of default ChannelHandler implementations in adapter mode, designed to simplify the development of application processing logic. Here are the adapter classes that are common when writing a custom ChannelHandler:
- ChannelHandlerAdapter
- ChannelInbouAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandler
ChannelHandler has a life cycle that calls operations when a ChannelHandler is added or removed from a ChannelPipline.
type | describe |
---|---|
handlerAdded | ChannelHandler is added to ChannelPipline |
handlerRemoved | ChannelHandler was removed by ChannelPipline |
exceptionCaught | An error occurred in ChannelPipline |
We can customize our own processing logic by implementing the ChannelInboundHandler or ChannelOutboundHandler interface, Also can be done by extending ChannelInboundHandlerAdapter or ChannelOutboundHandlerAdapter class.
Resource management: As the processor processes data, we need to ensure that there is no resource leakage at the end of the process, and the ByteBuf reference counting technique described later addresses this problem. Netty provides resource Detector to detect memory leaks. Leakage level can be through the Java – Dio.net ty. To specify leakDetectionLevel = leakage level. The leak levels defined by Netty are:
level | describe |
---|---|
DISABLED | Disable leak detection |
SIMPLE | Use a default sampling rate of 1% and report found leaks (default level) |
ADVANCED | Use a 1% sampling rate and report the leaks found and the corresponding locations where the messages were accessed |
PARANOID | Sample each message access and report the leaks found and the corresponding access location |
Encoders and decoders are typical ChannelHandler implementations.
In server development, pay attention to resolving sticky packages. The general solution is to define a protocol format and parse the data according to the protocol upon receiving the information. Netty provides different types of abstractions for different format designs, most of which are named XxxDecoder and XxxEncoder, such as ProtobufEncoder and ProtobufDecoder, which support Google Protocol Buffers.
Netty decoding encoders can be broadly divided into two categories:
- From ByteToMessage (ByteToMessage)
- From MessageToMessage
We can implement our own processors by extending netty’s preset decoders and encoders. For each message read from an inbound Channel, after the channelRead() method completes, it calls the decode() method provided by the decoder and forwards the decoded bytes to the next ChannelInboundHandler in the ChannelPipline. The exit is similar.
decoder
Netty bytes to the news of the encoder provides a base class: ByteToMessageDecoder (inherited ChannelInboundHandlerAdapter), the class will be on inbound data buffer, until ready to process. It has two most important methods:
API | describe |
---|---|
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) | Abstract methods that must be implemented. The decode() method is called with a ByteBuf containing the incoming data and a List to add decoded messages to. The call to this method is repeated until it is determined that no new elements have been added to the List or that there are no more bytes in the ByteBuf to read. Then, if the List is not empty, its contents will be passed to the next ChannelInboundHandler in the ChannelPipeline. |
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List< Objec t> out) | This method is called once when a Channel’s state becomes inactive. The default is to call the decode() method. |
ReplayingDecoder extends ByteToMessageDecoder (inherited ChannelInboundHandlerAdapter), ReplayingDecoder when processing the data don’t have to determine the length of the receiving data. ReplayingDecoder implements its own ReplayingDecoderByteBuf, throws an exception when there is not enough data, and then ReplayingDecoder resets the readerIndex and calls the decode method again.
Although ReplayingDecoder is more convenient than ByteToMessageDecoder, in fact, ReplayingDecoder runs slightly slower than ByteToMessageDecoder.
Netty provides a base class for message-to-message encoder implementations: MessageToMessageDecoder. It has the most important method:
API | describe |
---|---|
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) | Abstract methods that must be implemented. The decode() method is called with a ByteBuf containing the incoming data and a List to add decoded messages to. The call to this method is repeated until it is determined that no new elements have been added to the List or that there are no more bytes in the ByteBuf to read. Then, if the List is not empty, its contents will be passed to the next ChannelInboundHandler in the ChannelPipeline. |
Netty provides a TooLongFrameException exception, which is thrown when the decoder exceeds a specified frame size limit, preventing the decoder from running out of memory by buffering large amounts of data.
Decoding delimite-based protocols: The Delimited messaging protocol uses defined characters to mark the beginning or end of a message or message segment (frame). Decoders for handling delimiter based and length based protocols are:
The name of the | describe |
---|---|
DelimiterBasedFrameDecoder | A generic decoder to extract frames using any user-supplied delimiter |
LineBasedFrameDecoder | A decoder that extracts frames separated by line terminators (\n or \r\n). The decoder is quicker than the DelimiterBasedFrameDecoder |
Decoding length-based protocols: A length-based protocol defines a frame by encoding its length to the frame’s header, rather than using a special delimiter to mark its end. Decoders for length based protocols are:
The name of the | describe |
---|---|
FixedLengthFrameDecoder | Extracts the fixed-length frame specified when the constructor is called |
LengthFieldBasedFrameDecoder | Extract the frame according to the length value in the encoding frame header; The offset and length of this field are specified in the constructor |
The encoder
The encoder implements the ChannelOutboundHandler and converts outbound data from one format to another.
MessageToByteEncoder is the base class for converting messages to bytes. The most important method is:
API | describe |
---|---|
encode(ChannelHandlerContext ctx,I msg,ByteBuf out) | Abstract methods that must be implemented. Is called to pass in an outbound message of type I to be encoded by the class as ByteBuf. This ByteBuf will then be forwarded to the next ChannelOutboundHandler in the ChannelPipeline |
ByteToMessageDecoder has more decodeLast methods than MessageToByteEncoder because the decoder usually needs to produce the last message after a Channel is closed.
MessageToMessageEncoder is the base class for converting messages to messages. The most important methods are:
API | describe |
---|---|
encode(ChannelHandlerContext ctx,I msg,List< Object > out) | Abstract methods that must be implemented. Each message written by the write() method is passed to the encode() method to encode one or more outbound messages. These outbound messages are then forwarded to the next ChannelOutboundHandler in the ChannelPipeline |
codecs
From above, we know that the encoder is inherited ChannelInboundHandlerAdapter, and inherited ChannelOutboundHandlerAdapter decoder, so if we implement these two features at the same time, whether it can be integrated into the encoding and decoding in a class? Netty provides us with both byte and message codecs.
ByteToMessageCodec: Abstract class that combines ByteToMessageDecoder and MessageToByteEncode. This class consists of three important methods:
API | describe |
---|---|
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) | This method will be called whenever there are bytes available to consume. It converts the inbound ByteBuf to the specified message format and forwards it to the next ChannelInboundHandler in the ChannelPipeline |
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) | The default implementation of this method delegates to the decode() method. It is called only once when a Channel’s state becomes inactive. It can be overridden to implement special processing. |
encode(ChannelHandlerContext ctx,msg,ByteBuf out) | This method will be called for every message of type I that will be encoded and written to outbound ByteBuf |
Message codec: The MessageToMessageCodec abstract class, which enables round-tripping of this transformation in a single class by using MessageToMessageCodec. Two important methods of this class:
API | describe |
---|---|
protected abstract decode(ChannelHandlerContext ctx,INBOUND_IN msg,List< Object > out) | This method is called and passed a message of type INBOUND_IN. It decodes them into messages of type OUTBOUND_IN, which are forwarded to the next ChannelInboundHandler in the ChannelPipeline |
protected abstract encode(ChannelHandlerContext ctx,OUTBOUND_IN msg,List< Object > out) | This method will be called for each message of type OUTBOUND_IN. These messages will be encoded as INBOUND_IN messages and forwarded to the next ChannelOutboundHandler in the ChannelPipeline |
CombinedChannelDuplexHandler encoder and the decoder can be grouped to, as shown in the following cases:
// ByteToCharDecoder is a custom encoder, CharToByteEncoder is a custom decoder
public class CombinedByteCharCodec extends
CombinedChannelDuplexHandler<ByteToCharDecoder.CharToByteEncoder> {
public CombinedByteCharCodec(a) {
super(new ByteToCharDecoder(), newCharToByteEncoder()); }}Copy the code
Serialization and deserialization
Netty provides three serialization methods:
-
Serialization comes with the JDK.
The name of the describe CompatibleObjectDecode Decoders that interoperate with non-Netty based remote nodes that use JDK serialization CompatibleObjectEncoder Encoders that interoperate with non-Netty based remote nodes serialized with the JDK ObjectDecoder Decoders built on JDK serialization that use custom serialization to decode; It provides an improvement in speed when there are no other external dependencies. Otherwise, other serialization implementations are preferable ObjectEncoder Encoders built on JDK serialization that are encoded using custom serialization; It provides an improvement in speed when there are no other external dependencies. Otherwise, other serialization implementations are preferable -
Serialization with JBoss: JBoss not only fixes some issues with the JDK’s built-in serializer, but also improves performance.
The name of the describe CompatibleMarshallingDecoder,CompatibleMarshallingEncoder Compatible with remote nodes that only use JDK serialization MarshallingDecoder,MarshallingEncoder This applies to nodes using JBoss Marshalling. These classes must be used together -
Serialization using Protocol Buffers: Protocol Buffers is a data exchange format developed and open-source by Google.
The name of the describe ProtobufDecoder The message is decoded using protobuf ProtobufEncoder The message is encoded using protobuf ProtobufVarint32FrameDecoder Dynamically split the received ByteBuf based on the value of the “Base 128 Varints” integer length field of Google Protocol Buffers in the message ProtobufVarint32LengthFieldPrepender Append a length field of Google Protocal Buffers integer “Base 128 Varints” to ByteBuf
Preset ChannelHandler
Using HTTPS: Netty uses the Javax.net.ssl package through SslHandler to implement SSL encryption. It also provides an SSLEngine implementation of the OpenSSL tool, OPenSSLEngine, which performs better than the JDK’s SSLEngine. By default, Netty tries to load OpenSSLEngine, and if it fails to load JdkSSLEngine. Its related methods are:
API | describe |
---|---|
setHandshakeTimeout (long,TimeUnit),setHandshakeTimeoutMillis (long),getHandshakeTimeoutMillis() | Set and get the timeout period after which handshake ChannelFuture will be notified of failure |
setCloseNotifyTimeout (long,TimeUnit),setCloseNotifyTimeoutMillis (long),getCloseNotifyTimeoutMillis() | Sets and gets the timeout period, after which a close notification is triggered and the connection is closed. This will also cause notification of the ChannelFuture to fail |
handshakeFuture() | Returns a ChannelFuure that will be notified when the handshake is complete. If the handshake has already been performed, a ChannelFuture containing the results of the previous handshake is returned |
close(),close(ChannelPromise),close(ChannelHandlerContext,ChannelPromise) | Send close_notify to request that the underlying SslEngine be turned off and destroyed |
Http codecs: Netty provides multiple ChannelHandlers for formatting data into Http responses or parsing requests into data. The following figure shows the components of an HTTP request response:
The main codecs are:
The name of the | describe |
---|---|
HttpRequestEncoder | Encode HttpRequest, HttpContent, and LastHttpContent messages into bytes |
HttpResponseEncoder | Encode HttpResponse, HttpContent, and LastHttpContent messages as bytes |
HttpRequestDecoder | Decode the bytes into HttpRequest, HttpContent, and LastHttpContent messages |
HttpResponseDecoder | Decode the bytes into HttpResponse, HttpContent, and LastHttpContent messages |
HTTP aggregation: For some request or response data, Netty’s codecs may not parse it in its entirety, but instead parse it into multiple pieces of data, such as HttpServerCodec, which can only get parameters in the URI. If a POST request is used, the information is stored in messageBody, so it cannot be fully parsed. You need to add HttpObjectAggregator. The HttpObjectAggregator architecture is as follows:
MessageAggregator decode() has a currentMessage parameter, which is a member variable of the handler. Each channel corresponds to a handler instance, and currentMessage stores the results of multiple decode iterations. This is the key to aggregation implementation.
HTTP compression: Although HTTP data compression costs the server clock, it saves network traffic and speeds up transmission rates. The client can use HttpContentDecompressor to process the content from the server, and the server uses HttpContentCompressor() to compress the data.
Websocket: Netty provides a variety of frameworks for implementing WebSocket long connections. Here are the websocketFrame types:
The name of the | describe |
---|---|
BinaryWebSocketFrame | Data frame: Binary data |
TextWebSocketFrame | Data frame: Text data |
ContinuationWebSocketFrame | Data frame: the text or binary data belonging to the previous BinaryWebSocketFrame or TextWebSocketFrame |
CloseWebSocketFrame | Control frame: a CLOSE request, the status code for closing, and the reason for closing |
PingWebSocketFrame | Control frame: request a PongWebSocketFrame |
PongWebSocketFrame | Control frame: response to a PingWebSocketFrame request |
To add security to the WebSocket, simply add SslHandler as the first ChannelHandler to the ChannelPipeline.
Connection management: Netty provides a manager for detecting and processing idle and timeout connections. The main ones are:
The name of the | describe |
---|---|
IdleStateHandler | An IdleStateEvent event is emitted when the connection is idle for too long. You can then handle the IdleStateEvent event by overriding the userEventTriggered() method in ChannelInboundHandler |
ReadTimeoutHandler | If no inbound data is received within the specified time interval, a ReadTimeoutException is thrown and the corresponding Channel is closed. This ReadTimeoutException can be detected by overriding the exceptionCaught() method in ChannelHandler |
WriteTimeoutHandler | If no outbound data is written within the specified time interval, a WriteTimeoutException is thrown and the corresponding Channel is closed. You can detect this WriteTimeoutException by overriding the exceptionCaught() method of your ChannelHandler |
ChannelPipline interface
The ChannelPipline interface implements chained calls to add ChannelHandler containers. When a Channel is created, it is automatically assigned to the ChannelPipline it belongs to.
As shown, when an inbound message comes in, it starts at the head of the ChannelPipline and is passed to the first ChannelboundHandler. After this ChannelHandler has been processed, It is passed to the next ChannelInboundHandler until it reaches the end of the ChannelPipline. The outbound of messages is similar to the inbound.
When a ChannelHandler is added to a ChannelPipline, it is assigned a ChannelHandlerContext that represents the binding between ChannelHandler and ChannelPipline.
In Netty, there are two ways to send messages:
- Write directly to a Channel, which causes messages to flow from the end of the ChannelPipline.
- Write directly to the ChannelHandlerContext, which will allow messages to flow from the next ChannelHandlerContext in the ChannelPipline.
As ChannelPipline propagates an event, it tests whether the type of the next ChannelHandler in ChannelPipline matches the direction of the event’s movement, and if it doesn’t, it skips the ChannelHandler.
ChannelHandlerContext
ChannelHandlerContext is used to enable ChannelHandler to interact with ChannelPipline. ChannelHandler can notify the next ChannelHandler of the ChannelPipline that it belongs to to modify its ChannelPipline.
ChannelHandlerContext API:
ChannelHandlerContext has some methods that Channel and ChannelPipline have as well. Note that if you call these methods on a Channel or Channel pipeline, They propagate along the entire ChannelPipeline. Calling the same method on ChannelHandlerContext will start with the currently associated ChannelHandler and will only propagate to the next ChannelHandler in the ChannelPipeline capable of handling the event.
ServerBootstrap/Boostrap
Netty’s Bootstrap class provides a container for the network layer configuration of the application. ServerBootstrap is used to boot the server and Bootstrap is used to boot the client.
We can see that both ServerBootstrap and Bootstrap implement AbstractBootstrap abstract classes.
In the previous article, when implementing the server-side bootstrap, we created and bound two EventLoopGroups. Why? Check the source code and we can see its explanation:
Server boot requires an EventLoopGroup to indicate that the server itself is bound to the socket that the local port is listening for, and a second group to handle client connections.
Bootstrap. Group (EventLoopGroup group) ¶ As in the first line of ServerBootstrap.group(EventLoopGroup parentGroup, EventLoopGroup childGroup).
ByetBuf
The basic unit of network transport is bytes, and Java’s ByetBuffer API can be used for byte manipulation. But ByteBuf has some limitations:
-
Its length is fixed. Once the allocation is completed, the capacity cannot be dynamically expanded and contracted. When the POJO object to be encoded is larger than the capacity of ByteBuffer, index out-of-bounds exceptions will occur.
-
ByteBuffer has only one pointer position that identifies the bit-control. Flip () and rewind() need to be manually called when reading and writing, which easily leads to program processing failure.
-
API functions are limited, and some advanced and practical features need to be realized by user programming.
For this, Netty built on top of ByteBuffer to implement a new and powerful ByteBuf API with the following benefits:
- Can be extended by user-defined buffer types.
- Zero copy with built-in compound buffer.
- Capacity grows.
- Read and write use different Pointers.
- Method chain calls are supported.
- Support reference counting.
- Pooling is supported.
Usage mode of ByteBuf
Heap buffer
The heap-buffer mode is also known as the backing Array mode. To store the data in the JVM’s heap space by storing the data in arrays. The following is an example:
public static void heapBuffer(a) {
// Create a Java heap buffer
ByteBuf heapBuf = Unpooled.buffer();
if (heapBuf.hasArray()) { // Check if there is a supporting array
byte[] array = heapBuf.array(); // Get the support array reference
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); // Computes the first byte offset
int length = heapBuf.readableBytes(); // Get the number of readable bytes
handleArray(array, offset, length); // Call your own method}}Copy the code
Direct buffer
The direct buffer is direct memory allocated out of the heap and does not occupy the capacity of the heap. It is suitable for socket transmission, avoiding the process of copying data from internal buffer to direct buffer, and has good performance. The main disadvantages are that they are expensive to allocate and release compared to heap-based buffers, and because the data is not on the Java heap, you need to copy it again before processing.
public static void directBuffer(a) {
ByteBuf directBuf = Unpooled.directBuffer();
if(! directBuf.hasArray()) {// If it is not the heap buffer
int length = directBuf.readableBytes(); // Get the number of readable bytes
byte[] array = new byte[length]; // Allocate a new array to hold the data
directBuf.getBytes(directBuf.readerIndex(), array); // Copy the data to the new array
handleArray(array, 0, length); // Call your own method}}Copy the code
Compound buffer
Compound buffers are netty specific buffers. Essentially similar to providing a composite view of one or more BytebuFs, different types of BytebuFs can be added and removed as needed. The composite buffer does not support access to its supporting array. Therefore, if you want to access, you need to copy the content to the heap memory before accessing it.
public static void byteBufComposite(a) {
// Composite buffer, just provide a view
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = Unpooled.buffer();
ByteBuf bodyBuf = Unpooled.directBuffer();
messageBuf.addComponents(headerBuf, bodyBuf); // Append the ByteBuf instance to the CompositeByteBuf
messageBuf.removeComponent(0); // Remove the buffer at index position 0
for (ByteBuf buf : messageBuf) { // Iterate over the bufferSystem.out.println(buf.toString()); }}Copy the code
Byte-level operation
Random access indexes: ByteBuf’s indexes start at 0.
public static void byteBufRelativeAccess(a) {
ByteBuf buffer = Unpooled.buffer();
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);// Leave readerIndex unchanged
System.out.println((char) b); }}Copy the code
Methods that require only one index parameter do not change readIndex and writeIndex, although they can be changed manually by calling readerIndex(index) and writeIndex(index).
Discardable bytes: DiscardReadBytes are the area between [0, readerIndex]. The discardReadBytes() method can be called to discard bytes that have been read. The discardReadBytes() method moves the CONTENT of the readable byte region. Multiple data replication costs may affect performance
Readable bytes: The readable byte area is the area between [readerIndex, writerIndex]. Any operation of readxxx() and skipxxx() will change the readerIndex index.
Writable section: The writable section area is the area between [writerIndex, capacity]. Any operation with writexxx() will change the value of writerIndex.
Index management:
- MarkReaderIndex (), markWriterIndex() — marks the current position of the stream.
- ResetReaderIndex (), resetWriterIndex() — resets the stream to the marker position.
- ReadIndex (index), writeIndex(int) – moves the read/writeIndex to the specified position.
- Clear () — Sets readerIndex and writeerIndex to 0, but the contents of memory are not cleared.
Find operation: The simplest way to find a value specified by ByteBuf is to use indexOf(). More complex methods can be used to find ByteBufProcessor. For example, int index = buffer.foreachbyte (byteprocessor.find_cr); .
Derived buffer: Derived buffer provides an accessible view for ByteBuf. A view provides only one access and does not do any copying. If you modify the contents of the new ByteBuf instance, the corresponding source instance will also be modified. If you need to copy a true copy of an existing buffer, use copy() or copy(int, int).
Read/write operations: There are two types of read/write operations:
-
Get () and set() operations — start with the given index and leave the index unchanged.
-
Read () and write() operations — start with a given index and access the index based on the number of bytes already accessed.
ByteBufHolder interface
ByteBufHolder is an advanced feature of Netty that provides support for buffer pooling. The ByteBufHolder interface can be subclassed to add data fields as needed. Can be used to customize buffer type extension fields.
ByetBuf distribution
To each according to his need
Netty implements pooling (ByteBuf) through the ByteBufAllocator interface. A reference to ByteBufAllocator can be obtained through Channel or ChannelHandler’s ChannelHandlerContext.
Netty provides two implementations of Byte locator: PooledByte Locator (pooled) and unpolledByte Byte Locator (non-pooled). PooledByteBufAllocator is used by netty by default, but can also be changed through ChannelConfig.
Unpooled buffer
In the absence of a ByteBufAllocator reference, we can use netty’s Unpooled utility class to create an Unpooled ByteBufAllocator.
ByteBufUtil class
The ByteBufUtil class provides static helper methods for manipulating ByteBuf. The hexdump() method is greater than the ByteBuf content in a hexadecimal representation. The equals(ByteBuf, ByteBuf) method is used to determine whether two instances of ByteBuf are equal.
Reference counting
Netty introduced reference counting technology for ButeBuf and ButeBufHolder by implementing the ReferenceCounted interface. When the count is zero, the system reclaims the buffer, which reduces the overhead of memory allocation.