Introduction
We know that there are two data transmission methods, namely character stream and byte stream. Character stream means that the object of transmission is a character string. The format has been set, and the sender and receiver read according to a specific format. That's it, and byte stream refers to the transmission of data as the most primitive binary bytes.
Today I will introduce you to the stream-based data transmission in netty.
package and byte
Students who are familiar with the TCP/IP protocol should know that in TCP/IP, because the underlying protocol has the maximum number of data packets supported, for big data transmission, the data needs to be split and packaged, and these are unpacked. Send the assembled packages, and finally combine these packages at the receiver. There is a fixed structure in each package, so the receiver can clearly know how many packages should be combined as the final result.
So for netty, ByteBuf is transmitted in the channel, and in fact, the lowest level is the byte array. For this kind of byte array, the receiver does not know how many bytes should be combined to synthesize the original message, so the received bytes need to be combined at the receiving end to generate the final data.
So how should the byte data stream in netty be combined? We next look at two combination methods.
Manual combination
The basic idea of this combination is to construct a ByteBuf of the target size, and then write the received byte into the ByteBuf by calling the writeBytes method of ByteBuf. Finally, the corresponding data is read from ByteBuf.
For example, we want to send an int number from the server to the client. Generally speaking, int is 32bits, and then a byte is 8bits, then an int needs 4 bytes.
On the server side, you can create a byte array, which contains 4 elements. Send the 4-element byte to the client, then how should the client handle it?
First we need to create a clientHander, this handler should inherit ChannelInboundHandlerAdapter, and initialize a byteBuf containing 4 bytes when its handler is added to ChannelPipeline.
When the handler is added, a handlerAdded event will be triggered, so we can write:
private ByteBuf buf;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
//创建一个4个byte的缓冲器
buf = ctx.alloc().buffer(4);
}
In the above example, we allocated a 4-byte buffer from ctx and assigned it to the private variable buf in the handler.
When the handler is executed and deleted from the ChannelPipeline, the handlerRemoved event will be triggered. In this event, we can clean up the allocated Bytebuf. Generally speaking, we can call its release method, as shown below:
public void handlerRemoved(ChannelHandlerContext ctx) {
buf.release(); // 释放buf
buf = null;
}
Then the most critical step is to read the byte from the channel and put it in the 4-byte byteBuf. In the previous article, we mentioned that you can handle the logic of message reading in the channelRead method.
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
buf.writeBytes(m); // 写入一个byte
m.release();
if (buf.readableBytes() >= 4) { // 已经凑够4个byte,将4个byte组合称为一个int
long result = buf.readUnsignedInt();
ctx.close();
}
}
Each time the channelRead method is triggered, a byte of the read byte will be written into buf by calling the writeBytes method. When the readable byte of buf is greater than or equal to 4, it means that the 4 bytes are full and can be operated on.
Here we combine 4 bytes into an unsignedInt, and use the readUnsignedInt method to read from buf and the combination is called an int number.
Although the above example can solve the 4-byte byte problem, if the data structure is more responsible, the above method will not be effective, and too many data combination problems need to be considered. Next we look at another way.
Byte conversion class
Netty provides a conversion class of ByteToMessageDecoder, which can easily convert Byte to other types.
We only need to rewrite the decode method to realize the conversion of ByteBuf:
public class SquareDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
out.add(in.readBytes(in.readableBytes()));
}
}
The above example converts byte from input to output. Of course, you can also perform format conversion in the above method, as shown below:
public class TimeDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
out.add(in.readBytes(4));
}
}
The above example will first determine whether there are 4 bytes in in, and if there are 4 bytes, read them out and put them in out. Then some classmates will ask, isn’t the input from byte to byte? Why can we read 4 bytes at a time here? This is because ByteToMessageDecoder has a built-in cache device, so the in here is actually a cache set.
ReplayingDecoder
Netty also provides a simpler conversion ReplayingDecoder. If you use ReplayingDecoder to re-write the above logic, it looks like this:
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}
Only one line of code is required.
In fact, ReplayingDecoder is a subclass of ByteToMessageDecoder, which is the result of enriching some functions on ByteToMessageDecoder.
The difference between the two is that ByteToMessageDecoder also needs to call readableBytes to determine whether there are enough bytes that can be read. ReplayingDecoder can be used to read directly. It assumes that all bytes have been successfully accepted.
For example, the following code uses ByteToMessageDecoder:
public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
if (buf.readableBytes() < 4) {
return;
}
buf.markReaderIndex();
int length = buf.readInt();
if (buf.readableBytes() < length) {
buf.resetReaderIndex();
return;
}
out.add(buf.readBytes(length));
}
}
The above example assumes that the head of the byte is an int-sized array, which represents the length of the byte array. The int value needs to be read first, and then the corresponding byte data is read according to the int value.
It is equivalent to the following code:
public class IntegerHeaderFrameDecoder
extends ReplayingDecoder<Void> {
protected void decode(ChannelHandlerContext ctx,
ByteBuf buf, List<Object> out) throws Exception {
out.add(buf.readBytes(buf.readInt()));
}
}
The above code lacks the steps of judgment.
So how is this achieved?
In fact, ReplayingDecoder will pass a ByteBuf that will throw an Error. When the number of bytes read by ByteBuf does not meet the requirements, an exception will be thrown. When the ReplayingDecoder catches this exception, it will reset the buffer's readerIndex to the original state. , And then wait for the subsequent data to come in, and then call the decode method again.
Therefore, the efficiency of ReplayingDecoder will be relatively low. In order to solve this problem, netty provides the checkpoint() method. This is a save point. When an error is reported, you can not return to the original state, but return to the state saved when checkpoint() is called, which can reduce unnecessary waste.
Summarize
This article introduces several ways to perform stream operations and transformations in netty, I hope you will like them.
This article has been included in http://www.flydean.com/07-netty-stream-based-transport/
The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to discover!
Welcome to pay attention to my official account: "Program those things", know technology, know you better!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。