Netty是 一个异步事件驱动的网络应用程序框架, 用于快速开发可维护的高性能协议服务器和客户端。java
Netty是一个NIO客户端服务器框架,能够快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化并简化了TCP和UDP套接字服务器等网络编程。linux
“快速简便”并不意味着最终的应用程序会受到可维护性或性能问题的影响。Netty通过精心设计,具备丰富的协议,如FTP,SMTP,HTTP以及各类二进制和基于文本的传统协议。所以,Netty成功地找到了一种在不妥协的状况下实现易于开发,性能,稳定性和灵活性的方法。git
适用于各类传输类型的统一API - 阻塞和非阻塞套接字github
基于灵活且可扩展的事件模型,能够清晰地分离关注点redis
高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDAspring
真正的无链接数据报套接字支持(自3.1起)编程
详细记录的Javadoc,用户指南和示例windows
没有其余依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了安全
注意:某些组件(如HTTP / 2)可能有更多要求。 有关更多信息,请参阅 “要求”页面。bash
吞吐量更高,延迟更低
减小资源消耗
最小化没必要要的内存复制
完整的SSL / TLS和StartTLS支持
早发布,常常发布
自2003年以来,做者一直在编写相似的框架,他仍然以为你的反馈很珍贵!
从官方仓库 github.com/netty/netty Fork
出属于本身的仓库。为何要 Fork
?既然开始阅读、调试源码,咱们可能会写一些注释,有了本身的仓库,能够进行自由的提交。😈
使用 IntelliJ IDEA
从 Fork
出来的仓库拉取代码。
本文使用的 Netty 版本为 4.1.26.Final-SNAPSHOT
。
打开 IDEA 的 Maven Projects ,选择对应的 Profiles 。以下图所示:
jdk8
:笔者使用的 JDK 版本是 8 ,因此勾选了 jdk8
。若是错误的选择,可能会报以下错误:
java.lang.NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer
复制代码
linux
: 选择对应的系统版本。😈 笔者手头没有 windows 的电脑,因此不知道该怎么选。
修改完成后,点击左上角的【刷新】按钮,进行依赖下载,耐心等待...
在 codec-redis
模块中,类 FixedRedisMessagePool 会报以下类不存在的问题:
import io.netty.util.collection.LongObjectHashMap;
import io.netty.util.collection.LongObjectMap;
复制代码
解决方式以下:
cd common;
mvn clean compile;
复制代码
common
模块中,编译生成对应的类。为何能够经过编译生成对应的类呢,缘由参见 common
模块的 src/java/templates/io/netty/util/collection
目录下的 .template
文件。在 Github 上,也有多个针对这个状况讨论的 issue :
在 example
模块里,官网提供了多个 Netty 的使用示例。 本文以 telnet
包下来做为示例。哈哈哈,由于最简单且完整。
说明: 若是想直接获取工程那么能够直接跳到底部,经过连接下载工程代码。
若是对Netty不熟的话,能够看看以前写的一些文章。大神请无视☺。
首先仍是Maven的相关依赖:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<netty-all.version>4.1.6.Final</netty-all.version>
</properties>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty-all.version}</version>
</dependency>
</dependencies>
复制代码
添加了相应的maven依赖以后,配置文件这块暂时没有什么能够添加的,由于暂时就一个监听的端口而已。
代码模块主要分为服务端和客户端。 主要实现的业务逻辑: 服务端启动成功以后,客户端也启动成功,这时服务端会发送一条信息给客户端。客户端或者telnet发送一条信息到服务端,服务端会根据逻辑回复客户端一条客户端,当客户端或者telent发送bye
给服务端,服务端和客户端断开连接。
netty-helloworld
├── client
├── Client.class -- 客户端启动类
├── ClientHandler.class -- 客户端逻辑处理类
├── ClientHandler.class -- 客户端初始化类
├── server
├── Server.class -- 服务端启动类
├── ServerHandler -- 服务端逻辑处理类
├── ServerInitializer -- 服务端初始化类
复制代码
首先是编写服务端的启动类。
代码以下:
public final class Server {
public static void main(String[] args) throws Exception {
//Configure the server
//建立两个EventLoopGroup对象
//建立boss线程组 用于服务端接受客户端的链接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 建立 worker 线程组 用于进行 SocketChannel 的数据读写
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 建立 ServerBootstrap 对象
ServerBootstrap b = new ServerBootstrap();
//设置使用的EventLoopGroup
b.group(bossGroup,workerGroup)
//设置要被实例化的为 NioServerSocketChannel 类
.channel(NioServerSocketChannel.class)
// 设置 NioServerSocketChannel 的处理器
.handler(new LoggingHandler(LogLevel.INFO))
// 设置连入服务端的 Client 的 SocketChannel 的处理器
.childHandler(new ServerInitializer());
// 绑定端口,并同步等待成功,即启动服务端
ChannelFuture f = b.bind(8888);
// 监听服务端关闭,并阻塞等待
f.channel().closeFuture().sync();
} finally {
// 优雅关闭两个 EventLoopGroup 对象
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
复制代码
第6到8行: 建立两个EventLoopGroup对象。
第11行: 建立 ServerBootstrap 对象,用于设置服务端的启动配置。
#group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
方法,设置使用的 EventLoopGroup 。#channel(Class<? extends C> channelClass)
方法,设置要被实例化的 Channel 为 NioServerSocketChannel 类。在下文中,咱们会看到该 Channel 内嵌了 java.nio.channels.ServerSocketChannel
对象。是否是很熟悉 😈 ?#handler(ChannelHandler handler)
方法,设置 NioServerSocketChannel 的处理器。在本示例中,使用了 io.netty.handler.logging.LoggingHandler
类,用于打印服务端的每一个事件。#childHandler(ChannelHandler handler)
方法,设置连入服务端的 Client 的 SocketChannel 的处理器。在本实例中,使用 ServerInitializer() 来初始化连入服务端的 Client 的 SocketChannel 的处理器。第21行: 先调用 #bind(int port)
方法,绑定端口,后调用 ChannelFuture#sync()
方法,阻塞等待成功。这个过程,就是“启动服务端”。
第23行: 先调用 #closeFuture()
方法,监听服务器关闭,后调用 ChannelFuture#sync()
方法,阻塞等待成功。😈 注意,此处不是关闭服务器,而是“监听”关闭。
第26到27行: 执行到此处,说明服务端已经关闭,因此调用 EventLoopGroup#shutdownGracefully()
方法,分别关闭两个 EventLoopGroup 对象。
服务端主类编写完毕以后,咱们再来设置下相应的过滤条件。 这里须要继承Netty中ChannelInitializer类,而后重写initChannel该方法,进行添加相应的设置,传输协议设置,以及相应的业务实现类。 代码以下:
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final ServerHandler SERVER_HANDLER = new ServerHandler();
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 添加帧限定符来防止粘包现象
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 解码和编码,应和客户端一致
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
// 业务逻辑实现类
pipeline.addLast(SERVER_HANDLER);
}
}
复制代码
服务相关的设置的代码写完以后,咱们再来编写主要的业务代码。 使用Netty编写业务层的代码,咱们须要继承ChannelInboundHandlerAdapter 或SimpleChannelInboundHandler类,在这里顺便说下它们两的区别吧。 继承SimpleChannelInboundHandler类以后,会在接收到数据后会自动release掉数据占用的Bytebuffer资源。而且继承该类须要指定数据格式。 而继承ChannelInboundHandlerAdapter则不会自动释放,须要手动调用ReferenceCountUtil.release()等方法进行释放。继承该类不须要指定数据格式。 因此在这里,我的推荐服务端继承ChannelInboundHandlerAdapter,手动进行释放,防止数据未处理完就自动释放了。并且服务端可能有多个客户端进行链接,而且每个客户端请求的数据格式都不一致,这时即可以进行相应的处理。 客户端根据状况能够继承SimpleChannelInboundHandler类。好处是直接指定好传输的数据格式,就不须要再进行格式的转换了。
代码以下:
@Sharable
public class ServerHandler extends SimpleChannelInboundHandler<String> {
/** * 创建链接时,发送一条庆祝消息 */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 为新链接发送庆祝
ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n");
ctx.write("It is " + new Date() + " now.\r\n");
ctx.flush();
}
//业务逻辑处理
@Override
public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {
// Generate and write a response.
String response;
boolean close = false;
if (request.isEmpty()) {
response = "Please type something.\r\n";
} else if ("bye".equals(request.toLowerCase())) {
response = "Have a good day!\r\n";
close = true;
} else {
response = "Did you say '" + request + "'?\r\n";
}
ChannelFuture future = ctx.write(response);
if (close) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
//异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
复制代码
到这里服务端相应的代码就编写完毕了:rocket: 。
客户端这边的代码和服务端的不少地方都相似,我就再也不过多细说了,主要将一些不一样的代码拿出来简单的讲述下。 首先是客户端的主类,基本和服务端的差很少。 主要实现的代码逻辑以下:
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ClientInitializer());
Channel ch = b.connect("127.0.0.1",8888).sync().channel();
ChannelFuture lastWriteFuture = null;
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
break;
}
// Sends the received line to the server.
lastWriteFuture = ch.writeAndFlush(line + "\r\n");
// If user typed the 'bye' command, wait until the server closes
// the connection.
if ("bye".equals(line.toLowerCase())) {
ch.closeFuture().sync();
break;
}
}
// Wait until all messages are flushed before closing the channel.
if (lastWriteFuture != null) {
lastWriteFuture.sync();
}
} finally {
group.shutdownGracefully();
}
}
复制代码
客户端过滤其这块基本和服务端一致。不过须要注意的是,传输协议、编码和解码应该一致。
代码以下:
public class ClientInitializer extends ChannelInitializer<SocketChannel> {
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final ClientHandler CLIENT_HANDLER = new ClientHandler();
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
pipeline.addLast(CLIENT_HANDLER);
}
}
复制代码
客户端的业务代码逻辑。
主要时打印读取到的信息。
这里有个注解, 该注解Sharable主要是为了多个handler能够被多个channel安全地共享,也就是保证线程安全。 废话就很少说了,代码以下:
@Sharable
public class ClientHandler extends SimpleChannelInboundHandler<String> {
//打印读取到的数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.err.println(msg);
}
//异常数据捕获
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
复制代码
那么到这里客户端的代码也编写完毕了:rocket: 。
首先启动服务端,而后再启动客户端。
咱们来看看结果是否如上述所说。
十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x1c7da838] REGISTERED
十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x1c7da838] BIND: 0.0.0.0/0.0.0.0:8888
十月 02, 2018 10:03:00 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x1c7da838, L:/0:0:0:0:0:0:0:0:8888] ACTIVE
十月 02, 2018 10:03:51 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x1c7da838, L:/0:0:0:0:0:0:0:0:8888] RECEIVED: [id: 0xc033aea8, L:/127.0.0.1:8888 - R:/127.0.0.1:58178]
复制代码
Connected to the target VM, address: '127.0.0.1:37175', transport: 'socket'
Welcome to james!
It is Tue Oct 02 10:03:51 CST 2018 now.
yes
Did you say 'yes'?
hello world
Did you say 'hello world'?
bye
Have a good day!
Disconnected from the target VM, address: '127.0.0.1:37175', transport: 'socket'
Process finished with exit code 0
复制代码
telnet客户端 和服务端交互结果以下:
经过打印信息能够看出如上述所说。
关于netty 之 telnet HelloWorld 详解到这里就结束了。
netty 之 telnet HelloWorld 详解项目工程地址:
github.com/sanshengshu…
对了,也有不使用springBoot整合的Netty项目工程地址:
github.com/sanshengshu…
原创不易,若是感受不错,但愿给个推荐!您的支持是我写做的最大动力!
版权声明:
做者:穆书伟
博客园出处:www.cnblogs.com/sanshengshu…
github出处:github.com/sanshengshu…
我的博客出处:sanshengshui.github.io/