从RedisHTTP协议,看Nett协议设计,我发现了个惊天大秘密
1. 协议的作用
TCP/IP 中消息传输基于流的方式,没有边界
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则2. Redis 协议
如果我们要向 Redis 服务器发送一条 set name Nyima 的指令,需要遵守如下协议// 该指令一共有3部分,每条指令之后都要添加回车与换行符 *3r // 第一个指令的长度是3 $3r // 第一个指令是set指令 setr // 下面的指令以此类推 $4r namer $5r Nyimar 复制代码
客户端代码如下public class RedisClient { static final Logger log = LoggerFactory.getLogger(StudyServer.class); public static void main(String[] args) { NioEventLoopGroup group = new NioEventLoopGroup(); try { ChannelFuture channelFuture = new Bootstrap() .group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { // 打印日志 ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 回车与换行符 final byte[] LINE = {"r"," "}; // 获得ByteBuf ByteBuf buffer = ctx.alloc().buffer(); // 连接建立后,向Redis中发送一条指令,注意添加回车与换行 // set name Nyima buffer.writeBytes("*3".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("$3".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("set".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("$4".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("name".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("$5".getBytes()); buffer.writeBytes(LINE); buffer.writeBytes("Nyima".getBytes()); buffer.writeBytes(LINE); ctx.writeAndFlush(buffer); } }); } }) .connect(new InetSocketAddress("localhost", 6379)); channelFuture.sync(); // 关闭channel channelFuture.channel().close().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 关闭group group.shutdownGracefully(); } } } 复制代码
控制台打印结果1600 [nioEventLoopGroup-2-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x28c994f1, L:/127.0.0.1:60792 - R:localhost/127.0.0.1:6379] WRITE: 34B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.| |00000010| 0a 6e 61 6d 65 0d 0a 24 35 0d 0a 4e 79 69 6d 61 |.name..$5..Nyima| |00000020| 0d 0a |.. | +--------+-------------------------------------------------+----------------+ 复制代码
Redis 中查询执行结果
3. HTTP 协议
HTTP 协议在请求行请求头中都有很多的内容,自己实现较为困难,可以使用 HttpServerCodec 作为服务器端的解码器与编码器,来处理 HTTP 请求// HttpServerCodec 中既有请求的解码器 HttpRequestDecoder 又有响应的编码器 HttpResponseEncoder // Codec(CodeCombine) 一般代表该类既作为 编码器 又作为 解码器 public final class HttpServerCodec extends CombinedChannelDuplexHandler implements HttpServerUpgradeHandler.SourceCodec 复制代码
服务器代码public class HttpServer { static final Logger log = LoggerFactory.getLogger(StudyServer.class); public static void main(String[] args) { NioEventLoopGroup group = new NioEventLoopGroup(); new ServerBootstrap() .group(group) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); // 作为服务器,使用 HttpServerCodec 作为编码器与解码器 ch.pipeline().addLast(new HttpServerCodec()); // 服务器只处理HTTPRequest ch.pipeline().addLast(new SimpleChannelInboundHandler() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) { // 获得请求uri log.debug(msg.uri()); // 获得完整响应,设置版本号与状态码 DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK); // 设置响应内容 byte[] bytes = "Hello, World!
".getBytes(StandardCharsets.UTF_8); // 设置响应体长度,避免浏览器一直接收响应内容 response.headers().setInt(CONTENT_LENGTH, bytes.length); // 设置响应体 response.content().writeBytes(bytes); // 写回响应 ctx.writeAndFlush(response); } }); } }) .bind(8080); } } 复制代码
服务器负责处理请求并响应浏览器。所以只需要处理 HTTP 请求即可// 服务器只处理HTTPRequest ch.pipeline().addLast(new SimpleChannelInboundHandler() 复制代码
获得请求后,需要返回响应给浏览器。需要创建响应对象 DefaultFullHttpResponse,设置 HTTP 版本号及状态码,为避免浏览器获得响应后,因为获得 CONTENT_LENGTH 而一直空转,需要添加 CONTENT_LENGTH 字段,表明响应体中数据的具体长度// 获得完整响应,设置版本号与状态码 DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK); // 设置响应内容 byte[] bytes = "Hello, World!
".getBytes(StandardCharsets.UTF_8); // 设置响应体长度,避免浏览器一直接收响应内容 response.headers().setInt(CONTENT_LENGTH, bytes.length); // 设置响应体 response.content().writeBytes(bytes); 复制代码
运行结果
浏览器
控制台// 请求内容 1714 [nioEventLoopGroup-2-2] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x72630ef7, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:55503] READ: 688B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 47 45 54 20 2f 66 61 76 69 63 6f 6e 2e 69 63 6f |GET /favicon.ico| |00000010| 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a | HTTP/1.1..Host:| |00000020| 20 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 38 30 0d | localhost:8080.| |00000030| 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 6b 65 65 |.Connection: kee| |00000040| 70 2d 61 6c 69 76 65 0d 0a 50 72 61 67 6d 61 3a |p-alive..Pragma:| .... // 响应内容 1716 [nioEventLoopGroup-2-2] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x72630ef7, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:55503] WRITE: 61B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.| |00000010| 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a |.Content-Length:| |00000020| 20 32 32 0d 0a 0d 0a 3c 68 31 3e 48 65 6c 6c 6f | 22....Hello| |00000030| 2c 20 57 6f 72 6c 64 21 3c 2f 68 31 3e |, World!
| +--------+-------------------------------------------------+----------------+ 复制代码4. 自定义协议
组成要素魔数:用来在第一时间判定接收的数据是否为无效数据包版本号:可以支持协议的升级序列化算法:消息正文到底采用哪种序列化反序列化方式如:json、protobuf、hessian、jdk指令类型:是登录、注册、单聊、群聊… 跟业务相关请求序号:为了双工通信,提供异步能力正文长度消息正文编码器与解码器public class MessageCodec extends ByteToMessageCodec { @Override protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { // 设置魔数 4个字节 out.writeBytes(new byte[]{"N","Y","I","M"}); // 设置版本号 1个字节 out.writeByte(1); // 设置序列化方式 1个字节 out.writeByte(1); // 设置指令类型 1个字节 out.writeByte(msg.getMessageType()); // 设置请求序号 4个字节 out.writeInt(msg.getSequenceId()); // 为了补齐为16个字节,填充1个字节的数据 out.writeByte(0xff); // 获得序列化后的msg ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(msg); byte[] bytes = bos.toByteArray(); // 获得并设置正文长度 长度用4个字节标识 out.writeInt(bytes.length); // 设置消息正文 out.writeBytes(bytes); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
泰山爆红,究竟做对了什么引网友疯狂种草?记者黄寿赓王建伟刘国林薛瑞荆新年泰山现在究竟有多红?3月7日,星期二,泰山景区便对外公告,截至当日下午16时,当周星期五星期六夜间的0元门票预约量就已满额。而来自泰山景区的数据显示
顾村公园樱花节开幕啦!顾村公园樱花节开幕时间3。154。15!趁着昨天天气好,一早就开车去顾村公园,可没曾想路上那么堵,41公里的距离用了一个半小时,建议没有太多东西的人,地铁更方便哦!我们从2号门进园
速来,闵行这片玉兰花海美翻了春日渐暖,闵行文化公园里的玉兰花开了。刻玉玲珑吹兰芬馥,美得让人心醉。趁春光正好,来这片玉兰花海看看吧。闵行文化公园是上海种植玉兰面积最大数量最多的玉兰特色公园,现有白玉兰黄玉兰紫
提前认知八大旅游陷阱,开启尽兴旅游的乐趣!每个人心中都有独属于他的诗和远方。计划着什么时候有闲暇,约上仨俩好友或是携同家人一起去踏足一个风景如画的地方。为了能有一段愉悦的旅程,你需要提防几个众所周知的旅游陷阱!1。错峰陷阱
古城五福春早古城五福春早我少年時代古城的舊居是中山街70號,臨近五福巷,尚書洪範中對五福的詮釋曰五福,一曰壽,二曰富,三曰康寧,四曰攸好德,五曰考終命。壽富康寧比較好理解。攸好德是說所愛好珍惜
来林芝一定要去的13个旅游景点,你都打卡了吗?(下篇)头条旅游旅游西藏西藏旅游林芝林芝旅游西藏头条这里有被称为西藏瑞士的巴松措这里是世界上落差最大的垂直地貌分布地区这里拥有丰富的森林自然资源,自然出产了大量的经济植物和药用植物。这里是
山东斥巨资修建假古镇,门票免费,本地的市民最先并不看好越来越多的人们喜欢在节假日的时候旅行,放松身心的同时也能够欣赏到大江山河不同的美丽景色,而且还可以增加我们对于民俗风情的认识。在所有景区中,带有丰富历史文化底蕴的景区吸引了更多的人
犹如一幅春日油画!一起领略花开中国春暖花开之季,踏青赏花时节,让我们一起领略花开中国。园博园紫花马缨丹盛开眼下,在广西南宁市园博园,5000多平方米的紫花马缨丹竞相开放。紫花马缨丹喜欢湿润温暖的环境,南宁园博园在荷
美国签证面签时,你是这几种情况基本上是没有问题的很多朋友在办理美签的时候会被各种原因拒签,那么在面签时你是以下这几种情况会给你加分的,进一步促进你拿到签证。图片来源于网络1。旅游购物美国人是欢迎多金的中国人去拉动当地GDP的,但
勐腊县坚持旅游业态创新打造康养旅居第二居所勐腊县森林覆盖率达到惊人的87,民风淳朴,拥有得天独厚的资源优势。我县坚持国际生态康养智慧的发展原则,聚集文游医养体学智全产业链,积极融入大滇西旅游环线建设,发展新型乡村旅游,开发
数字中国通信行业核心资产!电信运营商夯实数字经济底座,受益上市公司梳理财联社3月12日讯(编辑笠晨)中国联通近日发布2022年年报,信达证券表示,公司总营收增速创近九年新高,剔除非经营性损益后,净利润规模创上市新高。中国电信周五盘中再创历史新高,年初