Tim

一枚野生程序员~

  • 主页
  • 分类
  • 标签
  • 归档
  • 关于
所有文章 工具

Tim

一枚野生程序员~

  • 主页
  • 分类
  • 标签
  • 归档
  • 关于

Netty编码实战与Channel生命周期

阅读数:次 2020-07-03
字数统计: 1.8k字   |   阅读时长≈ 8分

本次将搭建一个最简单的Hello Netty服务器,并且通过这个简单的示例了解了Channel的生命周期。最后将基于Netty搭建一个Websocket网页聊天小程序,可以使用户在Web浏览器或者移动端浏览器进行消息的收发,来深入体会一下使用Netty编码NIO服务器是多么便捷。

Hello Netty服务器

  • 构建一对主从线程组
  • 定义服务器启动类
  • 为服务器设置Channel
  • 设置处理从线程池的助手类初始化器
  • 监听启动和关闭服务器

1、构建主从线程组与服务启动类

首先新建一个Maven工程,引入Netty的依赖,我引入的依赖如下:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.50.Final</version>
</dependency>
</dependencies>

mark

2、设置Channel初始化器

每一个channel由多个handler共同组成管道(pipeline)

mark

mark

3、开始编写自定义的助手类

mark

然后接下来启动服务器,通过Postman访问一下http://localhost:8080得到如下结果:

mark

如果直接在浏览器端访问的话会打印两次客户端远程地址,因为浏览器默认还访问了http://localhost:8080/favicon.ico,或者使用在Linux环境下使用curl进行测试也是可以的。

探究Channel生命周期

我们通过重写下图所示的方法来研究一下Channel的生命周期(IDEA快捷键 Ctrl + O):

mark

重写完成之后的CustomHandler如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class CustomHandler extends SimpleChannelInboundHandler<HttpObject> {
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
// 获取Channel
Channel channel = ctx.channel();
if(msg instanceof HttpRequest) {
// 显示客户端的远程地址
System.out.println(channel.remoteAddress());
// 数据Copy至缓冲区(定义发送的数据消息)
ByteBuf content = Unpooled.copiedBuffer("<h1>Hello Netty</h1>", CharsetUtil.UTF_8);
// 构建一个Http Response
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
// 为响应增加一个数据类型和长度
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text.plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
// 把响应刷到客户端
ctx.writeAndFlush(response);
}
}

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-注册");
super.channelRegistered(ctx);
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-移除");
super.channelUnregistered(ctx);
}

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-活跃");
super.channelActive(ctx);
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-不活跃(断开了)");
super.channelInactive(ctx);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-读取数据完毕");
super.channelReadComplete(ctx);
}

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("用户事件触发");
super.userEventTriggered(ctx, evt);
}

@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("Channel-可写更改");
super.channelWritabilityChanged(ctx);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("捕获到异常");
super.exceptionCaught(ctx, cause);
}

@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("助手类添加");
super.handlerAdded(ctx);
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("助手类移除");
super.handlerRemoved(ctx);
}
}

通过Curl访问控制台打印如下:

mark

为什么要用CURL而不是浏览器或者PostMan呢?因为我们使用了HTTP1.1的版本,支持长连接,而且默认是开启状态,所以看不到Channel不活跃断开的状态,所以才使用CURL来访问。

网页版的WebSocket聊天室

1、Netty 服务器编码

Netty 服务器启动类WSServe.java如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class WSServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitializer());

ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

接下来是Channel初始化器WSServerInitializer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class WSServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// WebSocket基于Http协议,添加Http编解码器
pipeline.addLast(new HttpServerCodec());

// 添加对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());

// 对Http Message进行聚合,聚合成FullHttpRequest或FullHttpResponse
// 几乎在Netty中的编程都会使用到此Handler
pipeline.addLast(new HttpObjectAggregator(1024 * 64));

//-------------------- 以上是用于支持HTTP协议 ----------------------

// WebSocket服务器处理的协议,并且指定给客户端链接访问的路由
// 使用此Handler会直接帮你处理握手动作(Close、Ping、Pong)
// 对于WebSocket,都是以帧进行传输的,不同数据对应的帧也不同
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

// 自定义的Handler
pipeline.addLast(new ChatHandler());
}
}

最后是自定义的Handler,ChatHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.time.LocalDateTime;

// 对于WebSocket,都是以帧进行传输的,不同数据对应的帧也不同 -> TextWebSocketFrame
// TextWebSocketFrame是WebSocket专门用于处理文本的对象,Frame是消息的载体
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

// 用于记录和管理所有客户端的Channel
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 从客户端获取到的消息内容
String content = msg.text();
System.out.println("接收到的数据:" + content);
String message = "[服务器收到消息] " + LocalDateTime.now() + "消息为:" + content;
for(Channel channel: clients){
//channel.writeAndFlush(content); ERROR 不能直接传String,而是TextWebSocketFrame载体
channel.writeAndFlush(new TextWebSocketFrame(message));
}

