Netty快速入门
Netty是一个基于JAVA NIO类库的异步通信框架,他的架构特点是:异步非阻塞,基于事件驱动,高性能,高可靠和高可定制性。
rpc远程调用框架dubbo底层就是通过netty来实现的。zookeeper,rocketmq也是用的netty进行通信的。很多游戏开发也是通过netty进行通信的。
netty可以解决nio代码复杂的问题,容错机制。
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiyou</groupId>
<artifactId>netty-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.10.5.Final</version>
</dependency>
</dependencies>
</project>
自定义实现的过滤器
需要手动向管道中添加
package com.xiyou.netty;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.*;
/**
* 是管道中添加的过滤器
*/
public class HelloHandler extends SimpleChannelHandler {
/**
* 关闭连接,即使连接没有成功连接,我们也会在关闭的时候调用该方法
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelClosed------");
super.channelClosed(ctx, e);
}
/**
* 连接建立成功的时候,我们就会调用该方法
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelConnected------");
super.channelConnected(ctx, e);
}
/**
* 关闭连接的时候会调用该方法(只有当连接建立成功后再关闭才会调用)
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelDisconnected------");
super.channelDisconnected(ctx, e);
}
/**
* messageRecived方法抛出异常的时候调用该方法。该方法用来捕获异常
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("------exceptionCaught------");
super.exceptionCaught(ctx, e);
}
/**
* 用来接收客户端发来的消息
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
ChannelBuffer channelBuffer = (ChannelBuffer) e.getMessage();
System.out.println("------messageReceived------: " + new String(channelBuffer.array()));
super.messageReceived(ctx, e);
}
}
netty的服务端代码
package com.xiyou.netty;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* netty的服务端
* 可以利用telnet ip 端口号 进行调用
* ctrl + ] 可以发送数据
* 发送数据用 send XXX
*/
public class Server {
public static void main(String[] args) {
// 1.先创建一个服务类
ServerBootstrap bootstrap = new ServerBootstrap();
// 2.创建两个线程池,一个负责监控客户端的连接,一个负责监控客户端发送的数据
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
// 给服务类设置一个工厂
// 将两个线程池传入
// 设置NIO socket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
// 设置NIO的管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
// 先声明一个管道
ChannelPipeline pipeline = Channels.pipeline();
// 管道里装了一堆过滤器
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
// 用服务类绑定端口
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("服务端已经启动!!!");
}
}
从上面可以看到我们在自定义接受信息的时候,收到的是一个ChannelBuffer对象,我们需要手动将其转换成字符串类型,那么有没有更简单的方法呢?
我们可以在管道中添加解码器,来自动实现解码功能。
代码如下(这里只添加部分代码):
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
// 先声明一个管道
ChannelPipeline pipeline = Channels.pipeline();
// 向管道中添加一个解码的过滤器
pipeline.addLast("decode", new StringDecoder());
// 管道里装了一堆过滤器
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
从上面的代码可以看到,我们在设置NIO的管道工厂的时候,自己向里面添加了字符串的解码器,这时候我们就不需要在接受消息的时候主动对其进行转换。
/**
* 用来接收客户端发来的消息
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// ChannelBuffer channelBuffer = (ChannelBuffer) e.getMessage();
// System.out.println("------messageReceived------: " + new String(channelBuffer.array()));
System.out.println("------messageReceived------: " + e.getMessage());
super.messageReceived(ctx, e);
}
我们启动服务端之后,这里我们使用windows的cmd命令窗口进行调用
telnet 127.0.0.1 10101
telnet命令+ip+端口进行远程连接,并且按下Ctrl + 】符号我们可以进行发送消息
send hello
向服务端发送一个消息
这时候服务端打印接收到的消息hello
- 我们这里讲解下自定义Handler的重写的几个方法
(1)channelClosed
客户端关闭连接的时候,即使客户端的连接没有连接成功也会调用
(2)channelDisconnected
也是客户端关闭连接的时候使用的,但是唯一不同的是只有当客户端与服务端的连接建立成功后,再关闭才会进行调用该方法
(3)channelConnected
连接建立成功的时候我们会调用该方法
(4)messageReceived
这个方法是重点,当我们接收到客户端发来的消息的时候会调用该方法
(5)exceptionCaught
当我们接收消息的时候出现异常的时候,会执行该方法
当我们输入telnet 127.0.0.1 10101 并且向服务端发送hello字符串没有异常的时候,并主动将客户端断开连接,我们用上面的代码打印下面的内容
服务端已经启动!!!
------channelConnected------
------messageReceived------: hello
------channelDisconnected------
------channelClosed------
如果我们还希望向客户端回写数据呢?其实我们还是在receiveMessage的方法中进行简单的处理就行了,但是我们要注意,我们这里因为没有添加别的操作,所以回写的时候只能回写ChannelBuffer的类型。
我们利用getchannel().write()来进行回写。
/**
* 用来接收客户端发来的消息
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// ChannelBuffer channelBuffer = (ChannelBuffer) e.getMessage();
// System.out.println("------messageReceived------: " + new String(channelBuffer.array()));
System.out.println("------messageReceived------: " + e.getMessage());
// 回写数据
ChannelBuffer channelBuffer = ChannelBuffers.copiedBuffer("hi~".getBytes());
ctx.getChannel().write(channelBuffer);
super.messageReceived(ctx, e);
}
客户端连接
telnet 127.0.0.1 10101
send hello
按下回车
此时就会收到消息hi~
同理我们如果不想回写的数据格式是ChannelBuffer怎么办?可能大家都已经猜到了,没错就是再加一个过滤器----编码过滤器,让我们写的字符串自动转换成ChannelBuffer的格式,话不多说,上代码。
这里依旧只展出部分代码:
- 服务端
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
// 先声明一个管道
ChannelPipeline pipeline = Channels.pipeline();
// 向管道中添加一个解码的过滤器,读取的时候可以直接将其当做一个字符串
pipeline.addLast("decoder", new StringDecoder());
// 向管道中添加一个编码的过滤器,用于给客户端发送数据的时候可以直接发送一个字符串,不用将其转为ChannelBuffer格式
pipeline.addLast("encoder", new StringEncoder());
// 管道里装了一堆过滤器
pipeline.addLast("helloHandler", new HelloHandler());
return pipeline;
}
});
- handler的发送
直接发送字符串
/**
* 用来接收客户端发来的消息
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// ChannelBuffer channelBuffer = (ChannelBuffer) e.getMessage();
// System.out.println("------messageReceived------: " + new String(channelBuffer.array()));
System.out.println("------messageReceived------: " + e.getMessage());
// 回写数据
ctx.getChannel().write("yo oh heiheihei~");
super.messageReceived(ctx, e);
}
结果我们这里就不再显示了,有兴趣的可以自己试一试。
上面我们都是用cmd命令窗口当做其客户端的,这里我们可以自己写一个简单的客户端
netty的客户端
其实客户端的代码和服务端很类似
- 客户端的代码
package com.xiyou.netty;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* netty的客户端
*/
public class Client {
public static void main(String[] args) {
// 声明一个服务类
ClientBootstrap bootstrap = new ClientBootstrap();
// 声明两个线程池
// 监听和服务端的连接
ExecutorService boss = Executors.newCachedThreadPool();
// 监听和服务端的数据的交互
ExecutorService worker = Executors.newCachedThreadPool();
// socket工厂
bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
// 声明管道工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("clientHandler", new ClientHandler());
return pipeline;
}
});
// 连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10101));
System.out.println("客户端已经正常连接");
}
}
- ClientHandler:
package com.xiyou.netty;
import org.jboss.netty.channel.*;
import java.util.Scanner;
/**
* 是管道中添加的过滤器
*/
public class ClientHandler extends SimpleChannelHandler {
/**
* 关闭连接,即使连接没有成功连接,我们也会在关闭的时候调用该方法
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelClosed------");
super.channelClosed(ctx, e);
}
/**
* 连接建立成功的时候,我们就会调用该方法
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelConnected------");
Channel channel = ctx.getChannel();
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请输入");
String value = scanner.next();
if("stop".equals(value)){
break;
}
// 可以直接写字符串的原因就是设置了编码器,可以自动将字符串转换成ChannelBuffer
channel.write(value);
}
super.channelConnected(ctx, e);
}
/**
* 关闭连接的时候会调用该方法(只有当连接建立成功后再关闭才会调用)
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelDisconnected------");
super.channelDisconnected(ctx, e);
}
/**
* messageRecived方法抛出异常的时候调用该方法。该方法用来捕获异常
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("------exceptionCaught------");
super.exceptionCaught(ctx, e);
}
/**
* 用来接收客户端发来的消息
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
System.out.println("------messageReceived------: " + e.getMessage());
super.messageReceived(ctx, e);
}
}
注意一下,我们这里讲客户端给服务端发送的数据写到了channelConnected函数中,并且用其参数得到了channel,看起来上面这么写没什么问题,但是当我们一直处于循环中的时候,即使服务端给客户端进行了数据的回写我们也不能及时接受到。结果如下:
客户端已经正常连接
------channelConnected------
请输入
kj
请输入
lk
请输入
sd
请输入
stop
------messageReceived------: yo oh heiheihei~yo oh heiheihei~yo oh heiheihei~
当我们输入stop从循环中出来后,我们才能接收到服务端给客户端的回写数据,发了几次数据给服务端,就在这时候接受到几次服务端的回写数据。
所以我们这里改进了一下
/**
* 连接建立成功的时候,我们就会调用该方法
* @param ctx
* @param e
* @throws Exception
*/
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
System.out.println("------channelConnected------");
super.channelConnected(ctx, e);
}
Client
....
// 连接服务端
ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 10101));
// 这里的channel和Clienthandler中channelConnected方法中得到的channel是一个
Channel channel = connect.getChannel();
System.out.println("客户端已经正常连接");
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请输入");
// 可以直接写字符串的原因就是设置了编码器,可以自动将字符串转换成ChannelBuffer
channel.write(scanner.next());
}
此时的结果:
客户端已经正常连接
------channelConnected------
请输入
aasda
请输入
------messageReceived------: yo oh heiheihei~
asdasd
请输入
------messageReceived------: yo oh heiheihei~
stop
请输入
------messageReceived------: yo oh heiheihei~
这块我认为是主线程等待用户输入,另一个线程接收到的服务端的回写数据,就将其打印出来,所以这里我们的效果是,客户端输入一条数据得到服务端给我们的一条回写数据。
注意一点:
我们在客户端的receiveMessage方法中利用参数的方法getChannel()得到的是服务端的channel,在服务端的receiveMessage方法中利用参数的方法getMessage()得到的是客户端的channel