一 初遇 Netty
Netty 是什么?
Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架, 是一个用以快速开发高性能可扩展协议的服务器和客户端
Netty 能做什么?
Netty 是一个 NIO 客户端服务器框架, 使用它可以快速简单地开发网络应用程序, 比如服务器 (HTTP 服务器, FTP 服务器, webSocket 服务器, Redis 的 Proxy 服务器等等) 和客户端的协议 Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发
Netty 为什么好?
Netty 是建立在 NIO 基础之上, Netty 在 NIO 之上又提供了更高层次的抽象, 使用它你可以更容易利用 Java NIO 提高服务端和客户端的性能
Netty 的特性:
1. 设计
1.1 统一的 API, 适用于不同的协议(阻塞和非阻塞)
1.2 基于可扩展和灵活的事件驱动模型
1.3 高度可定制的线程模型 - 单线程, 一个或多个线程池, 如 SEDA
1.4 真正的无连接数据报套接字支持(自 3.1 以来)
2. 性能
2.1 更好的吞吐量, 低延迟
2.2 更省资源
2.3 尽量减少不必要的内存拷贝
3. 安全
完整的 SSL / TLS 和 StartTLS 协议的支持
4. 易用性
4.1 官方有详细的使用指南
4.2 对环境要求很低
NIO 和 IO 的区别是什么?
1. 一个面向字节一个面向缓冲;
IO 面向流意味着每次从流中读一个或多个字节, 直至读取所有字节, 它们没有被缓存在任何地方此外, 它不能前后移动流中的数据如果需要前后移动从流中读取的数据, 需要先将它缓存到一个缓冲区 Java NIO 的缓冲导向方法略有不同数据读取到一个它稍后处理的缓冲区, 需要时可在缓冲区中前后移动这就增加了处理过程中的灵活性但是, 还需要检查是否该缓冲区中包含所有您需要处理的数据而且, 需确保当更多的数据读入缓冲区时, 不要覆盖缓冲区里尚未处理的数据
2. NIO 是非阻塞 IO,IO 是阻塞 IO
阻塞意味着当一个线程调用 read() 或 write()时, 该线程被阻塞, 直到有一些数据被读取, 或数据完全写入, 该线程在此期间不能再干任何事情了而非阻塞不会这样
二 Netty 使用
环境要求:
- JDK 7+
- Maven 3.2.x
- Netty 4.x
Maven 依赖:
- <dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-all</artifactId>
- <version>4.0.32.Final</version>
- </dependency>
2.1 写个抛弃服务器
- DiscardServerHandler.java
- import io.netty.buffer.ByteBuf;
- import io.netty.channel.ChannelHandlerContext;
- import io.netty.channel.ChannelInboundHandlerAdapter;
- /**
- * handler 是由 Netty 生成用来处理 I/O 事件的
- */
- public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
- /**
- * 这里我们覆盖了 chanelRead() 事件处理方法
- * 每当从客户端收到新的数据时, 这个方法会在收到消息时被调用
- *((ByteBuf) msg).release(): 丢弃数据
- */
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
- // 默默地丢弃收到的数据
- ((ByteBuf) msg).release(); // (3)
- }
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
- // 当出现异常就关闭连接
- cause.printStackTrace();
- ctx.close();
- }
- }
目前我们已经实现了 DISCARD 服务器的一半功能, 剩下的需要编写一个 main() 方法来启动服务端的 DiscardServerHandler
- DiscardServer.java
- import io.netty.bootstrap.ServerBootstrap;
- import io.netty.channel.ChannelFuture;
- import io.netty.channel.ChannelInitializer;
- import io.netty.channel.ChannelOption;
- import io.netty.channel.EventLoopGroup;
- import io.netty.channel.nio.NioEventLoopGroup;
- import io.netty.channel.socket.SocketChannel;
- import io.netty.channel.socket.nio.NioServerSocketChannel;
- /**
- * 启动服务端的 DiscardServerHandler
- */
- public class DiscardServer {
- private int port;
- public DiscardServer(int port) {
- this.port = port;
- }
- public void run() throws Exception {
- // 在这个例子中我们实现了一个服务端的应用, 因此会有 2 个 NioEventLoopGroup 会被使用
- // 第一个经常被叫做 boss, 用来接收进来的连接
- // 第二个经常被叫做 worker, 用来处理已经被接收的连接, 一旦 boss 接收到连接, 就会把连接信息注册到 worker 上
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workerGroup = new NioEventLoopGroup();
- try {
- // 启动 NIO 服务的辅助启动类
- ServerBootstrap serverBootstrap = new ServerBootstrap();
- // 用于处理 ServerChannel 和 Channel 的所有事件和 IO
- serverBootstrap.group(bossGroup, workerGroup)
- .channel(NioServerSocketChannel.class) // (3)
- .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
- @Override
- public void initChannel(SocketChannel ch) throws Exception {
- ch.pipeline().addLast(new DiscardServerHandler());
- }
- })
- .option(ChannelOption.SO_BACKLOG, 128) // (5)
- .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
- // 绑定端口, 开始接收进来的连接
- ChannelFuture f = serverBootstrap.bind(port).sync(); // (7)
- // 等待服务器 socket 关闭
- // 在这个例子中, 这不会发生, 但你可以优雅地关闭你的服务器
- f.channel().closeFuture().sync();
- } finally {
- workerGroup.shutdownGracefully();
- bossGroup.shutdownGracefully();
- }
- }
- public static void main(String[] args) throws Exception {
- int port;
- if (args.length> 0) {
- port = Integer.parseInt(args[0]);
- } else {
- port = 8080;
- }
- new DiscardServer(port).run();
- }
- }
2.2 查看收到的数据
我们刚刚已经编写出我们第一个服务端, 我们需要测试一下他是否真的可以运行最简单的测试方法是用 telnet 命令例如, 你可以在命令行上输入 telnet localhost 8080 或者其他类型参数
然而我们能说这个服务端是正常运行了吗? 事实上我们也不知道, 因为他是一个 discard 服务, 你根本不可能得到任何的响应为了证明他仍然是在正常工作的, 让我们修改服务端的程序来打印出他到底接收到了什么
我们已经知道 channelRead() 方法是在数据被接收的时候调用让我们放一些代码到 DiscardServerHandler 类的 channelRead() 方法
修改 DiscardServerHandler 类的 channelRead(ChannelHandlerContext ctx, Object msg)方法如下:
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
- ByteBuf in = (ByteBuf) msg;
- try {
- while (in.isReadable()) { // (1)
- System.out.print((char) in.readByte());
- System.out.flush();
- }
- } finally {
- ReferenceCountUtil.release(msg); // (2)
- }
- }
再次验证, cmd 下输入: telnet localhost 8080 你将会看到服务端打印出了他所接收到的消息
如下:
你在 dos 界面输入的消息会被显示出来
2.3 写个应答服务器
到目前为止, 我们虽然接收到了数据, 但没有做任何的响应然而一个服务端通常会对一个请求作出响应让我们学习怎样在 ECHO 协议的实现下编写一个响应消息给客户端, 这个协议针对任何接收的数据都会返回一个响应
和 discard server 唯一不同的是把在此之前我们实现的 channelRead() 方法, 返回所有的数据替代打印接收数据到控制台上的逻辑因此, 需要把 channelRead() 方法修改如下:
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
- ctx.write(msg);
- ctx.flush();
- }
再次验证, cmd 下输入: telnet localhost 8080 你会看到服务端会发回一个你已经发送的消息 如下:
下一篇我们会学习如何用 Netty 实现聊天功能
来源: https://juejin.im/post/5abb4d4c518825557459c0df