// 下面这种方式与For循环一致
//clients.writeAndFlush(new TextWebSocketFrame(message));
}


@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
// 当客户端打开链接后,获取客户端的Channel并且添加Channel至ChannelGroup中进行管理
clients.add(channel);
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 当触发handlerRemoved,ChannelGroup会自动移除客户端的Channel
System.out.println("客户端断开, Channel对应的长ID:" + ctx.channel().id().asLongText());
System.out.println("客户端断开, Channel对应的短ID:" + ctx.channel().id().asShortText());
}
}

2、前端JavaScript编码

下面是前端需要用到的WebSocket API:

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div>发送消息</div>
<input type="text" id="msgContent"/>
<input type="button" onclick="CHAT.chat()" value="发送"/>

<div>接收消息</div>
<div id="receiveMsg" style="background-color: darkturquoise;"></div>
<script type="application/javascript">
window.CHAT = {
socket: null,
init: function(){
if(window.WebSocket){
CHAT.socket = new WebSocket("ws://127.0.0.1:8080/ws");
CHAT.socket.onopen = function(){
console.log('连接建立成功...');
},
CHAT.socket.onclose = function(){
console.log('连接建立关闭...');
},
CHAT.socket.onerror = function(){
console.log('连接建立发生错误...');
},
CHAT.socket.onmessage = function(e){
console.log('收到消息...' + e.data);
var receiveMsg = document.getElementById('receiveMsg');
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + "<br/>" + e.data;
}
}else{
alert('不支持WebSocket');
}
},
chat: function(){
var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
}
CHAT.init();
</script>
</body>
</html>

3、效果展示

Netty编码的小总结

首先是流程,先新建主从线程组,编写启动类,因为Netty官方推荐的模式也是主从线程模型。接下来是编写Channel初始化器,继承自ChannelInitializer,Channel注册后会执行里面的相应的初始化方法,通过Channel获取管道,然后添加需要的Handler,最后添加自己的自定义的Handler来处理请求。

赏

谢谢你请我喝咖啡

支付宝
微信
  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/1716304322.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!
  • NIO
  • RPC
  • 高性能网络

扫一扫,分享到微信

基于Netty实现PRC框架
Netty的Reactor线程模型
  1. 1. Hello Netty服务器
    1. 1.1. 1、构建主从线程组与服务启动类
    2. 1.2. 2、设置Channel初始化器
    3. 1.3. 3、开始编写自定义的助手类
  2. 2. 探究Channel生命周期
  3. 3. 网页版的WebSocket聊天室
    1. 3.1. 1、Netty 服务器编码
    2. 3.2. 2、前端JavaScript编码
    3. 3.3. 3、效果展示
  4. 4. Netty编码的小总结
© 2017-2021 Tim
本站总访问量次 | 本站访客数人
  • 所有文章
  • 工具

tag:

  • 生活
  • Android
  • 索引
  • MySQL
  • 组件通信
  • Nginx
  • JavaSE
  • JUC
  • JavaWeb
  • 模板引擎
  • 前端
  • Linux
  • 计算机网络
  • Docker
  • C/C++
  • JVM
  • 上传下载
  • JavaEE
  • SpringCloud
  • Golang
  • Gradle
  • 网络安全
  • 非对称加密
  • IDEA
  • SpringBoot
  • Jenkins
  • 字符串
  • vim
  • 存储
  • 文件下载
  • Mac
  • Windows
  • NIO
  • RPC
  • 集群
  • 微服务
  • SSH
  • 配置中心
  • XML
  • Chrome
  • 压力测试
  • Git
  • 博客
  • 概率论
  • 排序算法
  • 分布式
  • 异常处理
  • 文件系统
  • 哈希
  • openCV
  • 栈
  • 回溯
  • SpringCore
  • 流媒体
  • rtmp
  • 面向对象
  • Vue
  • ElementUI
  • 软件工程
  • 异步
  • 自定义UI
  • ORM框架
  • 模块化
  • 交互式
  • Jsoup
  • Http Client
  • LRUCache
  • RabbitMQ
  • 消息通信
  • 服务解耦
  • 负载均衡
  • 权限
  • 多线程
  • 单例模式
  • Protobuf
  • 序列化
  • Python
  • m3u8
  • 堆
  • 二叉树
  • 自定义View
  • 观察者模式
  • 设计模式
  • 线程池
  • 动态扩容
  • 高可用
  • GC
  • ffmpeg
  • SpringMVC
  • REST
  • Redis
  • 缓存中间件
  • UML
  • Maven
  • Netty
  • 高性能网络
  • IPC通信
  • IO
  • Stream
  • 发布订阅
  • SQLite
  • Hash
  • 集合框架
  • 链表
  • Lambda
  • 汇编语言
  • 组件化
  • Router
  • 开发工具

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia-plus根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 思维导图
  • PDF工具
  • 无损放大
  • 代码转图
  • HTTPS证书