文章目录

  • 1.协议的引入
  • 2. HTTP协议的编解码实现
  • 3. 自定义协议
    • 3.1自定义协议的要素
    • 3.2 自定义协议通过Netty进行编解码
  • 4. 啥时候用@Sharable

参考黑马程序员

1.协议的引入

协议就是客户端向服务端发送消息的时候,双方约定俗成的一套规矩,比如我在redis中创建一个key,value。按理说要用到这个命令,set key value。这个命令在发送的时候会被解析成redis服务器能看懂的形式。比如说set name zhangsan你看到的只是一条命令,实际上向服务端发送的是这样的

    /*set name zhangsan*3 $3set$4name$8zhangsan*/
  • *后面是3意思是,这个数组里有3个元素set key value正好3个元素。
  • $意思是这个元素有几个字节
  • 然后添加发送的内容
  • 每发送一条都要添加回车换行符

我们通过程序测试一下按照这个协议发送对还是不对。

    public static void main(String[] args) {final byte[] LINE = {13, 10};NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ch.pipeline().addLast(new LoggingHandler());ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelActive(ChannelHandlerContext ctx) {ByteBuf buf = ctx.alloc().buffer();buf.writeBytes("*3".getBytes());buf.writeBytes(LINE);buf.writeBytes("$3".getBytes());buf.writeBytes(LINE);buf.writeBytes("set".getBytes());buf.writeBytes(LINE);buf.writeBytes("$4".getBytes());buf.writeBytes(LINE);buf.writeBytes("name".getBytes());buf.writeBytes(LINE);buf.writeBytes("$8".getBytes());buf.writeBytes(LINE);buf.writeBytes("zhangsan".getBytes());buf.writeBytes(LINE);ctx.writeAndFlush(buf);}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;System.out.println(buf.toString(Charset.defaultCharset()));}});}});ChannelFuture channelFuture = bootstrap.connect("localhost", 6379).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error", e);} finally {worker.shutdownGracefully();}}

然后我们启动redis清空所有的key value看看到底添没添加key value。handler我们添加两个一个是channelActive,负责向服务器发送数据,一个是channelRead,负责接收服务端的回复。

我们能看到服务端给我们返回了一个ok说明这条数据已经被添加了。我们看看redis能不能查到这条数据。

如图所示我们能在redis中拿到这条key-value。

综上所述客户端和服务端之间想要进行通信,要约定好规则。这个规则就是协议。

2. HTTP协议的编解码实现

HTTP协议的实现比较复杂,但是Netty已经帮我们把HTTP的编解码器实现好了,我们只需要简单配置即可使用。我们可以在服务端添加Http的编解码器HttpServerCodec来实现对接收消息的解码和对发送消息的编码。来看一下HttpServerCodec声明。

public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>implements HttpServerUpgradeHandler.SourceCodec

我们可以看到它继承了一个组合编码器里面既有HttpRequestDecoder也有HttpResponseEncoder所以它既可以对数据进行编码也可以对数据进行解码。

    public static void main(String[] args) {NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(boss, worker);serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));ch.pipeline().addLast(new HttpServerCodec());ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {log.debug("{}", msg.getClass());}});}});ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.error("server error", e);} finally {boss.shutdownGracefully();worker.shutdownGracefully();}}

如图所示上面是请求行,包含了请求类型,请求路径,请求协议。然后底下一大堆就是请求体了。然后我们看看消息是什么类型的。

