Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
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程序作者的技术实力了。
1) NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
2) 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
3) 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
4) JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决
如第一部分所述,netty是一款收到大公司青睐的框架,在我看来,netty能够受到青睐的原因有三:
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:
阻塞IO的通信方式
非阻塞IO的通信方式
从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector
。
当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。
在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是阻塞的,这样就能让一个Thread处理更多的请求了。
下面两张图是基于BIO的处理流程和netty的处理流程,辅助你理解两种方式的差别:
BIO的处理流程
NIO的处理流程
除了BIO和NIO之外,还有一些其他的IO模型,下面这张图就表示了五种IO模型的处理流程:
五种常见的IO模型
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
要说Netty为什么封装好,这种用文字是说不清的,直接上代码:
public class PlainOioServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); //1
try {
for (;;) {
final Socket clientSocket = socket.accept(); //2
System.out.println("Accepted connection from " + clientSocket);
new Thread(new Runnable() { //3
@Override
public void run() {
OutputStream out;
try {
out = clientSocket.getOutputStream();
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4
out.flush();
clientSocket.close(); //5
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); //6
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class PlainNioServer {
public void serve(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address); //1
Selector selector = Selector.open(); //2
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3
final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
for (;;) {
try {
selector.select(); //4
} catch (IOException ex) {
ex.printStackTrace();
// handle exception
break;
}
Set readyKeys = selector.selectedKeys(); //5
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) { //6
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE |
SelectionKey.OP_READ, msg.duplicate()); //7
System.out.println(
"Accepted connection from " + client);
}
if (key.isWritable()) { //8
SocketChannel client =
(SocketChannel)key.channel();
ByteBuffer buffer =
(ByteBuffer)key.attachment();
while (buffer.hasRemaining()) {
if (client.write(buffer) == 0) { //9
break;
}
}
client.close(); //10
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
// 在关闭时忽略
}
}
}
}
}
}
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); //1
b.group(group) //2
.channel(OioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer() {//3
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
}
});
}
});
ChannelFuture f = b.bind().sync(); //6
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync(); //7
}
}
}
从代码量上来看,Netty就已经秒杀传统Socket编程了,但是这一部分博大精深,仅仅贴几个代码岂能说明问题,在这里给大家介绍一下Netty的一些重要概念,让大家更理解Netty。
Channel
数据传输流,与channel相关的概念有以下四个,上一张图让你了解netty里面的Channel。
Channel一览
基于netty+websocket的在线聊天室,能实现简单消息聊天,统计在线人数 。在线聊天室选择了使用 websocket 协议来实现长连接,类似场景如 IM,服务端即时推送等都使用了这个协议。netty 是业内主流的 NIO 框架,netty 对 Java NIO 做了封装,让开发者更多关注业务,降低开发成本。很多著名的 RPC 框架都采用了 netty 作为传输层,友好的 API,功能强大,内置了很多编解码协议,实现 websocket 协议也是十分方便。
netty5 + webSocket + springboot + freemarker + layui
public class ChatServer {
private int port =8002;
public void start() throws Exception {
//boss线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
//worker线程
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//netty服务,启动引擎
ServerBootstrap b = new ServerBootstrap();
//主从模式
b.group(bossGroup,workerGroup)
//主线程处理类
.channel(NioServerSocketChannel.class)
//配置信息
.option(ChannelOption.SO_BACKLOG,1024)// 针对主线程配置
// .childOption(ChannelOption.SO_KEEPALIVE,true);//子线程配置
//子线程的处理,Handler
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline =channel.pipeline();
// pipeline.addLast("idle", new ImIdleHandler(3600,3600,3600)); // 心跳检测
pipeline.addLast("http-codec", new HttpServerCodec()); // Http消息编码解码
pipeline.addLast("aggregator", new HttpObjectAggregator(65536)); // Http消息组装
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); // WebSocket通信支持
//http请求的业务逻辑处理
pipeline.addLast("handler", new WebSocketHandler()); // WebSocket服务端Handler
}
});
//等待客户端连接
ChannelFuture f = b.bind(port).sync();
System.out.println("chat-server start. port "+this.port);
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args){
try {
new ChatServer().start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@WebListener
public class NettyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.err.println("nettyListener Startup!");
new Thread(){
@Override
public void run(){
try {
new ChatServer().start();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
System.err.println("nettyListener end!");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler
/**消息处理类*/
public class MsgProcessor {
//记录在线用户
private static ChannelGroup onlineUsers = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
//自定义IM协议的解码器
private IMDecoder decoder = new IMDecoder();
//自定义IM协议的编码器
private IMEncoder encoder = new IMEncoder();
//channel自定义属性
private final AttributeKey USERNAME = AttributeKey.valueOf("username");
private final AttributeKey HEAD_PIC = AttributeKey.valueOf("headPic");
private final AttributeKey IP_ADDR = AttributeKey.valueOf("ipAddr");
private final AttributeKey ATTRS = AttributeKey.valueOf("attrs");
public void process(Channel client,String msg){
//将字符串解析为自定义格式
IMMessage request = decoder.decode(msg);
if(null == request){return;}
//获取消息发送者
String username = request.getSender();
//判断如果是登录动作,就往onlineUsers中加入一条数据
if(IMP.LOGIN.getName().equals(request.getCmd())){
client.attr(IP_ADDR).getAndSet("");
client.attr(USERNAME).getAndSet(request.getSender());
client.attr(HEAD_PIC).getAndSet(request.getHeadPic());
onlineUsers.add(client);
//像所有用户发送系统消息
for (Channel channel : onlineUsers) {//向其他人发送消息
if (channel != client) {
//自定义系统消息格式 [system][时间戳][用户数量][消息内容]
request = new IMMessage(IMP.SYSTEM.getName(), sysTime(), onlineUsers.size(), username + " 加入聊天室!");
}
//向自己发送消息
else {
request = new IMMessage(IMP.SYSTEM.getName(), sysTime(), onlineUsers.size(), username + " 欢迎进入cy聊天室!");
}
//自定义IM协议解码
String text = encoder.encode(request);
//发送消息
channel.writeAndFlush(new TextWebSocketFrame(text));
}
}
//如果是登出
else if(IMP.LOGOUT.getName().equals(request.getCmd())){
logout(client);
}
//如果是聊天信息
else if(IMP.CHAT.getName().equals(request.getCmd())){
for (Channel channel : onlineUsers) {//向其他人发送消息
if (channel != client) {
request.setSender(username);
}
//向自己发送消息
else {
request.setSender("MY_SELF");
}
//自定义IM协议解码
String text = encoder.encode(request);
//发送消息
channel.writeAndFlush(new TextWebSocketFrame(text));
}
}
//如果是鲜花
else if (IMP.FLOWER.getName().equals(request.getCmd())){
JSONObject attrs = getAttrs(client);
long currTime = sysTime();
if(null != attrs){
long lastTime = attrs.getLongValue("lastFlowerTime");
//60秒之内不允许重复刷鲜花
int seconds = 10;
long sub = currTime - lastTime;
if(sub < 1000 * seconds){
request.setSender("MY_SELF");
request.setCmd(IMP.SYSTEM.getName());
request.setContent("您送鲜花太频繁," + (seconds - Math.round(sub / 1000)) + "秒后再试");
String content = encoder.encode(request);
client.writeAndFlush(new TextWebSocketFrame(content));
return;
}
}
//正常送花
for (Channel channel : onlineUsers) {
if (channel == client) {
request.setSender("MY_SELF");
request.setContent("你给大家送了一波鲜花雨");
setAttrs(client, "lastFlowerTime", currTime);
}else{
request.setSender(getNickName(client));
request.setContent(getNickName(client) + "送来一波鲜花雨");
}
request.setTime(sysTime());
String content = encoder.encode(request);
channel.writeAndFlush(new TextWebSocketFrame(content));
}
}
}
/**
* 获取用户昵称
* @param client
* @return
*/
public String getNickName(Channel client){
return client.attr(USERNAME).get();
}
/**
* 获取用户远程IP地址
* @param client
* @return
*/
public String getAddress(Channel client){
return client.remoteAddress().toString().replaceFirst("/","");
}
/**
* 获取扩展属性
* @param client
* @return
*/
public JSONObject getAttrs(Channel client){
try{
return client.attr(ATTRS).get();
}catch(Exception e){
return null;
}
}
/**
* 获取扩展属性
* @param client
* @return
*/
private void setAttrs(Channel client,String key,Object value){
try{
JSONObject json = client.attr(ATTRS).get();
json.put(key, value);
client.attr(ATTRS).set(json);
}catch(Exception e){
JSONObject json = new JSONObject();
json.put(key, value);
client.attr(ATTRS).set(json);
}
}
/**
* 登出通知
* @param client
* @return
*/
public void logout(Channel client) {
IMMessage request = new IMMessage();
request.setSender(client.attr(USERNAME).get());
request.setCmd(IMP.SYSTEM.getName());
request.setOnline(onlineUsers.size());
request.setContent(request.getSender()+" 退出聊天室!");
//向所有用户发送系统消息
for (Channel channel : onlineUsers) {//向其他人发送消息
if (channel != client) {
//自定义IM协议解码
String text = encoder.encode(request);
//发送消息
channel.writeAndFlush(new TextWebSocketFrame(text));
}
}
onlineUsers.remove(client);
}
private long sysTime(){
return System.currentTimeMillis();
}
}