Netty入门二:Netty概述及Demo
Netty简述
今天,我们使用通用的应用程序或者类库来实现互相通讯,比如,我们经常使用一个 HTTP 客户端库来从 web 服务器上获取信息,或者通过 web 服务来执行一个远程的调用。
然而,有时候一个通用的协议或他的实现并没有很好的满足需求。比如我们无法使用一个通用的 HTTP 服务器来处理大文件、电子邮件以及近实时消息,比如金融信息和多人游戏数据。我们需要一个高度优化的协议来处理一些特殊的场景。例如你可能想实现一个优化了的 Ajax 的聊天应用、媒体流传输或者是大文件传输器,你甚至可以自己设计和实现一个全新的协议来准确地实现你的需求。
我们知道Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,由于 NIO 原生编程太过于复杂,Netty对其进行了优秀的封装。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
通过 Netty 我们可以快速简单地开发网络应用程序,比如服务器(HTTP服务器,FTP服务器,WebSocket服务器,Redis的Proxy服务器等等)和客户端的协议。Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发。
Netty和Tomcat有什么区别?
Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。
有人说netty的性能就一定比tomcat性能高,其实不然,tomcat从6.x开始就支持了nio模式,并且后续还有APR模式——一种通过jni调用apache网络库的模式,相比于旧的bio模式,并发性能得到了很大提高,特别是APR模式,而netty是否比tomcat性能更高,则要取决于netty程序作者的技术实力了。
服务器demo
让我们从 handler (处理器)的实现开始,handler 是由 Netty 生成用来处理 I/O 事件的。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(ctx.channel().remoteAddress() + "->Server :" + msg.toString());
ctx.write(msg); // (1)
ctx.flush(); // (2)
// final ChannelFuture future = ctx.writeAndFlush(msg);
// final ChannelFuture future = ctx.write(msg + "\n");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("server read complete");
ctx.flush();
TimeUnit.MILLISECONDS.sleep(200);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
-
EchoServerHandler 继承自 ChannelInboundHandlerAdapter,这个类实现了
ChannelInboundHandler接口,ChannelInboundHandler
提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承 ChannelInboundHandlerAdapter
类而不是你自己去实现接口方法。 -
这里我们覆盖了 chanelRead() 事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用。
-
ChannelHandlerContext 对象提供了许多操作,使你能够触发各种各样的 I/O 事件和操作。这里我们调用了 write(Object) 方法来逐字地把接受到的消息写入。
-
ctx.write(Object) 方法不会使消息写入到通道上,他被缓冲在了内部,你需要调用 ctx.flush() 方法来把缓冲区中数据强行输出。或者你可以用更简洁的 cxt.writeAndFlush(msg) 以达到同样的目的。
-
exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来并且把关联的 channel 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
目前为止一切都还不错,我们已经实现了服务器的一半功能,剩下的需要编写一个 main() 方法来启动服务端的 EchoServerHandler。
/** 应答服务器 */
public class EchoServer {
private int port;
public EchoServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.childHandler(
new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// 绑定端口,开始接收进来的连接
ChannelFuture f = b.bind(port).sync(); // (7)
System.out.println("Server start listen at " + port);
// 等待服务器 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 EchoServer(port).run();
}
}
-
NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器,Netty 提供了许多不同的 EventLoopGroup 的实现用来处理不同的传输。在这个例子中我们实现了一个服务端的应用,因此会有2个
NioEventLoopGroup
会被使用。第一个经常被叫做‘boss’,用来接收进来的连接。第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上。如何知道多少个线程已经被使用,如何映射到已经创建的
Channel上都需要依赖于 EventLoopGroup 的实现,并且可以通过构造函数来配置他们的关系。 -
ServerBootstrap 是一个启动 NIO 服务的辅助启动类。你可以在这个服务中直接使用
Channel,但是这会是一个复杂的处理过程,在很多情况下你并不需要这样做。 -
这里我们指定使用 NioServerSocketChannel 类来举例说明一个新的 Channel 如何接收进来的连接。
-
这里的事件处理类经常会被用来处理一个最近的已经接收的 Channel。ChannelInitializer
是一个特殊的处理类,他的目的是帮助使用者配置一个新的
Channel。也许你想通过增加一些处理类比如DiscardServerHandler 来配置一个新的 Channel
或者其对应的ChannelPipeline 来实现你的网络程序。当你的程序变的复杂时,可能你会增加更多的处理类到 pipline
上,然后提取这些匿名类到最顶层的类上。 -
你可以设置这里指定的 Channel 实现的配置参数。我们正在写一个TCP/IP 的服务端,因此我们被允许设置 socket
的参数选项比如tcpNoDelay 和 keepAlive。请参考 ChannelOption 和详细的 ChannelConfig
实现的接口文档以此可以对ChannelOption 的有一个大概的认识。 -
你关注过 option() 和 childOption() 吗?option() 是提供给NioServerSocketChannel
用来接收进来的连接。childOption() 是提供给由父管道 ServerChannel 接收到的连接,在这个例子中也是
NioServerSocketChannel。 -
我们继续,剩下的就是绑定端口然后启动服务。这里我们在机器上绑定了机器所有网卡上的 8080 端口。当然现在你可以多次调用 bind()
方法(基于不同绑定地址)。
恭喜!你已经熟练地完成了第一个基于 Netty 的服务端程序。
客户端demo
**
* Sends one message when a connection is open and echoes back any received data to the server.
* Simply put, the echo client initiates the ping-pong traffic between the echo client and server by
* sending the first message to the server.
*/
public final class EchoClient {
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8080"));
public static void main(String[] args) throws Exception {
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(HOST, PORT).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
}
}
-
BootStrap 和 ServerBootstrap 类似,不过他是对非服务端的 channel 而言,比如客户端或者无连接传输模式的 channel。
-
如果你只指定了一个 EventLoopGroup,那他就会即作为一个 boss group ,也会作为一个 workder group,尽管客户端不需要使用到 boss worker 。
-
代替NioServerSocketChannel的是NioSocketChannel,这个类在客户端channel 被创建时使用。
-
不像在使用 ServerBootstrap 时需要用 childOption() 方法,因为客户端的 SocketChannel 没有父亲。
-
我们用 connect() 方法代替了 bind() 方法。
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private final String firstMessage;
/** Creates a client-side handler. */
public EchoClientHandler() {
// firstMessage = Unpooled.buffer(EchoClient.SIZE);
// for (int i = 0; i < firstMessage.capacity(); i ++) {
// firstMessage.writeByte((byte) i);
// }
//// firstMessage.writeByte('\n');
firstMessage = "hello\n";
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(firstMessage);
System.out.println("channel active.");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("channel read from server: " + msg);
ctx.write(msg + "\n");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
System.out.println("channel read complete");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}