架构师-Netty(三)

《Netty权威指南》
基于Netty5.0案例

TCP粘包/拆包

在这里插入图片描述
TCP粘包/拆包发生原因:

  • 应用程序write写入的字节大小大于套接口发送的缓冲区大小;
  • 进行MSS大小的TCP分段
  • 以太网帧的payload大小MTU进行IP分片

在底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计解决,业界主流方案:

  • 消息定长,例如每个报文大大小为固定长度200字节,如果不够,空位补空格;
  • 在包尾增加回车换行符进行分割,例如FTP协议;
  • 将消息分为消息头和消息体,消息头中包含表示消息总长度(或消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;
  • 更复杂的应用协议
    下面是发生TCP粘包代码

服务端Handler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.Date;
public class TimerServerHandler extends ChannelInboundHandlerAdapter {
    private int counter;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        // buf.readableBytes() 方法可获取缓冲区可读字节数
        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 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) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
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) {

        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 ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    ch.pipeline().addLast(new TimerServerHandler());
                }
            });
            // 绑定端口,同步得带成功
            ChannelFuture f = b.bind(port).sync();
            // 等待服务监听端口关闭
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
         new TimeServer().bind(8080);
    }
}

客户端Handler

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();
    }
    // 当客户端和服务端TCP链路建立成功之后,Netty的NIO线程会调用channelActive
    @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);
        }
    }
    // 当服务端会应答消息时,chanelRead方法被调用
    @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) throws Exception {
        System.out.println("Unexpected exception from downstream : " + cause.getMessage());
        ctx.close();
    }
}
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TimerClient {
    public void connect(String host, int port) 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<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            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 {
        new TimerClient().connect("localhost", 8080);
    }
}

通过LineBaseedFrameDecoder和StringDecoder改造
修改 TimerServerHandler#channelRead

  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 Date(System.currentTimeMillis()).toString() : "BAD ORDER";
        currentTime = currentTime + System.getProperty("line.separator");
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(resp);
    }

修改 TimeClientHandler#channelRead

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
        String body = (String) msg;
        System.out.println("Now is : " + body + "; the counter is : " + ++counter);
    }

TimeClient 和 TimeServer 的pipeline 添加LineBasedFrameDecoder和StringDecoder

   ch.pipeline()
               .addLast(new LineBasedFrameDecoder(1024))
               .addLast(new StringDecoder())
               .addLast(new TimerServerHandler());

分隔符和定长解码器

TCP以流的方式进行数据传输,上层应用协议为类消息进行区分,往往采用如下4中方式:

  • 消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到一个完整消息;将计数器置位,重新开始读取下一个数据报;
  • 将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛;
  • 将特殊的分隔符作为消息结束标志,回车换行符就是一种特殊的结束分隔符;
  • 通过消息头中定义长度字段来标识消息的总长度。

Netty提供了4中解码器来解决对应的问题:

  • LineBasedFrameDecoder解决TCP的粘包问题
  • DelimiterBasedFrameDecoder 解决以分割符结束标志的消息的解码
  • FixedLengthFrameDecoder 解决对定长消息解码
  • LengthFieldBasedFrameDecoder 解决消息头长度标识消息的解码

编解码

MessagePack

<dependency>
     <groupId>org.msgpack</groupId>
     <artifactId>msgpack</artifactId>
     <version>0.6.12</version>
</dependency>

MsgpackEncoder

public class MsgpackEncoder extends MessageToByteEncoder<Object> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        MessagePack msgpack = new MessagePack();
        // Serialize
        byte[] raw = msgpack.write(msg);
        out.writeBytes(raw);
    }
}
public class MsgpackDecoder extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        final byte[] array;
        final int length = msg.readableBytes();
        array = new byte[length];
        msg.getBytes(msg.readerIndex(), array, 0, length);
        MessagePack msgpack = new MessagePack();
        out.add(msgpack.read(array));
    }
}

```pipeline`

	// LengthFieldBasedFrameDecoder和LengthFieldPrepender支持粘包/半包
      ch.pipeline()
                                    .addLast("frameEncoder",new LengthFieldBasedFrameDecoder(65535,0,2,0,2))
                                    .addLast("msgpack decoder", new MsgpackDecoder())
                                    .addLast("frameEnConder",new LengthFieldPrepender(2))
                                    .addLast("msgpack encoder", new MsgpackEncoder());

Protobuf

ProtobufDecoder 负责解码,不支持半包
ProtobufEncoder 负责编码
三种方式半包解码器:

  • ProtobufVarint32FrameDecoder 处理半包消息
  • 集成通用半包解码器LengthFieldBaseFrameDecoder
  • 继承ByteToMessageDecoder类,自己处理半包消息

JBoss Marshalling

