JAVA Socket编程学习10--解决TCP粘包分包问题

来源:互联网 发布:gre阅读 知乎 编辑:程序博客网 时间:2024/06/17 07:46
前言:
        根据我的第七篇文章http://blog.csdn.net/m0_37739193/article/details/78484577编写了NIO的Socket服务端代码后,接受UDP的数据正常,但是接收的TCP数据却出现了粘包分包/拆包/半包问题,查阅网上资料知道已经有开源的Netty提供了多种支持TCP粘包/拆包的解码器,用来满足用户的不同诉求。

        可是我已经根据Java原生的NIO写完了整个代码框架,要是换成Netty的话就整个框架代码就都得换了,由于嫌麻烦,我就开动自己的大脑,能不能自己通过代码来实现呢?在自己的努力之下好像是可以的哈,基本能把收来的数据处理成自己想要的。


首先模拟一个TCP客户端:

import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.PrintWriter;import java.net.InetSocketAddress;import java.net.Socket;import java.net.UnknownHostException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class HeavyTCPClient1 {private static ExecutorService tp = Executors.newCachedThreadPool();private static final int sleep_time = 1000*1000*1000;public static class EchoClient implements Runnable{public void run(){Socket client = null;PrintWriter writer = null;BufferedReader reader = null;try {client = new Socket();client.connect(new InetSocketAddress("localhost", 7788));writer = new PrintWriter(client.getOutputStream(), true);//writer.print("{'probeid':0,'type':0,'dbopt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"server_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n{'probeid':");writer.print("0,'type':0,'dbopt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"server_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n{'probeid':0,'type':0,'dbopt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"server_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n{'probeid':0,'type':0,'db");//writer.print("opt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"server_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n{'probeid':0,'type':0,'dbopt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"s");//writer.print("erver_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n{'probeid':0,'type':0,'dbopt':0,'data':{ \"client_ip\": \"192.168.102.29\", \"server_ip\": \"74.125.204.113\", \"client_port\": 13698 }}\r\n");writer.flush();reader = new BufferedReader(new InputStreamReader(client.getInputStream()));System.out.println("from server: " + reader.readLine());reader.close();writer.close();client.close();} catch (UnknownHostException e){e.printStackTrace();} catch (IOException e){e.printStackTrace();} finally {try {if (writer != null)writer.close();if (reader != null)reader.close();if (client != null)client.close();} catch (IOException e){}}}}public static void main(String args[]) throws IOException {EchoClient ec = new EchoClient();//for(int i=0;i<1000;i++)for(int i=0;i<1;i++)tp.execute(ec);tp.shutdown();}}

因为我是把服务端接收到的数据再发送到Kafka中,再把Kafka中的数据消费到MySQL中去的,所以在消费者端处理数据的粘包分包/拆包/半包问题:

import java.util.Arrays;import java.util.Properties;import java.util.regex.Matcher;import java.util.regex.Pattern;import org.apache.kafka.clients.consumer.ConsumerRecord;import org.apache.kafka.clients.consumer.ConsumerRecords;import org.apache.kafka.clients.consumer.KafkaConsumer;public class KafkaConsumerToMysql {static String first = "";private static String line = "";private static int b = 0;public static void main(String[] args) throws Exception {//    long a = System.currentTimeMillis();int a = 0;        Properties props = new Properties();        props.put("bootstrap.servers", "h153:9092");        props.put("group.id", "test");    //        props.put("enable.auto.commit", "true");        props.put("enable.auto.commit", "false");        props.put("auto.commit.interval.ms", "1000");        props.put("session.timeout.ms", "30000");        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");            props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");          KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);          consumer.subscribe(Arrays.asList("tcp"));        MysqlUtils mysql = new MysqlUtils();        String pattern = "\\{('\\D+':\\d+,){3}'data':\\{(\\s\\S+\\s\\S+,)+\\s\\S+:\\s\\d+\\s\\}\\}";        Pattern p = Pattern.compile(pattern);        while (true) {            ConsumerRecords<String, String> records = consumer.poll(100);            for (ConsumerRecord<String, String> record : records){            a++;            String[] data = record.value().split("\r\n");            for(int i=0;i<data.length;i++){            if(data.length <= 1){            line = data[0];            Matcher matcher = p.matcher(line);            if(!matcher.matches()){            first = line;            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+data[0]);            continue;            }else{            first = "";            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+data[0]);            mysql.put(data[0],a);            continue;            }            }else{            if(i == data.length-1){            line = data[data.length-1];            Matcher matcher = p.matcher(line);            if(!matcher.matches()){            first = line;            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+data[i]);            continue;            }else{            first = "";            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+data[i]);            mysql.put(data[i],a);            continue;            }            }            if(i == 0){            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+first+data[i]);            Matcher matcher = p.matcher(first+data[i]);            if(!matcher.matches()){            System.out.println("该条数据舍弃");            }else{            mysql.put(first+data[i],a);            }            }else{            System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), b+"-->"+data[i]);            mysql.put(data[i],a);            }            }            }            }        }    }}
基本思路:让客户端发数据的时候在每条完整的json字符串后加结束标识符\r\n,我这里把收到的数据用split按\r\n分隔,在代码中定义一个全局变量first,用正则表达式匹配分隔好的每条数据,由于粘包分包/拆包/半包所造成的数据分隔后只有data[0]和data[data.length-1]的数据是不完整的,这里就分两种情况了,第一种是分隔后的数据只有一条,那么用正则匹配这条数据,如果能匹配则说明该条数据是我们想要的,则可插入mysql中,如果不是则说明这条数据是不完整的,将它赋给first(如果出现粘包分包/拆包/半包问题,则第一组的最后一条数据和第二组的第一条数据合起来才是一条完整的数据);第二种是分隔后的数据大于一条,那么就需要用正则来匹配第一条和最后一条数据了,如果第一条不匹配则加上上一组的最后一条数据first,如果最后一条不匹配的话则再把这不完整的数据赋给first,以此类推。这里需要注意的是当客户端第一次连接的时候发送的第一组的第一条的数据前半部分应该是完整的,就怕特殊状态下这第一次第一条的数据就是少头部的数据从半路来的数据,这样的话这组的第一条不完整的数据加上上一组的最后一条不完整的数据就构不成一条完整的我想要的数据了,所以这里我就把这条数据舍弃不插入MySQL中。


TCP粘包/拆包
TCP编程底层都有粘包和拆包机制,因为我们在C/S这种传输模型下,以TCP协议传输的时候,在网络中的byte其实就像是河水,TCP就像一个搬运工,将这流水从一端转送到另一端,这时又分两种情况:
(1)如果客户端的每次制造的水比较多,也就是我们常说的客户端给的包比较大,TCP这个搬运工就会分多次去搬运。
(2)如果客户端每次制造的水比较少的话,TCP可能会等客户端多次生产之后,把所有的水一起再运输到另一端
上述第一种情况,就是需要我们进行粘包,在另一端接收的时候,需要把多次获取的结果粘在一起,变成我们可以理解的信息,第二种情况,我们在另一端接收的时候,就必须进行拆包处理,因为每次接收的信息,可能是另一个远程端多次发送的包,被TCP粘在一起的
引自:http://blog.csdn.net/linuu/article/details/51337748


我们考虑一下这样的情况:我们编写了一个机器人控制程序,通过一个遥控器(客户端)向机器人(服务器)建立了一个长连接,并通过这个连接连续不断的从遥控器发送控制指令给机器人。由于是连续控制指令,所以指令与指令之间没有间隔(实际上您还可以想想很多类似场景,例如:开发的Online对战游戏)。
我们使用JSON格式作为指令数据的承载格式。那么发送方和接收方的数据发送-接受过程可能如下图所示。


通过上图我们看到了接收方为了接受这两条连贯的指令,一共做了三次接受,第二次接收的时候,收到了一部分message1的内容和一部分message2的内容。这里要说明几个注意事项:
# MSS:MSS属性是TCP连接双方在三次握手时所确认的每一个TCP报文段中数据字段的最大长度。注意,一是连接双方协商出来的;二是只是数据段的最大长度,不包括IP协议头和TCP协议头的最大长度。
# 半包是指接收方应用程序在接收信息时,没有接收到一个完成的信息格式块;粘包是指,接收方应用程序在接受信息时,除了接收到发送方应用程序发送的某一个完整数据信息描述外,还接受到了一下发送方应用程序发送的下一个数据信息的一部分。
# 半包和粘包是针对应用程序来说的,这个问题只会发生在TCP一些进行连续发送数据时(TCP长连接)。UDP不会出现这个问题,因为UDP都是有边界的数据报;TCP短连接也不会出现,因为发送完一个指令信息后连接就断开了,不会发送第二个指令数据。
# 半包和粘包问题产生的根本是因为TCP本质上没有“数据块”的概念,而是一连串的数据流。在应用程序层面上我们所定义的“数据块”在TCP层面上并不被协议认可。
# 半包/粘包是一个应用层问题。要解决半包/粘包问题,就是在应用程序层面建立协商一致的信息还原依据。常见的有两种方式:一是消息定长,即保证每一个完整的信息描述的长度都是一定的,这样无论TCP/IP协议如何进行分片,数据接收方都可以按照固定长度进行消息的还原。二是在完整的一块数据结束后增加协商一致的分隔符(例如增加一个回车符)。
在JAVA NIO技术框架中,半包和粘包问题我们需要自己解决,如果使用Netty框架,它其中提供了多种解码器的封装帮助我们解决半包和粘包问题。甚至针对不同的数据格式,Netty都提供了半包和粘包问题的现成解决方式,例如之前我们提到的ProtobufVarint32FrameDecoder解码器,就是专门解决Protobuf数据格式在TCP长连接传输时的半包问题的。


下面我们会介绍FixedLengthFrameDecoder、DelimiterBasedFrameDecoder、LineBasedFrameDecoder来解决半包/粘包的问题。
1.使用FixedLengthFrameDecoder解决问题

FixedLengthFrameDecoder解码处理器将TCP/IP的数据按照指定的长度进行重新拆分,如果接收到的数据不满足设置的固定长度,Netty将等待新的数据到达:serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {    @Override    protected void initChannel(NioSocketChannel ch) throws Exception {        ch.pipeline().addLast(new ByteArrayEncoder());        ch.pipeline().addLast(new FixedLengthFrameDecoder(20));        ch.pipeline().addLast(new TCPServerHandler());        ch.pipeline().addLast(new ByteArrayDecoder());    }});
Netty上层的channelRead事件方法将在Channel接收到20个字符的情况下被触发;而如果剩余的内容不到20个字符,channelRead方法将不会被触发(但注意channelReadComplete方法会触发的啦)。

2.使用LineBasedFrameDecoder解决问题
LineBasedFrameDecoder解码器,基于最简单的“换行符”进行接收到的信息的再组织。windows和linux两个操作系统中的“换行符”是不一样的,LineBasedFrameDecoder解码器都支持。当然这个解码器没有我们后面介绍的DelimiterBasedFrameDecoder解码器灵活。

serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {    @Override    protected void initChannel(NioSocketChannel ch) throws Exception {        ch.pipeline().addLast(new ByteArrayEncoder());        ch.pipeline().addLast(new LineBasedFrameDecoder(100));        ch.pipeline().addLast(new TCPServerHandler());        ch.pipeline().addLast(new ByteArrayDecoder());    }});
那么如果客户端发送的数据是:
this is 0 client \r\n request 1 \r\n”
那么接收方重新通过“换行符”重新组织后,将分两次接受到数据:
this is 0 client 
request 1

3.使用DelimiterBasedFrameDecoder解决问题
DelimiterBasedFrameDecoder是按照“自定义”分隔符(也可以是“回车符”或者“空字符”注意windows系统中和linux系统中“回车符”的表示是不一样的)进行信息的重新拆分。

serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {    @Override    protected void initChannel(NioSocketChannel ch) throws Exception {        ch.pipeline().addLast(new ByteArrayEncoder());        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1500, false, Delimiters.lineDelimiter()));        ch.pipeline().addLast(new TCPServerHandler());        ch.pipeline().addLast(new ByteArrayDecoder());    }});
DelimiterBasedFrameDecoder有三个参数,这里介绍一下:
DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf... delimiters)
# maxFrameLength:最大分割长度,如果接收方在一段长度 大于maxFrameLength的数据段中,没有找到指定的分隔符,那么这个处理器会抛出TooLongFrameException异常。
# stripDelimiter:这个是一个布尔型参数,指代是否保留指定的分隔符。
# delimiters:设置的分隔符。一般使用Delimiters.lineDelimiter()或者Delimiters.nulDelimiter()。当然您也可以自定义分隔符,定义成bytebuf的类型就行了。
来自:http://blog.csdn.net/yinwenjie/article/details/48969853


