TCP、UDP协议属于七层协议中传输层的协议,这两种主流协议的差异:
基于以上特性,TCP、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)
……
Nett是一个NIO的通信框架,在基于netty的核心框架上,netty实现了各种协议支持,很多功能都是开箱即用的netty-github。
下图分为三个部分:
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.91.Finalversion>
<scope>compilescope>
dependency>
简单介绍一下netty框架中主要类的作用:
Channel
任务。每一个组中包含至少一个EventLoop
,每个EventLoop负责执行一个个实际的Channel任务。服务器可以直接设置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服务器发送数据。
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服务端 客户端交互成功。
不过也行你会对上面的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注入的先后顺序:
当入境执行链处理完毕后,入境也就执行完成了。但是在ServerStringHandler中,我们又向客户端回复了信息,这时进入出境的处理链,执行顺序为ChannelOutboundHandler注入的相反方向:
到这里一个完整的echo服务器就开发完成了。
netty-demo
netty实战
User guide for 5.x
java/io/netty/example