Stick/unpack
My last article was about my first experience with Netty. This article mainly explains how Netty solves TCP sticky/unpack problem.
TCP sticky/unpack is actually the most commonly used network programming. As long as you depend on TCP, you can’t get away. For example, if you make a network application, if you use sockets for network communication, you are actually operating at the transport layer (because sockets are the intermediate software abstraction layer that the application layer communicates with the TCP/IP protocol family, which is a set of interfaces). So you need to pay attention to TCP sticky/unpack issues; Or you do hardware communications. At present, many hardware companies like to take advantage of TCP to create the Internet of things, so this problem also needs to pay attention to.
Here’s what TCP sticky/unpack is!
TCP unpacking
Transmission control protocol (TCP) is an important transport protocol in Internet. TCP provides connection-oriented/reliable/ordered/byte stream transport services. So called byte stream because TCP is a stream protocol. A stream is an unbounded string of data. Because TCP is in the transmission layer, it does not understand the business of the upper layer (also do not need, because the upper layer is generally dependent on the lower layer rather than each other), so TCP transmission process, just according to the network situation, transmission media and other factors to find the best length of a transmission
So this begs the question: how does TCP partition and send the buffer data, since it does not know what the upper-layer business data means? Does a random partition result in a request being cut in half or more?
The answer is yes, in this case because TCP calculates the length of each byte stream according to its own policy/the current environment, and then splits the byte stream to the most appropriate length. As a result, a complete packet may be split by TCP and sent one or more times. This is the TCP unpacking behavior.
This is just the general principle, but if we go into more detail, there are several reasons why TCP unpacks packets at each network layer:
- The size of bytes written by the application write is greater than the size of the socket send buffer. In the development process of developers, the most common is code through Socket programmatically initiate links to remote hosts. When the size of bytes written to the socket buffer exceeds the maximum, it is unpacked.
- Perform TCP segmentation of MSS size. Both ends of the TCP connection are equipped with send cache and receive cache, which are used to temporarily store two-way communication data. During sending, applications can do their own work after sending data to the TCP cache, while TCP sends data at an appropriate time. However, the amount of data that TCP can fetch from the cache at a time is limited by one parameter, the Maximum Segment Size (MSS). The size of this parameter is usually set to the maximum link layer frame length sent by the localhost. MSS is set to ensure that a TCP packet segment (encapsulated in an IP datagram) plus the TCP and IP header length (typically 40 bytes) will fit into a single link layer. Note that MSS refers to the maximum length of the application layer data in the packet segment but does not include the maximum length of the TCP packet segment in the TCP header. Therefore, MSS limits the maximum length of the data field in the packet segment, resulting in packet unpacking.
- The Payload of Ethernet frames is greater than the MTU. This data fragment belongs to the link layer. The maximum length of network layer datagrams supported by different data link layer protocols varies. An Ethernet frame can carry up to 1500 bytes of data, but some data link layer protocols can carry much less than this value. The maximum amount of data that can be carried by a data link layer is called the Maxumum Transmission Unit (MTU). When the number of loaded data exceeds this value, IP data is fragmented.
If the amount of data per request is not large, you might ask, wouldn’t that avoid TCP unpacking? That’s another problem we’ll cover: TCP sticky packets.
Stick the TCP package
In fact, the amount of data we send at a time varies from business operation to business operation. It could be just to send a heartbeat, and the next second a big file transfer. So we have to deal with changing business scenarios. TCP improves transmission efficiency and is compatible with more complex environments. For example, in order to improve the success rate of network transmission (), the sender usually collects enough data before sending a TCP segment. So if you have multiple requests and the amount of data is very small, then the data sent by TCP may contain multiple requests, which causes multiple packets to stick together. Similarly, because the receiver does not receive packets from the buffer in time, multiple packets are received (the client sends a piece of data, but the server only receives a small part of the data, and the server takes the data left over from the buffer next time, resulting in sticky packets). This is TCP sticky packet behavior.
We have covered the general principles of TCP unpacking/sticky packets. So let’s talk about how we can solve this.
Unpack/sticky pack solution
On the TCP side, the sender is really cool. It can freely hair, it hair long hair short and no one cares about it. But the pain is on the receiving end, because it doesn’t know where the package ends up. So, the core of the solution is, at the top of the transport layer, we have to make a deal, we have to sign a deal, we all go along with this, we restrict the sender’s behavior and we give the receiver a sense of security, and then everyone is happy.
General solutions to this problem are as follows:
- The transmitted message is of fixed length. Once the length is known, the receiver can fetch the specified length each time and be done
- Add carriage return to the end of the packet as a split condition (end condition)
- The message is divided into header and body. The header contains the total length of the message. (Generally, the first field in the header can be designed to represent the total length of the message)
- More complex application layer protocols
How does Netty respond?
In fact, the problem is found, so only the means to solve the problem. Obviously, we can analyze the data stream as soon as the data is passed in through the Socket, and then encapsulate the corresponding datagrams. As mentioned in the previous article, Netty supports an extensible event-driven model, simply by handling each solution. Netty codecs such as LineBasedFrameDecoder and StringDecoder are read with a newline symbol \n or \r\n as an end symbol.
So let’s write the demo.
DEMO[using newline as end bit]
The core idea of the demo is simple: we want the request data for each request to be clearly separated, that is, both the server and the client can distinguish the individual request from the data and output back.
The service side
The first is the server startup code, TimeServer
TimeServer.java
public class TimeServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap s = new ServerBootstrap();
s.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
ChannelFuture f = s.bind(port).sync();
f.channel().closeFuture().sync();
}catch (Exception e) {
}finally{ bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }}public static void main(String[] args) throws Exception {
new TimeServer().bind(8080); }}Copy the code
TimeServer is similar to the code in the previous article, except that its childHandler initializes the content ChildChannelHandler.
ChildChannelHandler.java
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel ch) throws Exception {
/ / 1
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
/ / 2
ch.pipeline().addLast(new StringDecoder());
/ / 3
ch.pipeline().addLast(newTimeServerHandler()); }}Copy the code
There are three steps:
- Based on behavior codec
LineBasedFrameDecoder
And set the maximum line bytes to 1024 - use
StringDecoder
Decodes the previous node in the chain into a string - Business logic is processed according to the codec results of the former nodes in the chain
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// Since StringDecoder is decoded to be called string, it can be converted directly here
String body = (String) msg;
System.out.println("the time server receive order " + body + "; the counter is : " + ++counter);
/ / determine
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
// Get the event
currentTime = currentTime + System.getProperty("line.separator");
// Initialize ByteBuf based on size and flush to cache
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); }}Copy the code
The client
The client code is similar to the server code, so take a look at timeclient.java
public class TimeClient {
public void connect(int port, String host) {
// Create a NioEventLoopGroup thread group for the client to process I/O reads and writes
EventLoopGroup group = new NioEventLoopGroup();
try {
// Create a client-assisted startup class
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(newTimeClientHandler()); }});// Call connect to initiate an asynchronous request, call the synchronous method and wait for success
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
}catch (Exception e ) {
}finally{ group.shutdownGracefully(); }}public static void main(String[] args) {
new TimeClient().connect(8080."127.0.0.1"); }}Copy the code
Also, because the server is returning data, the client also has to do data parsing to prevent sticky/unpack. The client adds LineBasedFrameDecoder/LineBasedFrameDecoder/TimeClientHandler directly via an anonymous function.
So let’s just look at what’s changed in the TimeClientHandler.
TimeClientHandler.java
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(title04.TimeClientHandler.class.getName());
private int counter;
private byte[] req;
public TimeClientHandler(a) {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i=0; i<100;i++) {
message= Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
// When a TCP link is established between the client and server, the netty NIO thread calls the channelActive method
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("now is : " + body + "; the counter is : " + ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warning("unexpected exception from downstream: "+ cause.getMessage()); ctx.close(); }}Copy the code
TimeClientHandler in his channelActive method writes code that loops 100 times, so the client sends 100 requests, and the server should be able to split 100 requests.
So, one might ask, if I’m not using a newline as the end bit of a datagram, isn’t that not possible? If Netty can use newlines to split it, it must be able to abstract the logic to implement custom symbols. Let’s talk about the corresponding implementation class!
Custom delimiter as end flag bit
Custom delimiters are designed to avoid development scenarios that do not use newlines as flag bits. Netty by implementing DelimiterBasedFrameDecoder to completion with a delimiter as a mark at the end of decoding.
The service side
Let’s go straight to the demo. The server still has two parts: the launcher and the processor launcher echoServer.java
public class EchoServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap s = new ServerBootstrap();
s.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
// Add logs
.handler(new LoggingHandler())
/ / processing
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
// Use "$_" as the delimiter and then split
ByteBuf delimitoer = Unpooled.copiedBuffer("The $_".getBytes());
// Cuts the start and end of the flow according to the specified delimiter
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimitoer));
// String decode
ch.pipeline().addLast(new StringDecoder());
// Pass the character to the server processor
ch.pipeline().addLast(newEchoServerHandler()); }}); ChannelFuture f = s.bind(port).sync(); f.channel().closeFuture().sync(); }catch (Exception e) {
}finally{ bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }}public static void main(String[] args) throws Exception {
new EchoServer().bind(8080); }}Copy the code
Before the above code and the example of the difference is not big, mainly in the initialization ChannelHandler add more DelimiterBasedFrameDecoder on to solve the problem of glue bag/unpacking. The server is pretty much done with the data, and the server processor should have an easier time with EchoServerHandler.java.
EchoServerHandler.java
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
int count = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("this is "+ ++count + " times receive client:[" + body + "]" );
body += "The $_";
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}Copy the code
The client
Changes in the client and the server is consistent, mainly DelimiterBasedFrameDecoder added.
public class EchoClient {
public void connect(int port, String host) {
// Create a NioEventLoopGroup thread group for the client to process I/O reads and writes
EventLoopGroup group = new NioEventLoopGroup();
try {
// Create a client-assisted startup class
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimitoer = Unpooled.copiedBuffer("The $_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimitoer));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(newEchoClientHandler()); }});// Call connect to initiate an asynchronous request, call the synchronous method and wait for success
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
}catch (Exception e ) {
}finally{ group.shutdownGracefully(); }}public static void main(String[] args) {
new EchoClient().connect(8080."127.0.0.1"); }}Copy the code
The client processor needs to output the result to see if there are any problems with the separator
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private int counter;
static final String ECHO_REQ = "Hi, Lilinfeng. welcome to Netty.$_";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i=0; i <100;i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("this is "+ ++counter + " times receive server :[" + msg + "]");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}Copy the code
DEMO[Fixed length Codec as End flag bit]
Fixed-length codec is equivalent to the third solution to the sticky/unpack problem of TCP. The server side is still similar to the previous example, just add the corresponding codec.
The service side
EchoServer
public class EchoServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap s = new ServerBootstrap();
s.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
// Add a fixed-length codec
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(newEchoServerHandler()); }}); ChannelFuture f = s.bind(port).sync(); f.channel().closeFuture().sync(); }catch (Exception e) {
}finally{ bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }}public static void main(String[] args) throws Exception {
new EchoServer().bind(8080); }}Copy the code
The EchoServerHandler handler is relatively simple and can be output directly.
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
int count = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("receive client :[ " + msg + "]");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}Copy the code
The client
As for the client side also operates, there is not much difference between the two, so I won’t paste the code here.
conclusion
We recorded the following throughout the article
TCP Paste or unpack
What is theTCP Paste or unpack
For whatTCP Paste or unpack
What is the solution toNetty
forTCP Paste or unpack
The solution given
At the same time, we found that in Netty, if we had a new alternative solution to the problem, we made very little change at the code level. This also demonstrates the powerful extensibility of Netty transactional drivers.
Finished!