我们可以看到接收到的消息类型一个是DefaultHttpRequest,即请求头,另一个是LastHttpContent$1,即请求体。如果你想对这两种消息分别处理可以使用SimpleChannelInboundHandler泛型后面添加你感兴趣的消息的类。对该消息进行处理。

                    ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {// 获取请求log.debug(msg.uri());// 返回响应DefaultFullHttpResponse response =new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);byte[] bytes = "<h1>Hello, world!</h1>".getBytes();// 规定好返回数据的长度,返回之后浏览器就会结束不会等了。response.headers().setInt(CONTENT_LENGTH, bytes.length);response.content().writeBytes(bytes);// 写回响应ctx.writeAndFlush(response);}});

比如你想获得HttpRequest类型的消息,你就可以泛型上添加HttpRequest类型。这里我获取请求的uri并打印出来。然后返回响应,响应给浏览器Hello,world!然后我们再次打开浏览器向服务端发送请求。

3. 自定义协议

3.1自定义协议的要素

  • 魔数,用来在第一时间判定是否是无效数据包
  • 版本号,可以支持协议的升级
  • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
  • 指令类型,是登录、注册、单聊、群聊… 跟业务相关
  • 请求序号,为了双工通信,提供异步能力
  • 正文长度
  • 消息正文

3.2 自定义协议通过Netty进行编解码

我们自己设计一个协议就要按照上面协议的要素进行设计,首先我们先抽象出来一个消息类型,MyMessage,然后通过里面的消息类型来辨别他是哪种消息。不同的消息单独写一个具体的类然后继承自父类MyMessage。

MyMessage

@Data
public abstract class MyMessage implements Serializable {// 请求消息public final static int REQUEST_MESSAGE = 0;// 响应消息public final static int RESPONSE_MESSAGE = 1;// 请求序号private int sequenceId;public abstract int getMessageType();}

这个Message必须实现序列化接口

MyRequestMessage

@Data
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public class MyRequestMessage extends MyMessage{private String name;private int age;@Overridepublic int getMessageType() {return REQUEST_MESSAGE;}
}

因为我们这个是请求的消息,气球内容包括姓名和年龄,然后继承父类MyMessage,重写里面的获取消息类型的方法,返回请求消息。

然后我们需要实现对MyMessage的编解码。

@Slf4j
public class MyMessageCodec extends ByteToMessageCodec<MyMessage> {@Overridepublic void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception {// 4字节的魔数out.writeBytes(new byte[]{1,2,3,4});// 1字节的版本out.writeByte(1);// 1字节的序列化方式out.writeByte(0);// 1字节的指令类型out.writeByte(msg.getMessageType());// 4字节的请求序号out.writeInt(msg.getSequenceId());// 无意义,对齐填充,如果不填充就是15字节填充一下变成2的n次方out.writeByte(0xff);// 获取内容的字节数组ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(msg);byte[] bytes = bos.toByteArray();// 7. 长度out.writeInt(bytes.length);// 8. 写入内容out.writeBytes(bytes);}@Overridepublic void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {int magicNum = in.readInt();byte version = in.readByte();byte serializerType = in.readByte();byte messageType = in.readByte();int sequenceId = in.readInt();in.readByte();int length = in.readInt();byte[] bytes = new byte[length];in.readBytes(bytes, 0, length);ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));MyMessage message = (MyMessage) ois.readObject();log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);log.debug("{}", message);out.add(message);}
}

然后我们对他进行测试

public class TestMyMessageCodec {public static void main(String[] args) throws Exception {EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler(),new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0),new MyMessageCodec());// encodeMyRequestMessage message = new MyRequestMessage("wjz",21);// decodeByteBuf buf = ByteBufAllocator.DEFAULT.buffer();new MyMessageCodec().encode(null, message, buf);channel.writeInbound(buf);}
}

这边我们通过writeInboud的方式向channel中写入数据,然后channel中添加handler添加一个LoggingHandler为了打印日志。LengthFieldBasedFrameDecoder负责防止粘包和半包。然后就是我们自己的编解码器。最终我们看一下结果。

我们解析出来结果看到请求的消息被完全接收到。然后第一排就是我们的具体对象之前的内容。比如说这里的16909060实际上就是01 02 03 04,之后的01是版本号,00是序列化算法我们这里是jdk,00是请求类型,然后就是序列号00 00 00 00 我们没有设置就全是0,然后就是ff填充的,最后四个字节就是我们真正的消息长度。

4. 啥时候用@Sharable

我们在添加handler时总是new LoggingHandler或者new LengthFieldBasedFrameDecoder也就是说我们每次都要new一个新的handler,但是在有些时候我们可以让多个channel共用一个handler,比如说new LoggingHandler。但是有些时候我们就不能让多个channel共用一个handler,比如说LengthFieldBasedFrameDecoder。

如图所示

我两个channel共用一个解码器,这个是按长度解析的,8字节一条数据。首先channel要解析出来的数据应该是12345678,但是在channel1先把1234给到这个解码器之后,channel2也过来把他的数据交给了解码器,正好达到了8字节,所以输出的时候就是12341234,这就出问题了,所以LengthBasedFieldFrameDecoder就不能sharable。否则容易出现线程不安全的问题。

因此当handler不保存状态的时候它是可以被sharable的,如果handler保存了状态他就不能被sharable,会引发线程不安全问题。

Netty协议的设计与解析相关推荐

  1. Netty协议设计与解析

    Netty协议设计与解析 1. 为什么需要协议? TCP/IP 中消息传输基于流的方式,没有边界. 协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则 例如:在网络上传输 下雨天留客天留我 ...

  2. Netty 基础-协议设计与解析

    6. 协议设计与解析 6.1 为什么需要协议? TCP/IP 中消息传输基于流的方式,没有边界. 协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则 例如:在网络上传输 下雨天留客天留我不 ...

  3. 微软大佬带你深入解析websocket丨tcp自定义协议的设计丨服务器高并发场景的优化

    各个方面都离不开的websocket,只是你没有注意到 1. websocket的应用场景 2. tcp自定义协议的设计 3. 服务器高并发场景的优化 [Linux服务器开发系列]微软大佬带你深入解析 ...

  4. 协议簇:TCP 解析: Sequence Number

    简介 序列号(Sequence Number) 是 TCP 协议中非常重要的一个概念,以至于不得不专门来学习一下.这篇文章我们就来解开他的面纱. 在 TCP 的设计中,通过TCP协议发送的每个字节都对 ...

  5. 协议簇:TCP 解析: 建立连接

    简介 接前文 协议簇:TCP 解析: 基础, 我们这篇文章来看看 TCP 连接建立的过程,也就是众所周知的"三次握手"的具体流程. 系列文章 协议簇:TCP 解析:基础 协议簇:T ...

  6. Netty 高性能架构设计

    Netty 高性能架构设计 Netty 概述 原生 NIO 存在的问题 Netty 官网说明 Netty 的优点 Netty 版本说明 线程模型基本介绍 传统阻塞 I/O 服务模型 Reactor 模 ...

  7. RSF 分布式服务框架-传输协议层设计

    为什么80%的码农都做不了架构师?>>>    这是接上一篇文章<RSF 分布式服务框架设计>之后的续作,主要是 Hasor-RSF 协议层的设计. RSF 的协议层设计 ...

  8. 基于Linux系统的边界网关协议的设计与实现

    基于Linux系统的边界网关协议的设计与实现 3.6 BGP和RMer系统间通信 RMer系统和BGP系统之间采用的是UNIX本地的服务器客户端模式进行通信,它们创建的socket的地址格式为AF_U ...

  9. 物联网常见协议之Amqp协议及使用场景解析

    摘要:本文围绕AMQP协议,为大家详细解析AMQP协议.核心技术亮点.多协议之间的对比以及使用实践. 本文分享自华为云社区<物联网常见协议之Amqp协议及使用场景解析>,作者:张俭. 引言 ...

最新文章

  1. Martin Fowler谈《重构HTML:改善Web应用的设计》
  2. YUM部署高版本LNMP环境
  3. python处理utf8编码中文,及打印中文列表和字典
  4. linux网络编程之广播详细代码及文档说明 -,Linux网络编程之广播
  5. 软考网络工程师笔记-综合知识1
  6. 该服务器支持最多2100个参数,Mybatis批量查询拼装参数超长的解决办法
  7. apache的server-status如何分析的技术说明
  8. (day 32 - 位运算 )剑指 Offer 56 - I. 数组中数字出现的次数
  9. 无线通信基础(三):高斯噪声中的估计
  10. 常见的HTTP状态码大全
  11. uniapp选择图片压缩并上传
  12. uwsgi 的启动、停止、重启
  13. UNIX环境高级编程学习总结
  14. 大学生破译周鸿祎手机号 李开复放 橄榄枝
  15. 成功解决android 网络视频边下载变播放。
  16. linux的cut命令详解
  17. nest模块(module)
  18. MES系统的应用价值
  19. 二维码可以用哪款条码软件打印?
  20. Firewalld防火墙实例配置

热门文章

  1. 可怜的Sun公司,因为收购了MySQL,想卖自己都卖不了了
  2. w7计算机找不到桌面选项,Win7系统右键计算机属性不见了怎么办?
  3. ISO9001初次审核时需要准备哪些资料?
  4. mysql针对密码过期和非一体式的安装包
  5. windos server 2008集成usb3.0
  6. LinuxDeepin11.12Beta2版本发布
  7. 微信红包高级接口JAVA实现
  8. 博弈论 负极大值算法
  9. HTTP跳转到HTTPS
  10. 计算机软件技术应用于工程设计,计算机软件技术在化工工程设计中的应用论文原稿...