Netty使用LineBasedFrameDecoder解决TCP粘包拆包:
以下内容来自:http://www.cnblogs.com/wade-luffy/p/6165671.html(其中部分内容做了添加和修改)
无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制。

TCP粘包/拆包问题说明
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。


TCP粘包/拆包发生的原因
问题产生的原因有三个,分别如下。
(1)应用程序write写入的字节大小大于套接口发送缓冲区大小;
(2)进行MSS大小的TCP分段;
(3)以太网帧的payload大于MTU进行IP分片。


粘包问题的解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
(1)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
(2)在包尾增加回车换行符进行分割,例如FTP协议;
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;
(4)更复杂的应用层协议。


如果没有考虑读半包问题,在功能测试时往往没有问题,但是一旦压力上来,或者发送大报文之后,就会存在粘包/拆包问题。如果代码没有考虑,往往就会出现解码错位或者错误,导致程序不能正常工作。
未考虑TCP粘包导致功能异常案例:

package NettyTest2;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;public class TimeServer {    public void bind(int port) throws Exception {        //配置服务端的NIO线程组        //NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于网络事件的处理,        //实际上它们就是Reactor线程组。        //这里创建两个的原因是一个用于服务端接受客户端的连接,另一个用于进行SocketChannel的网络读写。        EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            //创建ServerBootstrap对象,它是Netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度。            ServerBootstrap b = new ServerBootstrap();            //调用ServerBootstrap的group方法,将两个NIO线程组当作入参传递到ServerBootstrap中。            //接着设置创建的Channel为NioServerSocketChannel,它的功能对应于JDK NIO类库中的ServerSocketChannel类。            //然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024,            //最后绑定I/O事件的处理类ChildChannelHandler,它的作用类似于Reactor模式中的handler类,            //主要用于处理网络I/O事件,例如记录日志、对消息进行编解码等。            b.group(bossGroup, workerGroup)                    .channel(NioServerSocketChannel.class)                    .option(ChannelOption.SO_BACKLOG, 1024)                    .childHandler(new ChildChannelHandler());            //绑定端口,同步等待成功            //服务端启动辅助类配置完成之后,调用它的bind方法绑定监听端口            //随后,调用它的同步阻塞方法sync等待绑定操作完成。            //完成之后Netty会返回一个ChannelFuture,它的功能类似于JDK的java.util.concurrent.Future,主要用于异步操作的通知回调。            ChannelFuture f = b.bind(port).sync();            //等待服务端监听端口关闭            //使用f.channel().closeFuture().sync()方法进行阻塞,等待服务端链路关闭之后main函数才退出。            f.channel().closeFuture().sync();        } finally {            //优雅退出,释放线程池资源            //调用NIO线程组的shutdownGracefully进行优雅退出,它会释放跟shutdownGracefully相关联的资源。            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }    private class ChildChannelHandler extends ChannelInitializer {        @Override        protected void initChannel(Channel channel) throws Exception {            channel.pipeline().addLast(new TimeServerHandler());        }    }    public static void main(String[] args) throws Exception {        new TimeServer().bind(8080);    }}
package NettyTest2;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class TimeServerHandler extends ChannelInboundHandlerAdapter {    private int counter;    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg)            throws Exception {        ByteBuf buf = (ByteBuf) msg;        byte[] req = new byte[buf.readableBytes()];        buf.readBytes(req);        String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());        System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter);        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?                new java.util.Date( System.currentTimeMillis()).toString() : "BAD ORDER";        currentTime = currentTime + System.getProperty("line.separator");        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());        ctx.writeAndFlush(resp);    }    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {        ctx.close();    }}
每读到一条消息后,就计一次数,然后发送应答消息给客户端。按照设计,服务端接收到的消息总数应该跟客户端发送的消息总数相同,
package NettyTest2;import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioSocketChannel;public class TimeClient {    public void connect(int port, String host) throws Exception {        // 配置客户端NIO线程组        //首先创建客户端处理I/O读写的NioEventLoop Group线程组        EventLoopGroup group = new NioEventLoopGroup();        try {            //继续创建客户端辅助启动类Bootstrap,随后需要对其进行配置。            //与服务端不同的是,它的Channel需要设置为NioSocketChannel            //然后为其添加handler,此处为了简单直接创建匿名内部类,实现initChannel方法,            //其作用是当创建NioSocketChannel成功之后,            //在初始化它的时候将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。            Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                    .option(ChannelOption.TCP_NODELAY, true)                    .handler(new ChannelInitializer() {                        @Override                        protected void initChannel(Channel channel) throws Exception {                            channel.pipeline().addLast(new TimeClientHandler());                        }                    });            // 发起异步连接操作            //客户端启动辅助类设置完成之后,调用connect方法发起异步连接,            //然后调用同步方法等待连接成功。            ChannelFuture f = b.connect(host, port).sync();            // 等待客户端链路关闭            //当客户端连接关闭之后,客户端主函数退出.            f.channel().closeFuture().sync();        } finally {            // 优雅退出,释放NIO线程组            //在退出之前,释放NIO线程组的资源。            group.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        new TimeClient().connect(8080, "127.0.0.1");    }}
package NettyTest2;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class TimeClientHandler extends ChannelInboundHandlerAdapter {    private int counter;    private byte[] req;    public TimeClientHandler() {        req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();    }    @Override    public void channelActive(ChannelHandlerContext ctx) {        ByteBuf message = null;        for (int i = 0; i < 100; i++) {            message = Unpooled.buffer(req.length);            message.writeBytes(req);            ctx.writeAndFlush(message);        }    }    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg)            throws Exception {        ByteBuf buf = (ByteBuf) msg;        byte[] req = new byte[buf.readableBytes()];        buf.readBytes(req);        String body = new String(req, "UTF-8");        System.out.println("Now is : " + body + " ; the counter is : " + ++counter);    }    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {        // 释放资源        ctx.close();    }}
客户端跟服务端链路建立成功之后,循环发送100条消息,每发送一条就刷新一次,保证每条消息都会被写入Channel中。按照我们的设计,服务端应该接收到100条查询时间指令的请求消息。客户端每接收到服务端一条应答消息之后,就打印一次计数器。按照设计初衷,客户端应该打印100次服务端的系统时间。

运行结果:
服务端运行结果如下
注意1:和原文章运行结果并不相同,后来发现是TimeServerHandler.java中的String body这行代码导致,但我咋么修改也运行不出原文章的结果来。。。
String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());的运行结果
The time server receive order : QUERY TIME ORDER
QUERY TIME ORDER
。。。。。。
QUERY TIME ORD ; the counter is : 1
The time server receive order : 
QUERY TIME ORDER
。。。。。。
QUERY TIME ORDER ; the counter is : 2

String body = new String(req, "UTF-8").substring(0, req.length);的运行结果
The time server receive order : QUERY TIME ORDER
QUERY TIME ORDER
。。。。。。
QUERY TIME ORDER ; the counter is : 1
The time server receive order : 
QUERY TIME ORDER
。。。。。。
 ; the counter is : 2


服务端运行结果表明它只接收到了两条消息,第一条包含57条“QUERY TIME ORDER”指令,第二条包含了43条“QUERY TIME ORDER”指令,总数正好是100条。我们期待的是收到100条消息,每条包含一条“QUERY TIME ORDER”指令。这说明发生了TCP粘包。
客户端运行结果如下
Now is : BAD ORDER
BAD ORDER
 ; the counter is : 1
按照设计初衷,客户端应该收到100条当前系统时间的消息,但实际上只收到了一条。这不难理解,因为服务端只收到了2条请求消息,所以实际服务端只发送了2条应答,由于请求消息不满足查询条件,所以返回了2条“BAD ORDER”应答消息。但是实际上客户端只收到了一条包含2条“BAD ORDER”指令的消息,说明服务端返回的应答消息也发生了粘包。由于上面的例程没有考虑TCP的粘包/拆包,所以当发生TCP粘包时,我们的程序就不能正常工作。


利用LineBasedFrameDecoder解决TCP粘包问题
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,只要能熟练掌握这些类库的使用,TCP粘包问题从此会变得非常容易,你甚至不需要关心它们,这也是其他NIO框架和JDK原生的NIO API所无法匹敌的。
服务端代码:

package NettyTest;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;public class TimeServer {    public void bind(int port) throws Exception {    // 配置服务端的NIO线程组        EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)                    .channel(NioServerSocketChannel.class)                    .option(ChannelOption.SO_BACKLOG, 1024)                    .childHandler(new ChildChannelHandler());            // 绑定端口,同步等待成功            ChannelFuture f = b.bind(port).sync();            // 等待服务端监听端口关闭            f.channel().closeFuture().sync();        } finally {            // 优雅退出,释放线程池资源            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }    @SuppressWarnings("rawtypes")private class ChildChannelHandler extends ChannelInitializer {        @Override        protected void initChannel(Channel arg0) throws Exception {            arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));            arg0.pipeline().addLast(new StringDecoder());            arg0.pipeline().addLast(new TimeServerHandler());        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if (args != null && args.length > 0) {            try {                port = Integer.valueOf(args[0]);            } catch (NumberFormatException e) {                // 采用默认值            }        }        new TimeServer().bind(port);    }}

package NettyTest;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class TimeServerHandler extends ChannelInboundHandlerAdapter {    private int counter;    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg)            throws Exception {        String body = (String) msg;        System.out.println("The time server receive order : " + body  + " ; the counter is : " + ++counter);        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?                new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";        currentTime = currentTime + System.getProperty("line.separator");        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());        ctx.writeAndFlush(resp);    }    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {        ctx.close();    }}

客户端代码:

package NettyTest;import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;public class TimeClient {    public void connect(int port, String host) throws Exception {// 配置客户端NIO线程组        EventLoopGroup group = new NioEventLoopGroup();        try {            Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                    .option(ChannelOption.TCP_NODELAY, true)                    .handler(new ChannelInitializer() {                        @Override                        public void initChannel(Channel ch)                                throws Exception {                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));//这两行高亮显示                            ch.pipeline().addLast(new StringDecoder());                            ch.pipeline().addLast(new TimeClientHandler());                        }                    });            // 发起异步连接操作            ChannelFuture f = b.connect(host, port).sync();            // 等待客户端链路关闭            f.channel().closeFuture().sync();        } finally {            // 优雅退出,释放NIO线程组            group.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if (args != null && args.length > 0) {            try {                port = Integer.valueOf(args[0]);            } catch (NumberFormatException e) {                // 采用默认值            }        }        new TimeClient().connect(port, "127.0.0.1");    }}

package NettyTest;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class TimeClientHandler extends ChannelInboundHandlerAdapter {    private int counter;    private byte[] req;    public TimeClientHandler() {        req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();    }    @Override    public void channelActive(ChannelHandlerContext ctx) {        ByteBuf message = null;        for (int i = 0; i < 100; i++) {            message = Unpooled.buffer(req.length);            message.writeBytes(req);            ctx.writeAndFlush(message);        }    }    @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) {        // 释放资源        ctx.close();    }}
两个变化:
1.拿到的msg已经是解码成字符串之后的应答消息
2.新增了两个解码器:第一个是LineBasedFrameDecoder,第二个是StringDecoder。

运行结果:
服务端执行结果如下
The time server receive order : QUERY TIME ORDER ; the counter is : 1
。。。。。。
The time server receive order : QUERY TIME ORDER ; the counter is : 100

客户端运行结果如下
Now is : Thu Feb 20 00:00:14 CST 2014 ; the counter is : 1
。。。。。。
Now is : Thu Feb 20 00:00:14 CST 2014 ; the counter is : 100


注意2:原文章中服务端和客户端的Handler都继承了ChannelHandlerAdapter类,但是当我将代码导入到Myeclipse中时(运行环境为jdk1.7和netty-4.0.53)却报错根本无法运行程序
解决:将ChannelHandlerAdapter改为ChannelInboundHandlerAdapter,后来才知道ChannelHandlerAdapter是ChannelInboundHandlerAdapter的父类(http://blog.csdn.net/linuu/article/details/51315373)
注意3:在代码中有这么行代码System.getProperty("line.separator"),一开始我居然不知道是什么意思,哎,还是我孤陋寡闻了啊。然后上网查了下,这是搜索的结果
在java中存在一些转义字符,比如"\n"为换行符,但是也有一些JDK自带的一些操作符
比如 : System.getProperty("line.separator")
这也是换行符,功能和"\n"是一致的,但是此种写法屏蔽了 Windows和Linux的区别 ,更保险一些.


        程序的运行结果完全符合预期,说明通过使用LineBasedFrameDecoder和StringDecoder成功解决了TCP粘包导致的读半包问题。对于使用者来说,只要将支持半包解码的handler添加到ChannelPipeline中即可,不需要写额外的代码,用户使用起来非常简单。


LineBasedFrameDecoder和StringDecoder的原理分析
        LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
        StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的handler。LineBasedFrameDecoder + StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。
        如果发送的消息不是以换行符结束的该怎么办呢?或者没有回车换行符,靠消息头中的长度字段来分包怎么办?是不是需要自己写半包解码器?答案是否定的,Netty提供了多种支持TCP粘包/拆包的解码器,用来满足用户的不同诉求。