Netty 高级应用

1. 编解码器

概念:在网络应用中,需要实现某种编解码器。将原始字节数据与自定义消息数据进行相互转换。网络中都是以字节码的形式传输的。

对Netty而言,编解码器由两部分组成:编码器、解码器

  • 编码器:将消息对象转为字节或其他序列形式在网络上传输
  • 解码器:负责将字节或其他序列形式转为指定的消息对象

Netty的编解码器实现了ChannelHandlerAdapter,也是一种特殊的ChannelHandler,所以依赖与ChannelPipeline,可以将多个编解码器链接在一起,以实现复杂的转换逻辑。

  1. 解码器
  • ByteToMessageDecoder:用于将字节转为消息,需要检查缓冲区是否有足够的字节
  • ReplayingDecoder:继承ByteToMessageDecoder,不需要检查缓冲区是否有足够的字节,但是ReplayingDecoder速度略慢于ByteToMessageDecoder,同时不是所有的ByteBuf都支持。项目复杂性高则使用ReplayingDecoder,否则使用ByteToMessageDecode
  • MessageToMessageDecoder:用于从一种消息解码为另一种消息(如POJO到POJO)

解码器示例:

public class DemoDecoder extends MessageToMessageDecoder<ByteBuf> {

    @Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
String msg = byteBuf.toString(CharsetUtil.UTF_8);
list.add(msg);
}
}

通道里加入解码器:

 protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new DemoDecoder());
socketChannel.pipeline().addLast(new DemoNettyServerHandle());
}
  1. 编码器
  • MessageToByteEncoder:将消息转为字节
  • MessageToMessageEncoder:用于从一种消息编码为另外一种消息(例如POJO到POJO)

编码器示例:

public class DemoEncoder extends MessageToMessageEncoder<String> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, String s, List<Object> list) throws Exception {
list.add(Unpooled.copiedBuffer(s,CharsetUtil.UTF_8));
}
}
  1. 编码解码器Codec

同时具备编码与解码功能

  • ByteToMessageCodec
  • MessageToMessageCodec

2. 基于Netty的HTTP服务器开发

效果如图:

代码如下:

public class NettyHttpServer {

    private int port;

    public NettyHttpServer(int port) {
this.port = port;
} public static void main(String[] args) {
new NettyHttpServer(8090).run();
} public void run(){
EventLoopGroup bossGroup=null;
EventLoopGroup workerGroup=null;
try{
bossGroup=new NioEventLoopGroup(1);
workerGroup=new NioEventLoopGroup();
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码器
socketChannel.pipeline().addLast(new HttpServerCodec());
socketChannel.pipeline().addLast(new NettyHttpServerHandler()); }
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    @Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
if(httpObject instanceof HttpRequest){
DefaultHttpRequest request=(DefaultHttpRequest)httpObject;
if(request.uri().equals("/favicon.ico")){
//图标不响应
return;
}
System.out.println("接收到请求:"+request.uri());
ByteBuf byteBuf = Unpooled.copiedBuffer("你好,我是服务端", CharsetUtil.UTF_8);
DefaultFullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK,byteBuf);
//设置响应头
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
channelHandlerContext.writeAndFlush(response);
}
}
}

3. 粘包和拆包

简介:粘包和拆包是TCP网络编程中不可避免的,无论客户端还是服务端,当我们读取或发送消息的时候都要考虑TCP底层的粘包/拆包机制。

粘包产生的原因:

  • 应用程序写入的数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上
  • 接收方不及时读取套接字缓冲区数据
  • TCP默认使用Nagle算法,将小数据包合并

拆包产生的原因:

  • 数据太大超过剩余缓冲区的大小
  • 数据太大超过MSS最大报文长度

粘包和拆包的解决方案

  1. 消息长度固定,累计读取到定长的报文就认为是一个完整的信息
  2. 将换行符作为消息结束符
  3. 将特殊的分隔符作为消息的结束标志
  4. 通过在消息头中定义长度字段来标识消息总长度

Netty中粘包和拆包的解决方案

Netty提供了4种解码器来解决:

  1. 固定长度拆包器FixedLengthFrameDecoder
  2. 行拆包器LineBasedFrameDecoder,以换行符作为分隔符
  3. 分隔符拆包器DelimiterBasedFrameDecoder,通过自定义的分隔符进行拆分
  4. 基于数据包长度的拆包器LengthFieldBasedFrameDecoder,将应用层数据包的长度最为拆分一句。要求应用层协议中包含数据包的长度。

