- write() now accepts a ChannelPromise and returns ChannelFuture as most
users expected. It makes the user's life much easier because it is
now much easier to get notified when a specific message has been
written.
- flush() does not create a ChannelPromise nor returns ChannelFuture.
It is now similar to what read() looks like.
I must admit MesageList was pain in the ass. Instead of forcing a
handler always loop over the list of messages, this commit splits
messageReceived(ctx, list) into two event handlers:
- messageReceived(ctx, msg)
- mmessageReceivedLast(ctx)
When Netty reads one or more messages, messageReceived(ctx, msg) event
is triggered for each message. Once the current read operation is
finished, messageReceivedLast() is triggered to tell the handler that
the last messageReceived() was the last message in the current batch.
Similarly, for outbound, write(ctx, list) has been split into two:
- write(ctx, msg)
- flush(ctx, promise)
Instead of writing a list of message with a promise, a user is now
supposed to call write(msg) multiple times and then call flush() to
actually flush the buffered messages.
Please note that write() doesn't have a promise with it. You must call
flush() to get notified on completion. (or you can use writeAndFlush())
Other changes:
- Because MessageList is completely hidden, codec framework uses
List<Object> instead of MessageList as an output parameter.
The API changes made so far turned out to increase the memory footprint
and consumption while our intention was actually decreasing them.
Memory consumption issue:
When there are many connections which does not exchange data frequently,
the old Netty 4 API spent a lot more memory than 3 because it always
allocates per-handler buffer for each connection unless otherwise
explicitly stated by a user. In a usual real world load, a client
doesn't always send requests without pausing, so the idea of having a
buffer whose life cycle if bound to the life cycle of a connection
didn't work as expected.
Memory footprint issue:
The old Netty 4 API decreased overall memory footprint by a great deal
in many cases. It was mainly because the old Netty 4 API did not
allocate a new buffer and event object for each read. Instead, it
created a new buffer for each handler in a pipeline. This works pretty
well as long as the number of handlers in a pipeline is only a few.
However, for a highly modular application with many handlers which
handles connections which lasts for relatively short period, it actually
makes the memory footprint issue much worse.
Changes:
All in all, this is about retaining all the good changes we made in 4 so
far such as better thread model and going back to the way how we dealt
with message events in 3.
To fix the memory consumption/footprint issue mentioned above, we made a
hard decision to break the backward compatibility again with the
following changes:
- Remove MessageBuf
- Merge Buf into ByteBuf
- Merge ChannelInboundByte/MessageHandler and ChannelStateHandler into ChannelInboundHandler
- Similar changes were made to the adapter classes
- Merge ChannelOutboundByte/MessageHandler and ChannelOperationHandler into ChannelOutboundHandler
- Similar changes were made to the adapter classes
- Introduce MessageList which is similar to `MessageEvent` in Netty 3
- Replace inboundBufferUpdated(ctx) with messageReceived(ctx, MessageList)
- Replace flush(ctx, promise) with write(ctx, MessageList, promise)
- Remove ByteToByteEncoder/Decoder/Codec
- Replaced by MessageToByteEncoder<ByteBuf>, ByteToMessageDecoder<ByteBuf>, and ByteMessageCodec<ByteBuf>
- Merge EmbeddedByteChannel and EmbeddedMessageChannel into EmbeddedChannel
- Add SimpleChannelInboundHandler which is sometimes more useful than
ChannelInboundHandlerAdapter
- Bring back Channel.isWritable() from Netty 3
- Add ChannelInboundHandler.channelWritabilityChanges() event
- Add RecvByteBufAllocator configuration property
- Similar to ReceiveBufferSizePredictor in Netty 3
- Some existing configuration properties such as
DatagramChannelConfig.receivePacketSize is gone now.
- Remove suspend/resumeIntermediaryDeallocation() in ByteBuf
This change would have been impossible without @normanmaurer's help. He
fixed, ported, and improved many parts of the changes.
This pull request introduces a new operation called read() that replaces the existing inbound traffic control method. EventLoop now performs socket reads only when the read() operation has been issued. Once the requested read() operation is actually performed, EventLoop triggers an inboundBufferSuspended event that tells the handlers that the requested read() operation has been performed and the inbound traffic has been suspended again. A handler can decide to continue reading or not.
Unlike other outbound operations, read() does not use ChannelFuture at all to avoid GC cost. If there's a good reason to create a new future per read at the GC cost, I'll change this.
This pull request consequently removes the readable property in ChannelHandlerContext, which means how the traffic control works changed significantly.
This pull request also adds a new configuration property ChannelOption.AUTO_READ whose default value is true. If true, Netty will call ctx.read() for you. If you need a close control over when read() is called, you can set it to false.
Another interesting fact is that non-terminal handlers do not really need to call read() at all. Only the last inbound handler will have to call it, and that's just enough. Actually, you don't even need to call it at the last handler in most cases because of the ChannelOption.AUTO_READ mentioned above.
There's no serious backward compatibility issue. If the compiler complains your handler does not implement the read() method, add the following:
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
Note that this pull request certainly makes bounded inbound buffer support very easy, but itself does not add the bounded inbound buffer support.
This commit introduces a new API for ByteBuf allocation which fixes
issue #643 along with refactoring of ByteBuf for simplicity and better
performance. (see #62)
A user can configure the ByteBufAllocator of a Channel via
ChannelOption.ALLOCATOR or ChannelConfig.get/setAllocator(). The
default allocator is currently UnpooledByteBufAllocator.HEAP_BY_DEFAULT.
To allocate a buffer, do not use Unpooled anymore. do the following:
ctx.alloc().buffer(...); // allocator chooses the buffer type.
ctx.alloc().heapBuffer(...);
ctx.alloc().directBuffer(...);
To deallocate a buffer, use the unsafe free() operation:
((UnsafeByteBuf) buf).free();
The following is the list of the relevant changes:
- Add ChannelInboundHandler.freeInboundBuffer() and
ChannelOutboundHandler.freeOutboundBuffer() to let a user free the
buffer he or she allocated. ChannelHandler adapter classes implement
is already, so most users won't need to call free() by themselves.
freeIn/OutboundBuffer() methods are invoked when a Channel is closed
and deregistered.
- All ByteBuf by contract must implement UnsafeByteBuf. To access an
unsafe operation: ((UnsafeByteBuf) buf).internalNioBuffer()
- Replace WrappedByteBuf and ByteBuf.Unsafe with UnsafeByteBuf to
simplify overall class hierarchy and to avoid unnecesary instantiation
of Unsafe instances on an unsafe operation.
- Remove buffer reference counting which is confusing
- Instantiate SwappedByteBuf lazily to avoid instantiation cost
- Rename ChannelFutureFactory to ChannelPropertyAccess and move common
methods between Channel and ChannelHandlerContext there. Also made it
package-private to hide it from a user.
- Remove unused unsafe operations such as newBuffer()
- Add DetectionUtil.canFreeDirectBuffer() so that an allocator decides
which buffer type to use safely
- Add Channel.metadata() and remove Channel.bufferType()
- DefaultPipeline automatically redirects disconnect() request to
close() if the channel has no disconnect operation
- Remove unnecessary disconnect() implementations
- ChannelBuffer gives a perception that it's a buffer of a
channel, but channel's buffer is now a byte buffer or a message
buffer. Therefore letting it be as is is going to be confusing.
- Extracted some handler methods from ChannelInboundHandler into
ChannelStateHandler
- Extracted some handler methods from ChannelOutboundHandler into
ChannelOperationHandler
- Moved exceptionCaught and userEventTriggered are now in
ChannelHandler
- Channel(Inbound|Outbound)HandlerContext is merged into
ChannelHandlerContext
- ChannelHandlerContext adds direct access methods for inboud and
outbound buffers
- The use of ChannelBufferHolder is minimal now.
- Before: inbound().byteBuffer()
- After: inboundByteBuffer()
- Simpler and better performance
- Bypass buffer types were removed because it just does not work at all
with the thread model.
- All handlers that uses a bypass buffer are broken. Will fix soon.
- CombinedHandlerAdapter does not make sense anymore either because
there are four handler interfaces to consider and often the two
handlers will implement the same handler interface such as
ChannelStateHandler. Thinking of better ways to provide this feature
- DefaultChannelPipeline uses this information to reject invalid buffer
access in inbound(Message|Byte)Buffer. Otherwise, a user can access
a message buffer when the channel is stream-oriented.
- Because ChannelType cannot be both STREAM and MESSAGE, catch-all
buffer has been removed to avoid confusion and unexpected behavior
(it's already causing headache.)
- As a result, codec embedder needs rework.
- Add EventExecutor and make EventLoop extend it
- Add SingleThreadEventExecutor and MultithreadEventExecutor
- Add EventExecutor's default implementation
- Fixed an API design problem where there is no way to get non-bypass
buffer of desired type
- Previous API did not support the pipeline which contains multiple
MessageToStreamEncoders because there was no way to find the closest
outbound byte buffer. Now you always get the correct buffer even if
the handler that provides the buffer is placed distantly.
For example:
Channel -> MsgAEncoder -> MsgBEncoder -> MsgCEncoder
Msg(A|B|C)Encoder will all have access to the channel's outbound
byte buffer. Previously, it was simply impossible.
- Improved ChannelBufferHolder.toString()
- AbstractChannel keeps the expected number of written bytes so that
the ChannelFuture of a flush() operation is notified on right timing.
- Added ChannelBufferHolder.size() to make this possible
- Added AbstractChannel.isCompatible() so that only compatible EventLoop
is accepted by a channel on registration
- Added ChannelOption to make channel options type-safe
- Moved writeSpinCount property to ChannelConfig and removed Nio*Config
- Miscellaneous cleanup
introducing
ChannelOption
- Optimized AbstractChannelBuffer.discardReadBytes()
- Split ChannelHandlerInvoker into ChannelInboundInvoker and
ChannelOutboundInvoker
- Channel implements ChannelOutboundInvoker
- ChannelOutboundInvoker.nextOut() is now out()
- ChannelOutboundHandlerContext.out() is now prevOut()
- Added the outbound operations without future
parameter to ChannelOutboundInvoker for user convenience
- All async operations which requires a ChannelFuture as a parameter
now returns ChannelFuture for user convenience
- Added ChannelFutureFactory.newVoidFuture() to allow a user specify
a dummy future that is of no use
- I'm unsure if it is actually a good idea to introduce it. It might
go away later.
- Made the contract of AbstractChannel.doXXX() much simpler and moved
all common code up to AbstractChannel.DefaultUnsafe
- Added Channel.isOpen()
- Fixed a bug where MultithreadEventLoop always shut down its child
event loops on construction
- Maybe more changes I don't remember :-)
- Remove large portion of code thanks to the new API
- SocketChannel implementations are instantiated without factories
- Retrofit the existing code with the new API
- Remove the classes and properties that are not necessary anymore
- Remove SingleThreadEventLoop.newRegistrationTask() and let
Channel.Unsafe handle registration by itself
- Channel.Unsafe.localAddress() and remoteAddress()
- JdkChannel is replaced by Channel.Unsafe.