我们都知道TCP是基于字节流的传输协议。那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句号什么地方有分号这个对于TCP底层来说并不清楚。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段,之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。所以对于这个数据拆分成大包小包的问题就是我们今天要讲的粘包和拆包的问题。
粘包和拆包这两个概念估计大家还不清楚,通过下面这张图我们来分析一下:
假设客户端分别发送两个数据包D1,D2个服务端,但是发送过程中数据是何种形式进行传播这个并不清楚,分别有下列4种情况:
我们知道在TCP协议中,应用数据分割成TCP认为最适合发送的数据块,这部分是通过“MSS”(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS规定了TCP传往另一端的最大数据块的长度。这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。
tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
发生粘包拆包的原因主要有以下这些:
我们知道tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:
当然应用层还有更多复杂的方式可以解决这个问题,这个就属于网络层的问题了,我们还是用java提供的方式来解决这个问题。我们先看一个例子看看粘包是如何发生的。
服务端:
public class HelloWordServer {
private int port;
public HelloWordServer(int port) {
this.port = port;
}
public void start(){
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer());
try {
ChannelFuture future = server.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
HelloWordServer server = new HelloWordServer(7788);
server.start();
}
}
服务端Initializer:
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 自己的逻辑Handler
pipeline.addLast("handler", new HelloWordServerHandler());
}
}
服务端handler:
public class HelloWordServerHandler extends ChannelInboundHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String)msg;
System.out.println("server receive order : " + body + ";the counter is: " + ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
客户端:
public class HelloWorldClient {
private int port;
private String address;
public HelloWorldClient(int port,String address) {
this.port = port;
this.address = address;
}
public void start(){
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ClientChannelInitializer());
try {
ChannelFuture future = bootstrap.connect(address,port).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
HelloWorldClient client = new HelloWorldClient(7788,"127.0.0.1");
client.start();
}
}
客户端Initializer:
public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 客户端的逻辑
pipeline.addLast("handler", new HelloWorldClientHandler());
}
}
客户端handler:
public class HelloWorldClientHandler extends ChannelInboundHandlerAdapter {
private byte[] req;
private int counter;
public BaseClientHandler() {
req = ("Unless required by applicable law or agreed to in writing, software\n" +
" distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
" See the License for the specific language governing permissions and\n" +
" limitations under the License.This connector uses the BIO implementation that requires the JSSE\n" +
" style configuration. When using the APR/native implementation, the\n" +
" penSSL style configuration is required as described in the APR/native\n" +
" documentation.An Engine represents the entry point (within Catalina) that processes\n" +
" every request. The Engine implementation for Tomcat stand alone\n" +
" analyzes the HTTP headers included with the request, and passes them\n" +
" on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software\n" +
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
"# See the License for the specific language governing permissions and\n" +
"# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log\n" +
"# each component that extends LifecycleBase changing state:\n" +
"#org.apache.catalina.util.LifecycleBase.level = FINE"
).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message;
//将上面的所有字符串作为一个消息体发送出去
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String buf = (String)msg;
System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
运行客户端和服务端我们能看到:
我们看到这个长长的字符串被截成了2段发送,这就是发生了拆包的现象。同样粘包我们也很容易去模拟,我们把BaseClientHandler中的channelActive方法里面的:
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
这几行代码是把我们上面的一长串字符转成的byte数组写进流里发送出去,那么我们可以在这里把上面发送消息的这几行循环几遍这样发送的内容增多了就有可能在拆包的时候把上一条消息的一部分分配到下一条消息里面了,修改如下:
for (int i = 0; i < 3; i++) { message = Unpooled.buffer(req.length); message.writeBytes(req); ctx.writeAndFlush(message); }
改完之后我们再运行一下,输出太长不好截图,我们在输出结果中能看到循环3次之后的消息服务端收到的就不是之前的完整的一条了,而是被拆分了4次发送。
对于上面出现的粘包和拆包的问题,Netty已有考虑,并且有实施的方案:LineBasedFrameDecoder。
我们重新改写一下ServerChannelInitializer:
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(2048));
// 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// 自己的逻辑Handler
pipeline.addLast("handler", new BaseServerHandler());
}
}
新增:pipeline.addLast(new LineBasedFrameDecoder(2048))。同时,我们还得对上面发送的消息进行改造BaseClientHandler:
public class BaseClientHandler extends ChannelInboundHandlerAdapter {
private byte[] req;
private int counter;
req = ("Unless required by applicable dfslaw or agreed to in writing, software" +
" distributed under the License is distributed on an \"AS IS\" BASIS," +
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
" See the License for the specific language governing permissions and" +
" limitations under the License.This connector uses the BIO implementation that requires the JSSE" +
" style configuration. When using the APR/native implementation, the" +
" penSSL style configuration is required as described in the APR/native" +
" documentation.An Engine represents the entry point (within Catalina) that processes" +
" every request. The Engine implementation for Tomcat stand alone" +
" analyzes the HTTP headers included with the request, and passes them" +
" on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software" +
"# distributed under the License is distributed on an \"AS IS\" BASIS," +
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
"# See the License for the specific language governing permissions and" +
"# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log" +
"# each component that extends LifecycleBase changing state:" +
"#org.apache.catalina.util.LifecycleBase.level = FINE\n"
).getBytes();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message;
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String buf = (String)msg;
System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
去掉所有的”\n”,只保留字符串末尾的这一个。原因稍后再说。channelActive方法中我们不必再用循环多次发送消息了,只发送一次就好(第一个例子中发送一次的时候是发生了拆包的),然后我们再次运行,大家会看到这么长一串字符只发送了一串就发送完毕。程序输出我就不截图了。下面来解释一下LineBasedFrameDecoder。
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf 中的可读字节,判断看是否有”\n” 或者” \r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器。支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。这个对于我们确定消息最大长度的应用场景还是很有帮助。
对于上面的判断看是否有”\n” 或者” \r\n”以此作为结束的标志我们可能回想,要是没有”\n” 或者” \r\n”那还有什么别的方式可以判断消息是否结束呢。别担心,Netty对于此已经有考虑,还有别的解码器可以帮助我们解决问题,下节我们继续学习。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/a953713428/article/details/67100345
内容来源于网络,如有侵权,请联系作者删除!