DelimiterBasedFrameDecoder示例:

ByteBuf byteBuf =
Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));

LengthFieldBasedFrameDecoder构造器参数讲解:

public LengthFieldBasedFrameDecoder(

ByteOrder byteOrder,

int lengthFieldOffset,

int lengthFieldLength,

int lengthAdjustment,

int initialBytesToStrip,

boolean failFast)

  • byteOrder是指明Length字段是大端序还是小端序,因为Netty要读取Length字段的值,所以大端小端要设置好,默认Netty是大端序ByteOrder.BIG_ENDIAN。

  • maxFrameLength是指最大包长度,如果Netty最终生成的数据包超过这个长度,Netty就会报错。

  • lengthFieldOffset是指明Length的偏移位

  • lengthFieldLength是Length字段长度

  • lengthAdjustment 这个参数很多时候设为负数,这是最让小伙伴们迷惑的。下面我用一整段话来解释这个参数

当Netty利用lengthFieldOffset(偏移位)和lengthFieldLength(Length字段长度)成功读出Length字段的值后,Netty认为这个值是指从Length字段之后,到包结束一共还有多少字节,如果这个值是13,那么Netty就会再等待13个Byte的数据到达后,拼接成一个完整的包。但是更多时候,Length字段的长度,是指整个包的长度,如果是这种情况,当Netty读出Length字段的时候,它已经读取了包的4个Byte的数据,所以,后续未到达的数据只有9个Byte,即13 - 4 = 9,这个时候,就要用lengthAdjustment来告诉Netty,后续的数据并没有13个Byte,要减掉4个Byte,所以lengthAdjustment要设为 -4!!!

  • initialBytesToStrip,跳过的个数。比如这里initialBytesToStrip设置为4,那么Netty就会跳过前4位解析后面的内容

  • failFast 参数一般设置为true,当这个参数为true时,netty一旦读到Length字段,并判断Length超过maxFrameLength,就立即抛出异常。

示例:

 @Override
public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
for (int i=0;i<100;i++){
byte[] bytes = "你好,我是客户端".getBytes(CharsetUtil.UTF_8);
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);
channelHandlerContext.writeAndFlush(byteBuf);
}
}

第2个参数和第三个参数表示:0-4个字节是内容长度字段,第五个参数的4代表跳过前4个字节。

 socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(60535,0,4,0,4));

最后输出的内容:

4. 基于Netty和WebSocket的聊天室案例

1. WebSocket简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。相比HTTP协议,WebSocket具备如下特点:

  1. 支持双向通信,实时性更强
  2. 更好的二进制支持
  3. 较少的开销:协议控制的数据包头部较小

应用场景:

  • 社交订阅
  • 协同编辑
  • 股票基金报价
  • 体育实况更新
  • 多媒体聊天
  • 在线教育

2. 服务端开发

  1. 引入依赖

基于SpringBoot环境

 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <!--添加thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency> <dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.72.Final</version>
</dependency>
  1. 核心后端代码
@Component
public class NettyWebSocketServer implements Runnable { @Autowired
private NettyConfig nettyConfig; @Autowired
private WebSocketChannelInit webSocketChannelInit; private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private EventLoopGroup wokerGroup = new NioEventLoopGroup(); @PreDestroy
public void close(){
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
} @Override
public void run() {
try{
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,wokerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(webSocketChannelInit);
ChannelFuture channelFuture = serverBootstrap.bind(nettyConfig.getPort()).sync();
System.out.println("Netty服务端启动成功");
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
} }
}
@Component
public class WebSocketChannelInit extends ChannelInitializer { @Autowired
private NettyConfig nettyConfig; @Autowired
private WebSocketHandler webSocketHandler; @Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//对http协议的支持
pipeline.addLast(new HttpServerCodec());
//对大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
//post请求分为3部分。request line、request header、body
//HttpObjectAggregator将多个信息转化为单一的request或者response对象
pipeline.addLast(new HttpObjectAggregator(8000));
//将http协议升级为ws协议,websocket的支持
pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath())); pipeline.addLast(webSocketHandler);
}
}
@Component
@ChannelHandler.Sharable //设置通道共享
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { private List<Channel> channels=new ArrayList<>(); @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
channels.add(ctx.channel());
System.out.println("有新的连接了...");
} @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channels.remove(ctx.channel());
System.out.println("连接下线了");
} @Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception { String text = textWebSocketFrame.text();
Channel currentChannel = channelHandlerContext.channel();
for (Channel channel:channels){
//自己不给自己发消息
if(!channel.equals(currentChannel)){
channel.writeAndFlush(new TextWebSocketFrame(text));
}
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
Channel channel = ctx.channel();
channels.remove(channel);
}
}

