目录
- 一、理论知识
- 1. 网络协议TCP/UDP
- 2. netty简介
- 3. 依赖
- 4. netty核心类介绍
- 二、开发实战
- 1. 服务端
- 2. 客户端
- demo源码
- 参考
一、理论知识
1. 网络协议TCP/UDP
TCP、UDP协议属于七层协议中传输层的协议,这两种主流协议的差异:
- TCP是一个面向连接的、可靠的、基于字节流(socket)的传输层通信协议;
- UDP是一个面向数据包(datagram)的、简单的、不可靠的通信协议。
- TCP进行有序、无错的传输;
- UDP则可能出现数据包丢失、错误或重复。
- UDP不需要额外的保证,结构简单,只管发送数据,通常UDP的传输速度比TCP更快。
- UPD支持广播,TCP则不支持,TCP需要先建立端到端的连接才能发送数据。
基于以上特性,TCP、UDP的使用场景也不同:
- TCP: HTTPS、HTTP、SMTP、FTP等等。
- UDP:视频流、语音流、即时通信、广播等等。
在netty服务端中使用哪种协议取决于你在启动类AbstractBootstrap.channel(Class<? extends C> channelClass)
中使用了哪种Channel
接口实现类(这里的TCP/IP并非指TCP/IP协议簇,其表示的是传输层使用TCP传输,网络层使用IP来提供路由和寻址):
ServerSocketChannel
:TCP/IP协议,主要实现类NioServerSocketChannel、EpollServerSocketChannelDatagramChannel
:UDP/IP协议,主要实现类NioDatagramChannel、EpollDatagramChannel- 其它协议……
如官网Echo源码中使用NioServerSocketChannel进行网络通信:
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
……
2. netty简介
Nett是一个NIO的通信框架,在基于netty的核心框架上,netty实现了各种协议支持,很多功能都是开箱即用的netty-github。
下图分为三个部分:
- 核心包:零拷贝、高扩展的事件模型、多协议统一的API
- 传输服务:包括字节流&数据包传输(TCP&UDP)、HTTP隧道、虚拟管道等服务
- 协议支持:这一部分提供了HTTP&websocket协议、SSL/TLS安全协议、zlib/gzip等压缩技术、大文件传输等等支持
3. 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.91.Final</version>
<scope>compile</scope>
</dependency>
4. netty核心类介绍
简单介绍一下netty框架中主要类的作用:
- AbstractBootstrap:抽象的引导类。通过该引导类可以配置整个端的生命周期。
- ServerBootstrap:AbstractBootstrap子类,用于创建方便创建服务端引导类。
- Bootstrap:AbstractBootstrap子类,用于创建方便创建客户端引导类。
- EventLoopGroup:事件循环执行组,用于循环执行某项
Channel
任务。每一个组中包含至少一个EventLoop
,每个EventLoop负责执行一个个实际的Channel任务。 - Channel:netty的核心组件,它代表一个到实体(硬件设备、文件、网络套接字、或能执行一个或多个不同的I/O操作的程序组件)的开放连接,如进行读操作和写操作。每个与实体的连接都会封装为一个Channel,随后被注册到EventLoop中,然后由EventLoop负责调度处理。
- ChannelHandler:Channel的回调处理器,ChannelHandler的每个方法都是Channel的一个回调方法,当Channel触发了某个事件时,就会调用一系列ChannelHandler回调处理器中的某个回调方法,回调方法会对该事件进行各种处理。
- ChannelInboundHandler:实现ChannelHandler,是入站(入境,即是由外部传入的操作)事件的回调处理器。譬如远程建立连接或该连接失活、数据读取、用户事件、错误事件都会触发一个入站事件。
- ChannelOutboundHandler: 实现ChannelHandler,是出站(出境,即是由内部传出的操作)事件的回调处理器。譬如主动创建或关闭对外连接、将数据写到或冲刷到套接字。
- ChannelHandlerAdapter:实现ChannelHandler,如果将ChannelInboundHandler、ChannelOutboundHandler看作是一种出入站的标记接口的话,那么ChannelHandlerAdapter就是实现各种出入站事件的核心骨架。你可以在各种出入站处理器中看见它的身影。
- ChannelInitializer:特殊的入站处理器,它将在一个Channel被注册到EventLoop时去初始化Channel,常用来设置Channel的ChannelPipeline。
- ChannelPipeline:一个Channel内部的出入站列表,包含了一系列的ChannelHandler。
二、开发实战
1. 服务端
- 服务端两个EventLoopGroup,bossGroup用于监听连接,有连接接入后转发到workerGroup;workerGroup用于处理与Channel的操作事件。
- bossGroup注册事件处理器:
- LoggingHandler:日志处理器,DEBUG模式
- workerGroup注册事件处理器:
- LoggingHandler:日志处理器,DEBUG模式
- StringDecoder:字符串解码,将字节流转为字符
- StringEncoder:字符串编码,将字符串转为字节流
- ServerStringHandler:自定义处理器,将接收的字符原封不动的写回
服务器可以直接设置port,也可以像netty官方示例代码一样使用System.getProperty()
来获取java启动命令的键值-Dkey=value
。
public class EchoServerRunner {
private final int port;
public EchoServerRunner(Integer port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
EchoServerRunner server = new EchoServerRunner(8001);
server.start();
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
// 新的Channel 如何进行数据传输
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
// 打印bossGroup处理日志
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 设置出入消息的处理链
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ServerStringHandler());
}
});
// 绑定监听服务端口,并开始接收进来的连接
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ServerStringHandler,自定义处理器代码:
public class ServerStringHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端接收到消息:" + msg);
ctx.writeAndFlush(msg);
}
}
ok,一个echo服务器的功能就写好了,我们可以通过cmd的telnet
命令来测试连接
telnet localhost 8001
连接成功后,服务端显示日志:
这时我们就可以开始传输数据了,先后输入1
c
,控制台打印1cc
(前面的一个c是本地输入回显)。
服务端的日志打印中,服务器接收(READ)到两次1byte(1B)的数据:31
63
,解码后的原始数据为1
c
。
解释一下下面的一段内容:
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 |1 |
+--------+-------------------------------------------------+----------------+
0 1 2 3 4 5 6 7 8 9 a b c d e f
表示的是一个包中的第几个字节流(8bit = 1byte),其最大值也表示了一个包的最大容量。00000000
表示的是包的序号,32位,TCP为了不丢包,就会给每一个包设置一个序号,服务端会按着序号顺序逐步接收,接收成功一个包即返回ack,这也保证了数据接收的有序性。31
表示的是传输的数据,一行中的一列占8字节,这里用的十六进制ASCII表示。1
表示的是该包转码后的数据。
ASCII码的对照表:
ascii-code
ascii
ok,服务端看起来没什么问题了,我们接下来就用netty来模拟一个用户给echo服务器发送数据。
2. 客户端
- 客户端只有一个EventLoopGroup,因为我们只需要处理消息收发,并不需要监听某个端口的连接。
- workerGroup注册事件处理器:
- LoggingHandler:日志处理器,DEBUG模式
- StringDecoder:字符串解码,将字节流转为字符
- StringEncoder:字符串编码,将字符串转为字节流
- ClientStringHandler:自定义处理器,用来处理服务器回复
public class EchoClientRunner {
private String host;
private Integer port;
public EchoClientRunner(String host, Integer port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
EchoClientRunner client = new EchoClientRunner("127.0.0.1", 8001);
client.start();
}
public void start() throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup)
// 新的Channel 如何接收进来的连接
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 设置出入消息的处理链
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ClientStringHandler());
}
});
// 创建一个连接
ChannelFuture f = b.connect(host, port).sync();
// 创建连接后手动发送一个请求
f.channel().writeAndFlush("Hello!");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
先启动echo服务器,再启动客户端
服务器日志如下:
客户端日志如下:
echo服务端 客户端交互成功。
不过也行你会对上面的ChannelPipeline中的执行器顺序感到疑惑。这里就不得不提一下入境(ChannelInboundHandler)、出境(ChannelOutboundHandler)以及同时有出入境(ChannelDuplexHandler)处理器的执行顺序的区别。
// 服务端
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ServerStringHandler());
// 客户端
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG), new StringDecoder(), new StringEncoder(), new ClientStringHandler());
以服务端举例,当建立连接,客户端发送过来数据时,这时就会进入入境的处理链,执行顺序是ChannelInboundHandler注入的先后顺序:
- LoggingHandler(ChannelDuplexHandler)
- StringDecoder(ChannelInboundHandler)
- ServerStringHandler(ChannelInboundHandler)
当入境执行链处理完毕后,入境也就执行完成了。但是在ServerStringHandler中,我们又向客户端回复了信息,这时进入出境的处理链,执行顺序为ChannelOutboundHandler注入的相反方向:
- StringEncoder(ChannelOutboundHandler)
- LoggingHandler(ChannelDuplexHandler)
到这里一个完整的echo服务器就开发完成了。
demo源码
netty-demo
参考
netty实战
User guide for 5.x
java/io/netty/example