目录
24.1 StringDecoder,StringEncoder
24.3 java序列化相关的编解码器:ObjectDecoder,ObjectEncoder
22.编解码器
22.1 编解码的概念
首先明确一点,网络传输大部分是建立在TCP传输协议上的,TCP是一个无边界的字节流传输协议,它不认你的那些什么User user = new User(),这玩意到了传输层早就没了,传输层看到的一律都是字节流。所以这里就有一层交互的问题,我们在应用层编码,编码出来的东西都是结构化的东西,但是在网络上传输的时候是要转为字节流的,这样才能被网络识别使用。这一过程称之为编码
而且为了方便转为字节流,在应用端发去传输层的时候我们一般要做一次序列化,序列化是为了保证格式。
而在对端接收的时候,它是从网络上接收的是字节流数据,但是对端应用是要处理数据的,你字节流太难读了,太麻烦。所以还要把字节流转换为应用端能读取识别的东西,这一过程就称之为解码。
22.2 netty中的编解码
netty作为网络通信传输框架,它也实现了自己的编解码器,在netty中处理编解码器的就是处理数据的Handler,因为handler实际上就是处理数据的,而我们编解码就是对于数据的,所以其实就是handler,更加准确的说就是ChannelHandler。其实他就是一种特殊的Handler,用来处理编解码的
我们来看一下实际处理编解码的类:
该类的注释翻译:
用于将字节编码/解码为消息(反之亦然)的即时编码/解码器。
可将其视为 ByteToMessageDecoder 和 MessageToByteEncoder 的组合。
请注意,ByteToMessageCodec 的子类不得使用 @Sharable 进行注解。
那么实际上干活的还是ByteToMessageDecoder和MessageToByteEncoder,那么我们就来看看这两个类
- ByteToMessageDecoder
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
我们看到这个字节流转为Message消息数据的意思就是解码,就是接收端的转为实际的消息数据。我们看到它继承了ChannelInboundHandlerAdapter,这个是输入的消息处理器Handler,这个其实可以理解,因为我们要解码就是在接收端处理的,把从网络接收来的字节流转换成消息Message数据,所以我们在这里其实就是在接收端处理的,那就是ChannelInboundHandlerAdapter这个handler了。但是ByteToMessageDecoder是一个抽象类,实际开发中并不能使用,无法实例化,我们是要使用它的子类
- MessageToByteEncoder
public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
这个的原理和上面那个解码器是一样的,只不过这个是编码器,要把实际数据编码成字节流,所以在输出端写出端做的。同样的,我们开发中也无法使用它,用的是它的子类。
22.3 序列化
我们上面看到了编码是要把数据转为字节流或解码把字节流转为数据,但是有一个问题,我们上面所说的TCP是字节流传输,字节流是非结构化的无边界的流数据,虽然流有序,但是无边界呀。假如我们在对端接收到字节流,你咋知道对面传来的User对象是什么结构呢?你怎么把接收到的字节流转换成User对象呢?不能吧。
所以我们要在两端约定一个格式。比如约定好User的结构就是x结构,然后转换为字节流,对端拿到字节流,然后再按照x结构转换为User对象,在对端就可以拿到一个你要的结构化的数据了
总结:
发送端进行编码操作的过程中,会进行序列化,序列化就是把Java对象转换成一种特殊的格式,这种格式底层传输时对应的也是一种字节流二进制数据,只不过让该字节流具有了特殊格式。
接收端进行解码操作的过程中,会进行反序列化,反序列化就是把特殊格式的字节流二进制数据进行转换回Java对象。
当然,序列化与反序列化通常是第三方给的一组接口调用。
比如:
调用JSON提供的序列化API接口可以把java对象转换成JSON字符串这种特殊的格式(序列化),底层可以再把json这种特殊的格式转换成二进制字节流进行传输。接收方拿到二进制字节流数据后首先会转换回json字符串这种特殊的格式,然后再会调用JSON提供的反序列化API接口把JSON这种特殊的格式转换成java对象(反序列化)。JSON提供的序列化与反序列化API接口就一组规范和协议。
在编程中,常见的序列化和反序列化方式包括:
JSON序列化和反序列化:将数据对象转换为JSON字符串形式,或将JSON字符串转换为数据对象形式。JSON是一种文本格式,易于阅读和解析。
Protobuf序列化和反序列化:使用Protobuf的二进制编码方式将数据对象转换为紧凑的二进制格式,或将二进制数据转换为数据对象形式。Protobuf具有较高的性能和空间效率。
XML序列化和反序列化:将数据对象转换为XML格式,或将XML格式转换为数据对象形式。XML是一种通用的标记语言,具有一定的结构和可读性。
序列化与反序列化只针对于Java对象
补充:序列化有什么好处?
23.编解码器在使用过程中的两部分核心内容
23.1 序列化协议(编码格式)(传输数据的格式)
23.1.1 Java默认的序列化与反序列化
开发步骤:
1.Java对象类实现Seriliazable接口。该接口是一个标识性接口,里面没有任何方法。像这样的标识性接口,还有像Spring中的ThrowsAdvice
2.发送方首先使用ObjectOutputStream进行序列化Java对象,把Java对象转换成一种二进制的特殊格式,然后编码。这里其实还挺高效,序列化直接把Java对象转换成了二进制的格式,编码的时候就无需再进行过多的特殊转换了,底层网络传输或文件io传输就是二进制字节流形式
3.接收方首先会进行解码操作,把通过网络传输或文件io传输过来的二进制字节流解码一下,然后把这种特殊的二进制特殊格式使用ObjectInputStream进行反序列化转换回Java对象。
注意:
Java对象最好是加上一个版本号:
private static final long serialversionUID = 1; //如果不显示指定的话,也会默认生成一个该属性字段,只不过默认值为所在类对象的hashcode值
如果不显示指定serialversionUID会怎么样?
如图所示:如果不显示指定serialversionUID,默认情况下Java会为该类提供一个默认的serialversionUID,默认值为该类的hashcode值。但是呢,如上图所示的情况,如果序列化前我们的User类和反序列化后的User类不一样,反序列化后的User类比之前多了一个属性x,那么User类的hashcode值发生了变化,由于hashcode值变化了,所以导致serialversionUID变化了,所以序列化前后比对不上,错误----》那么反序列化后转换成的User类对象的属性值也无法赋值给id和name!
如果自己规定一个serialversionUID值,并且保证序列化与反序列化前后都不改变,那么即使我们改了User类的属性字段,那么比对serialversionUID值也可以比对成功,那么id属性和name属性的值也可以通过反序列化后的数据值给赋值成功!
存在问题:
1.无法跨语言,只能java
2.可读性差,直接把java对象序列化成二进制数据,虽然二进制很紧凑节省空间,并且编码解码的时候由于都是二进制数据,所以也很高效,但是二进制数据的可读性一定很差
3.java序列化后的数据大小很大:虽然是序列化成二进制数据,但是是ByteBuffer的5倍,传输效率低
4.java序列化的时间长,是ByteBuffer的5倍
23.1.2 XML的序列化与反序列化
步骤:
(1)发送方把Java对象进行XML序列化成一种XML格式的数据
<user>
<id>1</id>
<name>sunshuai</name>
</user>
(2)发送方把该XML格式的数据进行编码操作,转换成底层网络或文件IO传输的二进制字节流的形式
(3)接收方通过接收到的网络或文件IO读取到二进制字节流进行解码操作,解码成XML格式的数据
(4)接受发进行XML反序列化XML格式的数据回到Java对象
存在问题:
1.使用了太多无用的空间内存区存储与元数据信息,真实的数据其实占用的可能相对并不多
2.相比直接序列化成二进制然后编码的时候就无需再解析字符成二进制字节流的方式而言,这种序列化成XML格式再编码成二进制进行网络传输或文件IO,效率相对较低
优点:
可读性好
23.1.3 JSON的序列化与反序列化
步骤:
(1)发送方把Java对象进行JSON序列化成一种JSON格式的数据
{"name":"sunshuai","age":10}
(2)发送方把该JSON格式的数据进行编码操作,转换成底层网络或文件IO传输的二进制字节流的形式
(3)接收方通过接收到的网络或文件IO读取到二进制字节流进行解码操作,解码成JSON格式的数据
(4)接受发进行JSON反序列化JSON格式的数据回到Java对象
存在问题:
1.使用了太多无用的空间内存区存储与元数据信息,真实的数据其实占用的可能相对并不多
2.相比直接序列化成二进制然后编码的时候就无需再解析字符成二进制字节流的方式而言,这种序列化成XML格式再编码成二进制进行网络传输或文件IO,效率相对较低
优点:
1.可读性好
2.Http协议+JSON序列化/反序列化 ----》SpringCloud解决方案
23.1.4 msgpack的序列化与反序列化
步骤:
(1)发送方把Java对象进行msgpack序列化成一种特殊的二进制格式的数据
(2)发送方把该二进制格式的数据进行编码,由于已经是二进制格式的数据了,所以无需字符的解析和转换,所以效率相对JSON,XML很快。底层通过网络或文件IO传输这二进制字节流给接收方
(3)接收方通过接收到的网络或文件IO读取到二进制字节流进行解码操作,解码成特殊的二进制格式的数据
(4)接收方把该特殊的二进制格式的数据进行反序列化成Java对象
存在问题:
序列化的数据格式为二进制格式,可读性差
优点:
1.数据体量小:相对JSON,XML,这种直接序列化成二进制格式的方式更加压缩节省了许多不必要的空间,但是带来的弊端就是可读性变差
2.传输效率高:数据体量小,传输效率自然提升
3.序列化或反序列化时间短:序列化后就是二进制格式,编码时无需再进行字符的解析,序列化时间大大缩短。解码反序列化同理
补充:
号称是protobuf速度的4倍,但是并没有广泛推行使用。
23.1.5 protobuf的序列化与反序列化
步骤:
同理msgpack
关于protobuf的分析见下
Hadoop对于对象的序列化与反序列化就是使用该方式
23.1.6 对比Protobuf与JSON
23.1.7 序列化有啥好处?
23.2 具体的编解码器
ByteToMessageDecoder --- 实际开发解码器 就作为这个类型的子类
MessageToByteEncoder --- 实际开发编码器 就作为这个类型的子类
24.Netty常见的编解码器
24.1 StringDecoder,StringEncoder
可以进行字符串的编解码转换。但是这里有注意点,我们来看下源码:
我们看到这个StringDecoder的继承关系树,它是直接继承MessageToMessageDecoder这个类,而整个树上面都没有我们之前一直吹嘘的ByteToMessageDecoder这个万法归一的解码器,StringEncoder也是一样的。也没有这个东西的继承关系,那么又引出一个概念。
netty的编解码体系是有两套的:
1.就是我们前面说的ByteToMessageCodec,ByteToMessageDecoder,MessageToByteEncoder这个编解码体系
2.就是我们这里的StringDecoder和StringEncoder继承的MessageToMessageDecoder和MessageToMessageEncoder这个编解码体系
那么问题来了,为什么要搞两套体系呢?它们的区别是啥?
对于ByteToMessageDecoder或MessageToByteEncoder这个体系而言,这个是更加底层的
如下:ByteToMessageDecoder类,是直接也必须与ByteBuf对接,decode重写方法的参数类型写死成ByteBuf了。意味着ByteBuf读取socket内核缓冲区的字节流数据后,decode解码直接拿到的是该ByteBuf类型的数据进行解码,解码成你想要的数据类型。
MessageToByteEncoder同理:
这个只不过是编码。编码调用encode方法,把xxx数据类型的数据经过重写的encode编码后直接转换成ByteBuf类型的数据
对于MessageToMessageDecoder或MessageToMessageEncoder这个体系而言,这个是更加上层一些:
MessageToMessageDecoder:
MessageToMessageEncoder:
可以看出这个体系不是强制让你对ByteBuf类型数据进行decode解码,也不强制要求你encode把数据编码成ByteBuf类型。相对而言比较上层一些。
它们之间有何关系?
以MessageToMessageEncoder<I>的encode和MessageToByteEncoder<I>为例说明:
假设MessageToMessageEncoder<I>为:MessageToMessageEncoder<String>,经过encode方法后,out输出为List<User> out。
最终把这个List<User> out输出的结果交给MessageToByteEncoder进行操作,自然MessageToByteEncoder<I>为MessageToByteEncoder<User>,调用该类的encode方法,最终转换成ByteBuf类型字节流数据传递给socket内核缓冲区。
它们的关系不言而喻,其实就是一个可以互相协作的关系,上下层的一个关系
回过头来看StringDecoder与StringEncoder:
我们上面也可以看到MessageToMessageDecoder是泛型,泛型可以为任意类型,自然这个类型可以是ByteBuf类型,实际上它也可以处理ByteBuf类型的,比如:StringDecoder类
public class StringDecoder extends MessageToMessageDecoder<ByteBuf>
此时产生一个疑问:那么既然你都有一种泛型是ByteBuf类型,导致这都一样了,区别还有什么呢?这是不是就意味着MessageToMessageDecoder<T>/Encoder体系可以完全取代MessageToByteDecoder<T>体系了呢?
当然不是,它们还是有区别的,MessageToMessageDecoder是不解决封帧的问题的,意思就是没有解决半包粘包的问题,而ByteToMessageDecoder体系是自带解决这个问题的。
所以他既然不解决这个问题,你就要解决,也就是你要是使用StringDecoder这个体系,你就要加那些帧编解码器来处理半包黏包。举个例子。
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 我们这里使用一个定长的解码器,来处理半包和黏包问题
pipeline.addLast(new FixedLengthFrameDecoder(10));
pipeline.addLast(new StringDecoder());
// 这里使用一个日志的输出处理器
pipeline.addLast(new LoggingHandler());
}
当你用pipeline.addLast(new StringDecoder());来处理的时候,就必须要前面加上我们说的那几种解码器之一来解决半包黏包,不然必然会出现半包黏包问题,而因为StringDecoder所属的
MessageToMessageDecoder体系不解决半包黏包问题,所以你必须在前面加上来解决这个问题(你可以任选一个,但是总得加一个)。所有的MessageToMessage都要加这个半包黏包的处理器,任加一种,但是你得加,他本身处理不了,所以你需要考虑的情况问题就变多了哈。所以实际开发尽量少用,因为他的功能不是很全,尽量用ByteToMessageCodec来解决。
24.2 上面说的四种封帧的编解码器(其实都是一种解码器)
一定注意:封帧没有编码器,客户端无需编码,想怎么发数据就怎么发数据!
Netty为我们提供的四种封帧解码器都是继承ByteToMessageDecoder的解码器类
如下:以一个为例展示一下类结构图
24.3 java序列化相关的编解码器:ObjectDecoder,ObjectEncoder
它能直接处理java对象的编解码。在nnetty的体系中你要是想处理java对象的编解码序列化反序列化就不用调用inputstream或者outputstream了,在netty中存在一组处理java对象的。也就是ObjectDecoder 与 ObjectEncoder。
他在原有的java传输做了编解码的优化,但是还是基于的Serializable机制,这个优化会导致传输的数据量比原生的JDK要更小。具体是啥后面再研究TODO。
我们看到他的这个直接就集成了LengthFieldBasedFrameDecoder,也就是他自己就能解决半包黏包。就能知道他自己就能解决封帧问题。所以在处理java序列化的时候他就不会有半包黏包,不会出现你的对象过来接收了一半这种情况,他底层为你继承了解决半包黏包的处理。
细节分析一下ObjectDecoder的decode方法与ObjectEncoder的encode方法:
ObjectEncoder的encode方法
ObjectDecoder的decode方法
它们之间如何协作的:
补充:
序列化对象成为一种特殊的格式,这种格式可以是JSON字符串,XML,甚至可以是二进制。如果直接序列化成二进制字节流的形式,就像上面这种java序列化相关的编解码,就是直接序列化成二进制字节流的形式,有什么好处?最显而易见的好处肯定就是:编码的时候无需再对字符进行解析,因为序列化后就已经是二进制字节流的格式啦。还有一点好处就是:二进制可以尽可能的压缩空间,去除不必要的字符,符号,空格。
看看代码:
- 客户端
package com.messi.netty_core_02.netty08;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
public class ObjectNettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
//加一个对象编码器
pipeline.addLast(new ObjectEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush(new User(1,"leomessi"));
group.shutdownGracefully();
}
}
- 服务端
package com.messi.netty_core_02.netty08;
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.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ObjectNettyServer {
private static final Logger log = LoggerFactory.getLogger(ObjectNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap() ;
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加一个java对象解码器
pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
//添加输入处理器
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
User user = (User) msg;
log.info("客户端传过来的对象user为:{}",user);
}
});
//这里使用一个日志的输出处理器
pipeline.addLast(new LoggingHandler());
}
});
serverBootstrap.bind(8000);
}
}
- 运行结果
客户端输出:
服务端输出:
24.4 json相关的编解码器
netty只提供了json的解码器。叫做JsonObjectDecoder。
因为json本身就是字符串,它本身就是字符串,你直接按照字符串处理发送就可以了。
序列化就是把对象做成一种有格式的结构,比如json本身就是,你把User对象转为json字符串格式的过程就是把对象进行JSON序列化。
编码如下:
- 客户端
package com.messi.netty_core_02.netty08.json序列化方式;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class JsonNettyClient {
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
//把java对象序列化成json格式结构,然后封装为ByteBuf发出去
//序列化java对象成为Json格式时,有很多方式可以选择,如:gson,jackson等
User user = new User(1, "leo");
//使用jackson提供的接口,把java对象序列化成JSON格式的字符串: userJSON
ObjectMapper objectMapper = new ObjectMapper();
String userJSON = objectMapper.writer().writeValueAsString(user);
//把JSON格式字符串封装成ByteBuf,通过网络IO或文件IO发出去即可
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeCharSequence(userJSON, Charset.defaultCharset());
//通过网络IO发出去
channel.writeAndFlush(buffer);
group.shutdownGracefully();
}
}
- 服务端
package com.messi.netty_core_02.netty08.json序列化方式;
import com.messi.netty_core_02.netty08.User;
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.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JsonNettyServer {
private static final Logger log = LoggerFactory.getLogger(JsonNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.channel(NioServerSocketChannel.class);
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new JsonObjectDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
User user = (User) msg;
log.info("客户端传过来的对象user为:{}",user);
}
});
pipeline.addLast(new LoggingHandler());
}
});
serverBootstrap.bind(8000);
}
}
客户端输出:
服务端输出:
出现异常,结合异常可以分析出,此时回调传递过来的参数msg类型为ByteBuf,自然无法强转成User类型
此时出现了大大的疑惑,new JsonObjectDecoder()这一解码器没有解码成JSON格式吗?没有反序列化转化成User对象格式吗?
- 修改一下代码重新测试
package com.messi.netty_core_02.netty08.json序列化方式;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
public class JsonNettyServer {
private static final Logger log = LoggerFactory.getLogger(JsonNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.channel(NioServerSocketChannel.class);
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// pipeline.addLast(new JsonObjectDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// User user = (User) msg;
//拿到二进制字节流
ByteBuf byteBuf = (ByteBuf) msg;
//解码转化成JSON字符串格式
String userJson = byteBuf.toString(Charset.defaultCharset());
log.info("客户端传过来的对象user为:{}",userJson);
// log.info("客户端传过来的对象user为:{}",user);
}
});
pipeline.addLast(new LoggingHandler());
}
});
serverBootstrap.bind(8000);
}
}
结果:
客户端发送成功:
服务端输出成功:
一定注意,是解码后才转换成了一个字符串格式:
输出一个字符串,本来JSON就是一个字符串格式数据
24.4.1 所以说:Json解码器到底有什么用?
我们看到这玩意作为解码器,纯粹是个five,那么netty给你这个解码器是干嘛的呢?他不是用来把二进制字节流解码成JSON字符串,他也不是用来把JSON字符串反序列化为对象的,而是针对json数据做封帧的。
也就是对json数据做半包黏包的,他会给你把你的json做半包黏包处理让你拿到一个完整的客户端传过来的json,而不是一半或者多的。但是这个结果他依然是放在ByteBuf里面的,这就导致出现我们上面的情况。而我们来测试一下他的封帧作用。
- 没有JSON解码器的服务端,制造半包场景
package com.messi.netty_core_02.netty08.json序列化方式;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
public class JsonNettyServer {
private static final Logger log = LoggerFactory.getLogger(JsonNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.channel(NioServerSocketChannel.class);
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
//设置接收端每个连接对应的ByteBuf的内存大小,最小,初始默认值,最大值都设置为16字节
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// pipeline.addLast(new JsonObjectDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// User user = (User) msg;
// String userJson = (String) msg;
ByteBuf byteBuf = (ByteBuf) msg;
//解码成JSON字符串格式
String userJson = byteBuf.toString(Charset.defaultCharset());
log.info("客户端传过来的对象user为:{}",userJson);
// log.info("客户端传过来的对象user为:{}",user);
}
});
pipeline.addLast(new LoggingHandler());
}
});
serverBootstrap.bind(8000);
}
}
- 客户端依旧
package com.messi.netty_core_02.netty08.json序列化方式;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class JsonNettyClient {
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
//把java对象序列化成json格式结构,然后封装为ByteBuf发出去
//序列化java对象成为Json格式时,有很多方式可以选择,如:gson,jackson等
User user = new User(1, "leo");
//使用jackson提供的接口,把java对象序列化成JSON格式的字符串: userJSON
ObjectMapper objectMapper = new ObjectMapper();
String userJSON = objectMapper.writer().writeValueAsString(user);
//把JSON格式字符串封装成ByteBuf,通过网络IO或文件IO发出去即可
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeCharSequence(userJSON, Charset.defaultCharset());
//通过网络IO发出去
channel.writeAndFlush(buffer);
group.shutdownGracefully();
}
}
- 输出
客户端正常发出:
服务端接收数据,出现半包:
这就是出现了半包情况,而你加上这个json解码器就可以了,然后我们说他解决这个json的问题实际上
是入栈处理成对大括号的方法。如果你不做这个解码器,那就得用前面说的帧解码器,这就得约束偏移
量这些工作了。你要是用固定字符截取,不太行,因为json里面啥都有可能,你的那个拆分很可能把
json拆了。 而ObjectCoder则是传输前后写的是对象结构,序列化的时候知道对象结构,所以可以以整个对象做封帧处理。
24.4.2 服务端如何正确的获取到User对象?
- 客户端
package com.messi.netty_core_02.netty08.json序列化方式;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
public class JsonNettyClient2 {
public static void main(String[] args) throws JsonProcessingException, InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
User user = new User(10,"leo");
//序列化:把User对象转换成一种特殊的格式。这种格式为:JSON字符串 。使用的第三方技术:jackson
ObjectMapper objectMapper = new ObjectMapper();
String userJSON = objectMapper.writer().writeValueAsString(user);
//编码:把序列化后的特殊格式数据转换成二进制字节流形式存储到ByteBuf中
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeCharSequence(userJSON, Charset.defaultCharset());
//把编码后的数据写出到网络IO
channel.writeAndFlush(buffer);
group.shutdownGracefully();
}
}
- 服务端代码修改【自动解码+自动反序列化】
package com.messi.netty_core_02.netty08.json序列化方式;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.messi.netty_core_02.netty08.User;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.json.JsonObjectDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
public class JsonNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(JsonNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
//封帧操作:解决ByteBuf的半包粘包问题
pipeline.addLast(new JsonObjectDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//回调channelRead方法:netty会把网络IO接收到的二进制字节流数据存储到ByteBuf中
ByteBuf byteBuf = (ByteBuf) msg;
//解码:把ByteBuf中二进制字节流数据转换成JSON字符串格式
String JSONString = byteBuf.toString(Charset.defaultCharset());
//反序列化:把JSON字符串数据转换成User对象,使用的第三方技术:jackson
ObjectMapper objectMapper = new ObjectMapper() ;
User user = objectMapper.readValue(JSONString, User.class);
log.info("解码后的JSON字符串格式为: {}",JSONString) ;
log.info("反序列化后转换成User对象为:{}",user) ;
super.channelRead(ctx,user);
}
});
}
});
serverBootstrap.bind(8000);
}
}
输出:
客户端发送成功:
服务端接收成功:
24.4.3 总结
补充1:
为什么不能直接转递Java对象给服务端,非得序列化之后才能传输?
首先这个问题也是建立在客户端与服务端两端都使用的是Java语言开发,如果都是使用Java语言开发,那么两端分别是两台不同的JVM,不同的JVM它肯定不认可你传递过来的Java对象的,所以只能序列化成一种格式,服务端拿到你这种格式的数据再根据固定的反序列化方式生成Java对象。
如果两端是不同语言,需要涉及到跨语言传输对象的话,那么需要更高级的第三方序列化与反序列化方式来构建了,比如说:protobuf序列化协议方式
补充2:
提亮日志某些特定的行:
看一下:
24.5 Http协议
1.
WebSocket双向通信协议,有时候一些推送功能,不是前端请求后端资源的,而是后端服务器主动推送给前端服务器资源数据的。这个时候就需要使用WebSocket做双向通信。
2.
ip是服务器计算机的唯一标识
端口号是计算机上进程的唯一标识
通过ip找到浩瀚互联网中的某一台服务器计算机,再通过端口号找到这台计算机的对应的进程(进程对应的就是服务器软件)
统一资源标识符是在当前进程中找到特定对应的服务器资源
3.
4.剖析http协议对应的请求URL地址
URL英文全称:Uniform Resource Locator,中文直译为统一资源定位符。是互联网(web,移动)中,访问,操作功能(资源)的方式
URL的目的在于访问,操作互联网上的功能,或者资源
(1)什么是互联网上的功能?
就是网站(Web应用)提供的功能,资源
(2)所谓访问,操作互联网上的功能或资源
本质上就是找到web应用,并访问这个web应用的功能,或者资源
5.如何找到一个web应用上的某一具体的资源?
如下图:
(1)如何找到一个web应用的具体某个资源?
第一步:通过http协议对应的请求URL中的ip地址找到目标计算机服务器
第二步:通过http协议对应的请求URL中的port端口号找到目标进程程序(找到Tomcat软件)
通过http协议请求URL地址中的URI去找某个具体的资源:
(3)通过/项目名找到具体某一个web应用
(4)再根据项目名后面的资源名找到该web应用中具体某一个资源
举一个例子:http://localhost:8989/springweb2/UserController/addUser
(1)解析该http协议,通过host:localhost这一ip地址找到对应的服务器
(2)通过端口号找到该服务器上对应的Tomcat软件进程
(3)通过springweb2这一项目名找到该Tomcat软件进程中的某一具体的项目
(4)通过/UserController/addUser找到该springweb2项目中真正具体的资源进行访问执行
7.客户端请求服务端,发送的是请求报文
请求报文包含三部分:1.请求行 2.请求头 3.请求体(请求数据,但是当为Get请求时 请求数据携带在请求行)
请求行:
(1)请求方式(GET,POST,DELETE,PUT,HEAD,TRACE,CONNECT,OPTIONS)
(2)URL(如:http://localhost:8989/springweb2/UserController/addUser)
(3)协议版本(常用的有HTTP1.0,HTTP1.1)
请求头:
因为客户端宇服务器之间彼此不认识,所以需要通过请求头告知服务器一些附加的描述信息,这样可以更加有利于服务器了解client相关的情况
请求头具体字段如下:
User-Agent:发送请求的应用程序名(浏览器版本,OkHttp)
Content-Length:请求发送的数据大小 【很重要,解决半包粘包问题】
Accept-Encoding:通知服务器就可以发送的数据压缩格式 gzip
Accept-Language:通知服务器可以发送的语言
Cookie:cookie的数据,是通过请求头,提交到服务器端
Content-Type:告知服务器,我现在发送的数据是什么类型的,便于服务器处理。举个例子:如果该值等于application/json,意味着告知服务器我发送的是json数据。如果该值等于application/x-www-form-uriencoded告知服务器我发送的是表单文本数据。该值的类型统称为:MIME类型,还有更多的类型,如:text/html,text/plain,image/jpg,application/xml
请求体:
客户端向服务器端传递(提交)的数据内容
注意:这里的数据指定是POST方式提交的数据,而不是GET方式。GET方式是通过请求报文的请求行中携带传递数据的
补充:
1.浏览器发送请求时,会默认指定一些请求头信息
2.可以根据前端js代码,设置个性化的请求头信息
3.Http1.1协议:是有限的长连接
为什么提出Http1.1协议?为了提升通信的效率
长连接的优缺点:
优点:不会像短连接那样,频繁的连接断开造成性能消耗。频繁的连接断开其实就是频繁的进行三次握手和四次挥手,很浪费性能
缺点:长连接会占用服务器的socket资源,长时间不断开的话会极大影响服务端的吞吐并发量。所以Http1.1协议设计的有限的长连接,不是无限制的长连接。=
8.服务端响应客户端,发送的是响应报文
响应报文包含三部分:1.状态行 2.响应头 3.响应体(响应正文)
状态行
(1)协议版本
(2)状态码
(3)文本描述
响应头
因为客户端和服务器彼此之间相互不认识,所以需要通过响应头来告知客户端一些附加的描述信息。这样就可以更加有利于客户端更加正确的处理服务器端响应的数据。、
响应头具体字段如下:
Server:服务器软件的名字
Content-Length:服务器响应内容的大小
Content-Type:服务器响应数据的类型,便于浏览器处理。例如:text/html,application/json,image/jpg等
Set-Cookie:发送Cookie
Expries
Cache-Control:缓存相关
Pragma
注释:关于响应头的字段值,都是可以通过原生的Servlet-api进行设置的,如:response.setContentType("text/html;charset=UTF-8"); 其实就是通知Tomcat软件要把响应头的Content-Type字段的值设置成text/html;charset=UTF-8,表示服务器响应数据的类型,便于浏览器客户端进行处理
响应体(响应正文)
指的就是服务器响应的内容,如:HTML,JSON等
24.6 Http协议的编解码操作
Netty是支持对于http协议的编解码的,http是一个协议,在应用层使用这个协议也要对发送的数据做http的编码,netty也支持了对于这个编码的解码操作,也就是说netty可以用作http-web服务器,用来处理http请求。比如说像gateWay网关,gateway网关收发http请求然后转发给后台服务器,这个后续做自定义API网关的时候要好好总结。
大致过程:
我们使用浏览器来充当客户端,发送请求,浏览器使用的客户端请求就是http请求。
服务端代码使用netty来开发。
- 第一版服务端代码
package com.messi.netty_core_02.netty09;
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.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpNettyServer {
private static final Logger log = LoggerFactory.getLogger(HttpNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap() ;
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//这里使用一个日志的输出处理器
pipeline.addLast(new LoggingHandler());
/**
* pipeline.addLast(new HttpServerCodec())等价于下面两句代码!
* pipeline.addLast(new HttpRequestDecoder()):
* [服务端接收请求时,解码ByteBuf中的二进制流成一种特殊的格式,反序列化这一特殊格式转换为HttpObject对象]
* pipeline.addLast(new HttpRequestEncoder()):
* [服务端提供响应时,序列化HttpObject成一种特殊格式,编码这一格式成二进制资源流存储到ByteBuf中发出]
*/
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg为:{}",msg) ;
}
});
}
});
//监听8000端口,客户端浏览器访问时,访问http://ip:8000/xxx
serverBootstrap.bind(8000);
}
}
启动该服务端,浏览器发送请求,http://localhost:8000/leomessi,leomessi就是一个携带信息,你可以携带别的。
我们来看一下输出:
然后我们看到这个输出有两个问题:
1、msg的信息输出了两次
msg为:DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
msg为:EmptyLastHttpContent
2、两次的输出对象类型不一样,一个是DefaultHttpRequest,一个是EmptyLastHttpContent
首先他输出两次这个就很难绷,我们说第一个其实是HttpRequest是请求头的信息,HttpContent是请求体的内容,他是分两部分发过来的。既然是这样,那我们就要加强一下我们的代码,不然可能会有异常。
- 第二版服务端代码:
package com.messi.netty_core_02.netty09;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(HttpNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup() ;
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg为:{}",msg) ;
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
HttpHeaders headers = request.headers();
log.info("请求头信息为:{}",headers) ;
String uri = request.uri();
log.info("请求行信息为:{}",uri) ;
HttpVersion httpVersion = request.protocolVersion();
log.info("http版本信息为:{}",httpVersion);
} else if (msg instanceof HttpContent) {
//因为get不通过请求体传递数据而是通过url的请求行传递数据,所以HttpContent为空,它的类型为EmptyLastHttpContent
HttpContent httpContent = (HttpContent) msg;
//获取到ByteBuf的数据,然后你就可以对ByteBuf处理了
ByteBuf content = httpContent.content();
//后续业务处理.....
}
super.channelRead(ctx, msg);
}
});
}
});
serverBootstrap.bind(8000);
}
}
浏览器发送:http://localhost:8000/hellonetty
输出结果为:
同样,handler执行两次
- 第三版服务端代码
package com.messi.netty_core_02.netty09;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
public class HttpNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(HttpNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup() ;
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg为:{}",msg) ;
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
HttpHeaders headers = request.headers();
log.info("请求头信息为:{}",headers) ;
String uri = request.uri();
log.info("请求行信息为:{}",uri) ;
HttpVersion httpVersion = request.protocolVersion();
log.info("http版本信息为:{}",httpVersion);
//Get请求由于数据没在请求体中携带过来,是在请求行中携带过来的 所以在这里响应
//这里使用DefaultFullHttpResponse而不是DefaultHttpResponse,因为full这个会把你的响应头和响应体一起返回
//DefaultHttpResponse则是把响应头和响应体分开的,你得发两次,也就是像我们看到的那样:发过来时分两条Message,执行两次handler
DefaultFullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK) ;
//响应体内容
byte[] content = "<h1>Hello Netty<h1>".getBytes();
//设置响应头的长度,告诉浏览器或客户端,我们的数据长度是多少,它们就知道该读取多少数据内容了
//等同于之前开发web时候的response.setHeader(Content-Length,content.length);
response.headers().set(CONTENT_LENGTH,content.length) ;
//数据写入
response.content().writeBytes(content);
//刷新写出到socket内核缓冲区 通过网络IO发给客户端浏览器渲染出来
ctx.writeAndFlush(response);
} else if (msg instanceof HttpContent) {
//因为get不通过请求体传递数据而是通过url的请求行传递数据,所以HttpContent为空,它的类型为EmptyLastHttpContent
HttpContent httpContent = (HttpContent) msg;
//获取到ByteBuf的数据,然后你就可以对ByteBuf处理了
ByteBuf content = httpContent.content();
//后续业务处理.....
}
super.channelRead(ctx, msg);
}
});
}
});
serverBootstrap.bind(8000);
}
}
输出:
再次在浏览器发起请求:就如实返回了。
而我们不管直接使用Servlet开发还是tomcat开发,其实都是这么处理的,只是它为你封装了很多东西,Tomcat在你返回响应的时候,很多参数都是给你设置好了,但是你原生使用netty处理,需要写很多内容,因为你要构建http协议的一些参数和内容
这就是Http协议的编解码处理器的逻辑
- 为什么msg发了两次?
因为一个http请求过来,http请求就是分为请求头和请求体
http请求到达netty服务端这边后,被解码器拿到,解码器就是HttpServerCodec,我们看一下它的源码:
public final class HttpServerCodec extends
CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodec
首先它集成了CombinedChannelDuplexHandler<HttpRequestDecoder,HttpResponseEncoder>这个类,拥有了HttpRequestDecoder对于http请求过来时的解码能力,也拥有了HttpResponseEncoder对于想要封装响应数据时的编码能力
这里我们聊得是解码,所以我们去看HttpRequestDecoder。他最终会继承到HttpObjectDecoder然后再继承ByteToMessageDecoder这个类
我们来看ByteToMessageDecoder的解码decode方法:
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
- 上述decode是个抽象方法,所以必然在子类实现了,我们去看HttpObjectDecoder作为子类是如何实现decode方法的?
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out)
注释:
1.ctx:上下文对象
2.buffer:ByteBuf对象,也就是客户端通过网络IO传输过来的请求数据都存储在该参数中,也就是包含了请求头和请求体的所有数据
3.out:存储请求内容产生的结果,是一个集合,可见decode方法会处理buffer这一ByteBuf数据,然后把数据处理分为多个,把请求头和请求体分别划分成一个message,一共两个message都给存储到out这一集合中。
后续遍历这一集合,取出每一个message都会走完一整套pipline,执行pipline里面所有的handler,也就是我们观察到的结果了,实际上不是发了两次请求,而是发了一次请求,只不过是解析成两个message(一个请求头,一个请求体),所以分别走了一次channelRead逻辑。这是一种合理的设计,因为Netty以消息为单位,也就是做到了统一处理,无视你各种上层的封装,到了这里后进行统一逻辑处理。
这个可以在断点看一下执行次数和链路。
前面我们的半包也是一样,其实就是他一次不够拿,拿了多次,然后多次每一个都要走一遍pipline处理链路。
- 针对拆分请求为多个message的第一次handler优化(SimpleChannelInboundHandler)
我们上面的服务端逻辑通过判断msg的类型来进行不同的处理,太麻烦了,这里简化一下,使用一个新的handler读取来简化这个操作。
package com.messi.netty_core_02.netty09;
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.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
public class HttpSimpleNettyServer {
private static final Logger log = LoggerFactory.getLogger(HttpSimpleNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.channel(NioServerSocketChannel.class);
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new HttpServerCodec());
//其实就是把之前判断的工作交给别人去做了,自己只需要传递一个泛型即可
pipeline.addLast(defaultEventLoopGroup,new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
HttpRequest request = msg;
HttpHeaders headers = request.headers();
log.info("请求头信息为:{}",headers) ;
String uri = request.uri();
log.info("请求行信息为:{}",uri) ;
HttpVersion httpVersion = request.protocolVersion();
log.info("http版本信息为:{}",httpVersion);
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
byte[] content = "<h1>hello netty<h1>".getBytes();
response.headers().set(CONTENT_LENGTH,content.length);
response.content().writeBytes(content);
ctx.writeAndFlush(response);
}
});
}
});
serverBootstrap.bind(8000);
}
}
输出:
分析:
- 针对拆分请求为多个message第二次Handler优化(FullHttpRequest)
之前我们为了避免多次处理判断类型,我们采用了Simple这种方式来指定消息类型。还有一种是我们可以直接把解码器拆解为两部分的:HttpRequest和HttpContent聚合起来成为一个,这里需要引入一个新的处理器:HttpObjectAggregator类
package com.messi.netty_core_02.netty09;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
public class HttpFullNettyServer {
private static final Logger log1 = LoggerFactory.getLogger(HttpFullNettyServer.class);
private static final Logger log = LoggerFactory.getLogger(HttpFullNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.channel(NioServerSocketChannel.class);
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new HttpServerCodec());
/**
* 限定http发送的最大长度为:1024字节,超过了就不再接受了,前面的封帧也限制
* 这个聚合处理会把上面解码完了的两个HttpMessage聚合成为一个FullHttpRequest(其实是子类,AggregatedFullHttpRequest)
* AggregatedFullHttpRequest这里面就是全部的了,你可以一起处理你的请求头和请求体
*/
pipeline.addLast(new HttpObjectAggregator(1024));
//其实就是把之前判断的工作交给别人去做了,自己只需要传递一个泛型即可
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("msg为:{}",msg);
FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
//请求头
HttpHeaders headers = fullHttpRequest.headers();
log.info("请求头信息为:{}", headers);
//请求体
ByteBuf content = fullHttpRequest.content();
log.info("请求体信息为:{}", content);
DefaultFullHttpResponse response = new DefaultFullHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.OK);
byte[] content2 = "<h1>hello netty epoll<h1>".getBytes();
response.headers().set(CONTENT_LENGTH, content2.length);
response.content().writeBytes(content2);
ctx.writeAndFlush(response);
}
});
}
});
serverBootstrap.bind(8000);
}
}
输出:
- Http协议的解码器,会出现半包粘包问题吗?
在netty中设计的编解码器类,只要是ByteToMessage的子类,一般都没有半包粘包的问题,要么就是继承了封帧解码器,要么就是像Http协议这种:头体分离,记录了消息的长度Content-Length,根据这一长度我们可以进行解决半包粘包问题。
但是对于我们自定义的类,即使是ByteToMessage的子类,我们也要自己考虑半包粘包的问题。
详细分析总结:
不会,因为请求体过来的时候,http设置了Content-length这个长度本身就是告诉了服务端,他能处理,何况,他还是ByteToMessage的子类,netty在继承这个类的时候就解决了这问题。如果是我们写得时候继承ByteToMessage也要考虑这个,并非继承了就能解决半包黏包,是netty继承之后netty自己就解决了。netty的体系中,他一般是继承了就去解决。
http协议的他是因为协议本身就携带了消息的长度和头体分离封装了,所以就服务端就能读到,当然这
个读到也是netty读到之后还需要根据这个长度来解决,不是读到就完了