3. 前端js代码

$(function () {
//这里需要注意的是,prompt有两个参数,前面是提示的话,后面是当对话框出来后,在对话框里的默认值
var username = "";
while (true) {
//弹出一个输入框,输入一段文字,可以提交
username = prompt("请输入您的名字", ""); //将输入的内容赋给变量 name ,
if (username.trim() === "")//如果返回的有内容
{
alert("名称不能输入空")
} else {
$("#username").text(username);
break;
}
} var ws = new WebSocket("ws://localhost:8081/chatService");
ws.onopen = function () {
console.log("连接成功.")
};
ws.onmessage = function (evt) {
showMessage(evt.data);
};
ws.onclose = function (){
console.log("连接关闭")
}; ws.onerror = function (){
console.log("连接异常")
}; function showMessage(message) {
// 张三:你好
var str = message.split(":");
$("#msg_list").append('<li class="active"}>\n' +
' <div class="main">\n' +
' <img class="avatar" width="30" height="30" src="/img/user.png">\n' +
' <div>\n' +
' <div class="user_name">'+str[0]+'</div>\n' +
' <div class="text">'+str[1]+'</div>\n' +
' </div> \n' +
' </div>\n' +
' </li>');
// 置底
setBottom();
} $('#my_test').bind({
focus: function (event) {
event.stopPropagation();
$('#my_test').val('');
$('.arrow_box').hide()
},
keydown: function (event) {
event.stopPropagation();
if (event.keyCode === 13) {
if ($('#my_test').val().trim() === '') {
this.blur();
$('.arrow_box').show();
setTimeout(this.focus(),1000);
} else {
$('.arrow_box').hide();
//发送消息
sendMsg();
this.blur();
setTimeout(this.focus())
}
}
}
});
$('#send').on('click', function (event) {
event.stopPropagation();
if ($('#my_test').val().trim() === '') {
$('.arrow_box').show()
} else {
sendMsg();
}
}); function sendMsg() {
var message = $("#my_test").val();
$("#msg_list").append('<li class="active"}>\n' +
' <div class="main self">\n' +
' <div class="text">'+message+'</div>\n' +
' </div>\n' +
' </li>');
$("#my_test").val(''); //发送消息
message = username + ":" + message;
ws.send(message);
// 置底
setBottom();
} // 置底
function setBottom() {
// 发送消息后滚动到底部
var container = $('.m-message');
var scroll = $('#msg_list');
container.animate({
scrollTop: scroll[0].scrollHeight - container[0].clientHeight + container.scrollTop() + 100
});
}
});

