Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架。很多其它业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。
通过对Netty的分析,我们将它的优点总结如下:
1) API使用简单,开发门槛低;
2) 功能强大,预置了多种编解码功能,支持多种主流协议;
3) 定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展;
4) 性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优;
5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入;
7) 经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。
正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架。
Netty的功能非常丰富,下图是Netty框架的组成:
想必大家都知道每学习一个新内容第一个案例就是HelloWorld,没错下面我们来编写Nettyd的HelloWorld
下载netty包,下载地址http://netty.io/
从上图中可以看出,服务器会写数据到客户端并且处理多个客户端的并发连接。从理论上来说,限制程序性能的因素只有系统资源和JVM。为了方便理解,这里举了个生活例子,在山谷或高山上大声喊,你会听见回声,回声是山返回的;在这个例子中,你是客户端,山是服务器。喊的行为就类似于一个Netty客户端将数据发送到服务器,听到回声就类似于服务器将相同的数据返回给你,你离开山谷就断开了连接,但是你可以返回进行重连服务器并且可以发送更多的数据。
虽然将相同的数据返回给客户端不是一个典型的例子,但是客户端和服务器之间数据的来来回回的传输和这个例子是一样的。本章的例子会证明这一点,它们会越来越复杂。
接下来后面内容将带着你完成基于Netty的客户端和服务器的应答程序。
写一个Netty服务器主要由两部分组成:
• 配置服务器功能,如线程、端口
• 实现服务器处理程序,它包含业务逻辑,决定当有一个请求连接或接收数据时该做什么
代码如下:
public class Server {
public static void main(String[] args) throws Exception {
//1 创建线两个程组
//一个是用于处理服务器端接收客户端连接的
//一个是进行网络通信的(网络读写的)
EventLoopGroup pGroup = new NioEventLoopGroup();
EventLoopGroup cGroup = new NioEventLoopGroup();
//2 创建辅助工具类,用于服务器通道的一系列配置
ServerBootstrap b = new ServerBootstrap();
b.group(pGroup, cGroup) //绑定俩个线程组
//3
.channel(NioServerSocketChannel.class) //指定NIO的模式
//4
.option(ChannelOption.SO_BACKLOG, 1024) //设置tcp缓冲区
.option(ChannelOption.SO_SNDBUF, 32*1024) //设置发送缓冲大小
.option(ChannelOption.SO_RCVBUF, 32*1024) //这是接收缓冲大小
//5
.option(ChannelOption.SO_KEEPALIVE, true) //保持连接
//6
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//在这里配置具体数据接收方法的处理
sc.pipeline().addLast(new ServerHandler());
}
});
//7 进行绑定
ChannelFuture cf1 = b.bind(8765).sync();
// 等待关闭
cf1.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
}
}
1、NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。在这个例子中我们实现了一个服务端的应用,因此会有2个NioEventLoopGroup会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的Channels上都需要依赖于EventLoopGroup的实现,并且可以通过构造函数来配置他们的关系。
2、ServerBootstrap 是一个启动NIO服务的辅助启动类。你可以在这个服务中直接使用Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。
3、这里我们指定使用NioServerSocketChannel类来举例说明一个新的Channel如何接收进来的连接。
4、你可以设置这里指定的通道实现的配置参数。我们正在写一个TCP/IP的服务端,因此我们被允许设置socket的参数选项比如tcpNoDelay和keepAlive。请参考ChannelOption和详细的ChannelConfig实现的接口文档以此可以对ChannelOptions的有一个大概的认识。
5、你关注过option()和childOption()吗?option()是提供给NioServerSocketChannel用来接收进来的连接。childOption()是提供给由父管道ServerChannel接收到的连接,在这个例子中也是NioServerSocketChannel。
6、这里的事件处理类经常会被用来处理一个最近的已经接收的Channel。ChannelInitializer是一个特殊的处理类,他的目的是帮助使用者配置一个新的Channel。也许你想通过增加一些处理类比如DiscardServerHandle来配置一个新的Channel或者其对应的ChannelPipeline来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到pipline上,然后提取这些匿名类到最顶层的类上。
7、我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的8080端口。当然现在你可以多次调用bind()方法(基于不同绑定地址)。
本小节重点内容:
• 创建ServerBootstrap实例来引导绑定和启动服务器
• 创建NioEventLoopGroup对象来处理事件,如接受新连接、接收数据、写数据等等
• 指定InetSocketAddress,服务器监听此端口
• 设置childHandler执行所有的连接请求
• 都设置完毕了,最后调用ServerBootstrap.bind() 方法来绑定服务器
服务器业务逻辑必须继承ChannelInboundHandlerAdapter并且重写channelRead方法,这个方法在任何时候都会被调用来接收数据,接收到的是字节,我会使用Netty的ByteBufa来将字节转换成字符串。
下面是handler的实现,其实现的功能是将客户端发给服务器的数据返回给客户端:
public class ServerHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("server channel active... ");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
System.out.println("Server :" + body );
String response = "进行返回给客户端的响应:" + body ;
ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
//接收到客户端的数据立马断掉客户端连接 服务端还是启动着
//.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
System.out.println("读完了");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t)
throws Exception {
ctx.close();
}
}
ServerHandler 继承自 ChannelHandlerAdapter,这个类实现了ChannelHandler接口,ChannelHandler提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实现接口方法。
Netty使用多个ChannelHandler来达到对事件处理的分离,因为可以很容的添加、更新、删除业务逻辑处理handler。Handler很简单,它的每个方法都可以被重写,它的所有的方法中只有channelRead方法是必须要重写的。
服务器写好了,现在来写一个客户端连接服务器。应答程序的客户端包括以下几步:
• 连接服务器
• 写数据到服务器
• 等待接受服务器返回相同的数据
• 关闭连接
在Netty中,编写服务端和客户端最大的并且唯一不同的使用了不同的BootStrap和Channel的实现,客户端需同时指定host和port来告诉客户端连接哪个服务器。看下面代码:
public class Client {
public static void main(String[] args) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
//1
Bootstrap b = new Bootstrap();
//2
b.group(group)
//3
.channel(NioSocketChannel.class)
//4
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//在这里配置具体数据接收方法的处理
sc.pipeline().addLast(new ClientHandler());
}
});
//5
ChannelFuture cf1 = b.connect("127.0.0.1", 8765).sync();
//发送消息
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("777".getBytes()));
Thread.sleep(1000);
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("666".getBytes()));
Thread.sleep(1000);
cf1.channel().writeAndFlush(Unpooled.copiedBuffer("888".getBytes()));
cf1.channel().closeFuture().sync();
//等待关闭
group.shutdownGracefully();
}
}
1、BootStrap和ServerBootstrap类似,不过他是对非服务端的channel而言,比如客户端或者无连接传输模式的channel。
2、如果你只指定了一个EventLoopGroup,那他就会即作为一个‘boss’线程,也会作为一个‘workder’线程,尽管客户端不需要使用到‘boss’线程。
3、代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel被创建时使用。
4、不像在使用ServerBootstrap时需要用childOption()方法,因为客户端的SocketChannel没有父channel的概念。
5、我们用connect()方法代替了bind()方法。
创建启动一个客户端包含下面几步:
• 创建Bootstrap对象用来引导启动客户端
• 创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接、接受数据、发送数据
• 创建InetSocketAddress并设置到Bootstrap中,InetSocketAddress是指定连接的服务器地址
• 添加一个ChannelHandler,客户端成功连接服务器后就会被执行
• 调用Bootstrap.connect()来连接服务器
• 最后关闭EventLoopGroup来释放资源
发消息这里我发了三条,每发一条是等待1秒的。至于为什么等1秒,或许下一篇文章你就懂了,如果不等待1秒会出现TCP的粘包、拆包问题。
你可以试一试发三条消息不设置等待时候服务端是一次打印完的还是分几次打印的。
记得每发一条数据都要冲刷一下,即:cf1.channel().flush()
上面我使用的是cf1.channel().writeAndFlush(....)
这个相当于cf1.channel().write(...);
cf1.channel().flush();
public class ClientHandler extends ChannelHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
System.out.println("Client :" + body );
String response = "收到服务器端的返回信息:" + body;
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}
这个客户端的业务逻辑跟服务端的业务逻辑基本上差不多,都需要使用ByteBuf来将字节转换成我们人读得懂得字符串。
客户端的编写完了,下面让我们来测试一下
服务端启动(图省略)
客户端启动后服务端的打印:
好!Netty的第一个程序到这就结束了!!!
源代码https://github.com/hfbin/Thread_Socket/tree/master/Socket/helloworld