1、TCP粘包与拆包
- TCP面向连接,面向流,提供高可靠性服务。在消息收发过程中,需要在发送端和接收端建立成对的Socket,发送端不会一有数据就进行发送,而是将多次间隔较小的,数据量较小的数据合并成一定长度的数据包整体发送。这样可以提高效率,但会给接收方分辨单个数据消息增加难度,因为面向流的通信是没有消息保护边界的。
- TCP粘包与拆包,是指发送端在发送多个数据消息时出现的不同情形。由于数据在发送前需要先转换为二进制字节码,当多个数据消息的字节码被合并成一个数据包发送时,称为粘包;当某个数据消息的字节码被划分到几个数据包内发送时,称为拆包。粘包和拆包可能使接收端解码数据包时出现错误。
- TCP粘包和拆包的解决方案:使用自定义协议+编解码器解决,只要接收端能够知道每次读取数据的长度,就可以按位读取,避免出现读取错误。关键问题——使接收端知道每次读取数据的长度。
2、TCP发送数据
- 使用TCP协议,客户端向服务器发送10条数据,观察服务器端接收消息的情况;
- 服务器myServer.java,服务器初始化类myServerInitializer.java,服务器自定义处理器myServerHandler.java。
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class myServer { public static void main(String[] args) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new mySeverInitializer()); //自己定义的初始化类 ChannelFuture channelFuture = serverBootstrap.bind(7001).sync(); channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } /************************************************************************************************/ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class mySeverInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new myServerHandler()); } } /************************************************************************************************/ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import java.nio.charset.Charset; import java.util.UUID; public class myServerHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //先把 byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); //将buffer转成一个字符串 String message = new String(buffer, Charset.forName("utf-8")); System.out.println("服务器接收到数据:" + message ); System.out.println("服务器接收到消息量 = "+(++this.count)); //服务器回送数据给客户端,随机ID ByteBuf byteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString()+"\n", Charset.forName("utf-8")); ctx.writeAndFlush(byteBuf); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
-
客户端 myClient.java,客户端初始化类myClientInitializer.java,客户端自定义处理器myClientHandler.java
import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; public class myClient { public static void main(String[] args) throws InterruptedException { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new myClientInitializer()); //自定义一个初始化对象 ChannelFuture channelFuture = bootstrap.connect("localhost", 7001).sync(); channelFuture.channel().closeFuture().sync(); }finally { group.shutdownGracefully(); } } } /************************************************************************************************/ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; public class myClientInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new myClientHandler()); } } /************************************************************************************************/ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; import java.nio.charset.Charset; public class myClientHandler extends SimpleChannelInboundHandler<ByteBuf> { private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //使用客户端循环发送10条数据 for (int i=0;i<10;++i){ ByteBuf buffer = Unpooled.copiedBuffer("hello,Server " + i, CharsetUtil.UTF_8); ctx.writeAndFlush(buffer); } } @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] bytes = new byte[msg.readableBytes()]; msg.readBytes(bytes); String message = new String(bytes, Charset.forName("utf-8")); System.out.println("客户端接收数据:"+message); System.out.println("客户端接收消息数量 = "+(++this.count)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
通过多开应用,每个客户端向服务器发送这10条数据时,发送次数和每次发送数据量都不同,表明由于服务端不清楚客户端发送的数据长度,出现了粘包和拆包问题。
3、通过自定义协议对TCP数据包进行分割
3-1、案例要求
- 要求客户端发送5个message对象,客户端每次发送一个message对象
- 服务器端每次接收一个message,分5次进行解码,每读取一个message,会回送一个message对象给客户端
3-2、案例分析
- 规定数据发送的形式:数据内容+数据长度。发送时先发送本次发送的数据长度,接收端接收后根据长度从数组中读取相应的数据内容。
- 自定义编码器和解码器,数据内容按照byte形式编解码,数据长度按照int形式编解码。
3-3、代码实现
- 客户端与服务器主程序与之前相同,不做赘述;
- 自定义协议MessageProtocol.java
//协议包 public class MessageProtocol { private int length; //关键 private byte[] context; public int getLength() { return length; } public byte[] getContext() { return context; } public void setLength(int length) { this.length = length; } public void setContext(byte[] context) { this.context = context; } }
- 自定义编码器myMessageEncoder.java
import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class myMessageEncoder extends MessageToByteEncoder<MessageProtocol> { @Override protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception { System.out.println("myMessageEncoder encode方法被调用"); out.writeInt(msg.getLength()); out.writeBytes(msg.getContext()); } }
- 自定义解码器myMessageDecoder.java
import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ReplayingDecoder; import java.util.List; public class myMessageDecoder extends ReplayingDecoder<Void> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { System.out.println("myMessageDecoder decode方法被调用"); //将得到的二进制字节码转换为 MessageProtocol 数据包 int length = in.readInt(); byte[] content = new byte[length]; in.readBytes(content); //封装成MessageProtocol对象,放入out中交给下一个handler处理 MessageProtocol messageProtocol = new MessageProtocol(); messageProtocol.setLength(length); messageProtocol.setContext(content); out.add(messageProtocol); } }
- 服务器初始化类myServerInitializer.java需要在handler前添加编解码器:pipeline.addLast(...);
- 客户端初始化类myClientInitializer.java需要在handler前添加编解码器:pipeline.addLast(...);
- 自定义服务器业务处理器myServerHandler.java
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; import java.util.UUID; public class myServerHandler extends SimpleChannelInboundHandler<MessageProtocol> { private int count; @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception { //接收数据并处理 int len = msg.getLength(); byte[] context = msg.getContext(); System.out.println("服务端接收到信息如下"); System.out.println("数据长度:"+len); System.out.println("内容:"+new String(context, CharsetUtil.UTF_8)); System.out.println("服务器接收到协议包数量 = "+(++this.count)); //回复消息 String response = UUID.randomUUID().toString(); int responseLen = response.getBytes("utf-8").length; byte[] responseBytes = response.getBytes("utf-8"); //构建一个协议包 MessageProtocol messageProtocol = new MessageProtocol(); messageProtocol.setLength(responseLen); messageProtocol.setContext(responseBytes); ctx.writeAndFlush(messageProtocol); } }
- 自定义客户端业务处理器myClientHandler.java
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; import java.nio.charset.Charset; public class myClientHandler extends SimpleChannelInboundHandler<MessageProtocol> { private int count; @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //使用客户端循环发送10条数据 for (int i=0;i<5;i++){ String mes = "今天下雨,出门带伞"; byte[] content = mes.getBytes(Charset.forName("utf-8")); int length = mes.getBytes(Charset.forName("utf-8")).length; //创建协议包 MessageProtocol messageProtocol = new MessageProtocol(); messageProtocol.setLength(length); messageProtocol.setContext(content); ctx.writeAndFlush(messageProtocol); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("异常消息 = "+cause.getMessage()); ctx.close(); } @Override protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception { int len = msg.getLength(); byte[] msgContext = msg.getContext(); System.out.println("客户端接收的消息如下:"); System.out.println("消息长度 = "+len); System.out.println("消息内容 = "+new String(msgContext, CharsetUtil.UTF_8)); System.out.println("客户端接收消息的数量 = "+(++this.count)); } }