Netty高级应用及聊天室实战的更多相关文章

  1. Netty网络聊天(一) 聊天室实战

    首发地址; Netty网络聊天(一) 聊天室实战 之前做过一个IM的项目,里面涉及了基本的聊天功能,所以注意这系列的文章不是练习,不含基础和逐步学习的部分,直接开始实战和思想引导,基础部分需要额外的去 ...

  2. Netty 仿QQ聊天室 (实战二)

    Netty 聊天器(百万级流量实战二):仿QQ客户端 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之15 [博客园 总入口 ] 源码IDEA工程获取链接:Java 聊天室 实战 源码 写在 ...

  3. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列。开源啦!!!

    自此系列博客开写以来,好多同学关心开源问题,之前由于网络问题,发布到Github上老是失败,今天终于在精简了好多无用的文件之后发布上去了. 注意:layim源代码并不开源,由于版权问题,请大家去官网了 ...

  4. Netty之多用户的聊天室(三)

    Netty之多用户的聊天室(三) 一.简单说明 笔者有意将Netty做成一个系列的文章,因为笔者并不是一个善于写文章的人,而且笔者学习很多技术一贯的习惯就是敲代码,很多东西敲着敲着就就熟了,然后再进行 ...

  5. 20分钟了解Epoll + 聊天室实战

    我们知道,计算机的硬件资源由操作系统管理.调度,我们的应用程序运行在操作系统之上,我们的程序运行需要访问计算机上的资源(如读取文件,接收网络请求),操作系统有内核空间和用户空间之分,所以数据读取,先由 ...

  6. netty系列之:文本聊天室

    目录 简介 聊天室的工作流程 文本处理器 初始化ChannelHandler 真正的消息处理逻辑 总结 简介 经过之前的系列文章,我们已经知道了netty的运行原理,还介绍了基本的netty服务搭建流 ...

  7. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列

    ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言  http://www.cnblogs.com/panzi/p/5742089.html ASP.NET S ...

  8. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列(不断更新中)

    项目简介 利用ASP.NET SignalR技术与Layim前端im框架实现的一个简单的web聊天室,包括单聊,群聊,加好友,加群,好友搜索,管理,群组管理,好友权限设置等功能.涉及技术: Elast ...

  9. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列(内容已过期,阅读请慎重)

    项目简介 利用ASP.NET SignalR技术与Layim前端im框架实现的一个简单的web聊天室,包括单聊,群聊,加好友,加群,好友搜索,管理,群组管理,好友权限设置等功能.涉及技术: Elast ...

随机推荐

  1. mybatis(1.2)

    为什么执行sql语句后 数据库表中不会更新 需要我们手动配置  两种方法 如下: 1:调用SqlSession接口的commit方法 2:获取Session的时候  SqlSessionFactory ...

  2. Python与Javascript相互调用超详细讲解(2022年1月最新)(三)基本原理Part 3 - 通过C/C++联通

    目录 TL; DR python调javascript javascript调python 原理 基于Node.js的javascript调用python 从Node调用python函数 V8 嵌入P ...

  3. RabbitMQ 中的分布式,普通 cluster 模式的构建

    RabbitMQ 如何做分布式 前言 集群配置方案 cluster 普通模式 镜像模式 federation shovel 节点类型 RAM node Disk node 集群的搭建 1.局域网配置 ...

  4. Android Sensor.TYPE_STEP_COUNTER 计步器传感器 步数统计

    注意:使用 计步器传感器 Sensor.TYPE_STEP_COUNTER 获取步数前需要手机支持该传感器 1.学习资料 1.1 SENSOR.TYPE_STEP_COUNTER 地址:开发者文档 翻 ...

  5. golang gin框架中使用protocol buffers和JSON两种协议

    首先,我使用protobuf作为IDL,然后提供HTTP POST + JSON BODY的方式来发送请求. 能不能使用HTTTP POST + PB序列化后的二进制BODY呢? 做了一下尝试,非常简 ...

  6. 【记录一个问题】android ndk下设置线程的亲缘性,总有两个核无法设置成功

    参考了这篇文章:https://blog.csdn.net/lanyzh0909/article/details/50404664 大体的代码如下: #include <pthread.h> ...

  7. Java on Visual Studio Code的更新 – 2022年1月

    大家好,欢迎来到 Visual Studio Code Java 更新 1 月版!这是我们新年的第一篇博客,我们将回顾 2021 年的亮点,并分享我们 2022 年的产品路线图!除此之外我们还有一些令 ...

  8. 机器学习-softmax回归 python实现

    ---恢复内容开始--- Softmax Regression 可以看做是 LR 算法在多分类上的推广,即类标签 y 的取值大于或者等于 2. 假设数据样本集为:$\left \{ \left ( X ...

  9. 在海外上传文件到中国AWS S3

    s3cmd --access_key= --secret_key=xxxx --region=cn-north-1 --host=s3.cn-north-1.amazonaws.com.cn --ho ...

  10. SAP下载报表速度慢?为啥你不试试python多线程

    由于SAP系统自身原因,或者公司内部ABAP代码的算法效率不高,我们经常遇到,手工执行某个事务代码下载某个报表会非常耗时,小爬曾见过公司某个自开发的报表,单家公司的数据下载超过半小时.如果我们刚好接到 ...