JBoss Marshalling 是一个java对象序列化包,对JDK默认的序列化框架进行优化

<dependency>
    <groupId>org.jboss.marshalling</groupId>
    <artifactId>jboss-marshalling</artifactId>
    <version>2.0.9.Final</version>
</dependency>
<dependency>
    <groupId>org.jboss.marshalling</groupId>
    <artifactId>jboss-marshalling-serial</artifactId>
    <version>2.0.9.Final</version>
    <scope>test</scope>
</dependency>
public final class MarshallingCodeCFactory {
    public static MarshallingDecoder buildMarshallingDecoder() {
        final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
        final MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        UnmarshallerProvider provide = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
        MarshallingDecoder decoder = new MarshallingDecoder(provide, 1024);
        return decoder;
    }

    public static MarshallingEncoder buildMarshallingEncoder() {
        final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory("serial");
        final MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory,configuration);
        MarshallingEncoder encoder = new MarshallingEncoder(provider);
        return encoder;
    }
}
   ch.pipeline().addLast(MarshallingCodeCFactory.buildMarshallingDecoder())                            .addLast(MarshallingCodeCFactory.buildMarshallingEncoder())

Netty多协议

Http

HttpRequestDecoder 请求解码器,把ByteBuf转换成HttpRequest
HttpResponseEncoder 相应的编码器,HttpResponse或者HttpContext转换成ByteBuf
HttpServercodec 上面两者结合
HttpObjectAggregator通过它可以把HttpMessageHttpContent 聚合成一个 FullHttpRequest 或者 FullHttpResponse (取决于是处理请求还是响应),而且它还可以帮助你在解码时忽略是否为“块”传输方式。
QueryStringDecoder:把 HTTP uri 分割成 path 和 key-value 参数对,也可以用来解码Content-Type = “application/x-www-form-urlencoded” 的 HTTP POST。特别注意的是,该 decoder 仅能使用一次

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        String url = request.uri();
        Map<String,String> resMap = new HashMap<>();
        resMap.put("method",request.method().name());
        resMap.put("url",url);

        String msg = "<html><head><title>test</title><head><body>你请求的url为:" + url + "</body></html>";
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                                                                HttpResponseStatus.OK,
                                                                 Unpooled.copiedBuffer(msg.getBytes()));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}
// pipeline
//                            HttpServerCodec
ch.pipeline().addLast("http-dcoder",new HttpRequestDecoder())
                                    .addLast("http-aggregator", new HttpObjectAggregator(65536))
                                    .addLast("http-encoder",new HttpResponseEncoder())
                                    .addLast("http-chunked",new ChunkedWriteHandler())//  图片传输处理器
                                    .addLast("fileServerHandler",new HttpRequestHandler());
// 或
ch.pipeline().addLast(new HttpServerCodec()) // http编码器
                                    .addLast("httpAggregator", new HttpObjectAggregator(512*1024)) // http消息聚合器
                                    .addLast("fileServerHandler",new HttpRequestHandler());

webSocket

WebSocket是HTML5开始提供的一种浏览器和服务端间进行全双工通信网络技术,在WebSocketAPI,浏览器和服务端只需要做一次握手动作。
webSocket特点:

  • 单一的TCP连接,采用全双工模式通信
  • 对代理、防火墙和路由器透明
  • 无头部信息、Cookie和身份验证
  • 无安全开销
  • 通过ping/pong帧保持链路激活
  • 服务器可以主动传送消息给客户端,不再需要客户端轮询
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;

import java.util.Date;

import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpUtil.setContentLength;

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 传统的HTTP接入
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        }

        // WebSocket 接入
        else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
            return;
        }
        // 判断是否是ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
        }
        // 仅处理文本消息,不支持二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
        }
        // 返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        System.out.println(ctx.channel() + " received " + request);

        ctx.channel().write(new TextWebSocketFrame(request + ", 欢迎使用Netty WebSocket服务,现在时刻: " + new Date().toString()));
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        // 如果HTTP解码失败,返回HTTP异常
        //
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpRespone(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        // 构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws//localhost:8080/websocket", null, false);
        handshaker = factory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }

    }

    private void sendHttpRespone(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回应答给客户端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            setContentLength(res, res.content().readableBytes());
        }

        // 若果是非keep-Alive,关闭连接
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!isKeepAlive(req) || (res.status().code() != 200)) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }


}

pipeline

 ch.pipeline().addLast(new HttpServerCodec())
                                    .addLast(new HttpObjectAggregator(65536))
                                    .addLast(new ChunkedWriteHandler())
                                    .addLast(new WebSocketServerHandler());

其他资源

https://zhuanlan.zhihu.com/p/26958943
http://ifeve.com/netty5-user-guide/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值