是什么
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients
netty是在java的nio基础上的一个异步、基于事件驱动的高性能、高可用、开源、使用java编写的网络IO框架,实现原理是reactor的主从模式,开发者可以使用netty来快速搭建一个高性能、高可用服务器
在dubbo、elasticsearch……中都使用了它
为什么要有它
解决了nio的一些缺点:
-
nio的API使用麻烦(比如:byteBuffer,每次都需要开发者调用flip())
-
使用nio实现高性能的服务器,需要开发者熟悉多线程编程 手动编写线程来实现异步、reactor模式
-
nio的可用性并不是很好,需要开发者处理tcp粘包半包、异常码流等各种情况
netty的特点:
-
支持http、tcp、udp、websocket、protobuf、文件、以及字节数组,支持nio、bio
-
支持零拷贝、解决了tcp粘包半包
-
netty的API使用简单(比如:自带线程池来实现异步处理、简化了负责监听的nioServerSocket的监听操作……),有大量开箱即用的组件,不需要手动造轮子,提高了开发效率(比如:负责处理读写的线程组、各种编解码器……)
-
完整的SSL/TLS和StartTLS支持
-
社区活跃、不断更新
应用场景
-
RPC框架中的通信组件,比如dubbo就用netty作为默认的基础通信组件
互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少, Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用
典型的应用有:阿里分布式服务框架Dubo的RPC框架使用Dubbo协议进行节点间通信, Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信
-
游戏服务器
网络游戏中服务器和客户端的交互需要延时低,而netty可以是基于tcp的,所以效率高
无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用
Netty作为高性能的基础通信组件,提供了tcp/udp和http协议栈,方便定制和开发私有协议栈,账号登录服务器
地图服务器之间可以方便的通过Netty进行高性能的通信
-
大数据领域
经典的 Hadoop的高性能通信和序列化组件(AVRO实现数据文件共享)的RPC框架,默认采用Netty进行跨界点通信
它的 Netty Service基于 Netty框架二次封装实现
(物联网也有用到netty)
demo
用netty做一个简单的http服务器,要实现的目标:
1. 启动服务器
2. 浏览器(也就是客户端)请求url:如 http://localhost:8585/?id=11&pwd=22
3. 服务器获取到请求的参数并返回响应
server端
http服务器要做的事情就是接收客户端发来的数据、对数据进行处理、响应客户端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
public class HttpServerPlus {
private final Logger logger = LoggerFactory.getLogger(HttpServerPlus.class);
private int port;
public HttpServerPlus(int port) {
this.port = port;
}
public HttpServerPlus() {
}
public void start(){
EventLoopGroup boss = null;
EventLoopGroup worker = null;
try {
ServerBootstrap boot = new ServerBootstrap();//配置服务器
boss = new NioEventLoopGroup();//只负责监听有新客户端连接的事件
// TODO netty只是一个IO框架 只负责基于NIO使用多路复用器高性能的接收客户端发来的数据,至于接收到客户端发来的数据怎么进行处理就是开发者的事了,比如:是同步处理还是异步处理都是开发者考虑的事,当然netty也提供了线程池可以进行异步处理
worker = new NioEventLoopGroup();// 无参构造方法的线程数是CPU核心数 * 2
boot.group(boss,worker)//TODO netty简化了将监听到的新client注册到worker线程池中的任意一个eventLoop的selector中的操作
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))//可以在这里绑定端口,也可以在下面的boot.bind()绑定
//option() 配置bossGroup
// 服务器端TCP内核模块维护有2个队列,我们称之为A,B吧
// 客户端向服务端connect的时候,发送带有SYN标志的包(第一次握手)
// 服务端收到客户端发来的SYN时,向客户端发送SYN ACK 确认(第二次握手)
// 此时TCP内核模块把客户端连接加入到A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
// TCP没和模块把客户端连接从A队列移到B队列,连接完成,应用程序的accept会返回
// 也就是说accept从B队列中取出完成三次握手的连接
// A队列和B队列的长度之和是backlog,当A,B队列的长之和大于backlog时,新连接将会被TCP内核拒绝
// 所以,如果backlog过小,可能会出现accept速度跟不上,A.B 队列满了,导致新客户端无法连接,
// 注意:backlog对程序支持的连接数并无影响,backlog影响的只是还没有被accept 取出的连接
.option(ChannelOption.SO_BACKLOG,1024)//SO_BACKLOG 影响与服务器发起了建立tcp连接的但是服务器还未accept()返回的客户端数量
//handler() 配置bossGroup
.handler(new LoggingHandler(LogLevel.INFO))
// ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,即 禁止使用Nagle算法
// Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效
// 负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,与TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送
// 数据,适用于文件传输。
//childOption() 配置workerGroup
.childOption(ChannelOption.TCP_NODELAY, true)
//childHandler() 配置workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//配置多个handler对有事件的client进行处理,pipeline将多个handler串联了起来,一个client的channel对应一个pipeline
ChannelPipeline pipeline = sc.pipeline();
//对http的请求和响应编解码 包括了HttpRequestDecoder、HttpResponseEncoder
pipeline.addLast(new HttpServerCodec());
//TODO 在长连接中怎么知道数据发送完毕了呢,1、通过content-length判断 传输的数据是否到达content-length的大小,
// 2、动态生成的文件没有Content-Length,它是分块传输(chunked),这时候就要根据chunked编码来判断,chunked编码的数据在最后有一个空chunked块,表明本次传输数据结束
pipeline.addLast(new ChunkedWriteHandler());//TODO 应该和content-length不用同时存在吧,待证实!
// netty对于post的Http请求分为了HttpRequest 和 HttpContent 两个部分,HttpRequest 主要包含请求头、请求方法等信息,HttpContent 主要包含请求体的信息。
// HttpObjectAggregator可以将请求合并为单一的一个FullRequest
pipeline.addLast(new HttpObjectAggregator(65536));
// 这个方法的作用是: http 100-continue用于客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,如果不处理,
// 客户端则不上传POST数据,如果处理,则POST上传数据。在现实应用中,通过在POST大数据时,才会使用100-continue协议
pipeline.addLast(new HttpServerExpectContinueHandler());
//自定义handler
pipeline.addLast(new ServerRequestHandler());
}
});
//绑定端口,开始接收客户端连接
//netty中的IO操作都是异步的,比如:bind、connect、read/write
ChannelFuture future = boot.bind().sync();//ChannelFuture可以异步,这里绑定端口是同步 也可以异步绑定端口,然后看回调是否成功
logger.info("HttpServerPlus 启动了");
//bind()后的回调
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (future.isSuccess()){
logger.info("服务端绑定" + port + "端口成功");
} else {
logger.error("服务端绑定" + port + "端口失败");
Throwable cause = future.cause();
logger.error("原因:" + cause.getMessage());
}
}
});
//阻塞等待服务器的channel关闭之后再退出main 程序结束,优雅的关闭服务器
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//优雅的关闭,释放线程池资源
boss.shutdownGracefully();
worker.shutdownGracefully();
logger.info("ChatClient 关闭了");
}
}
}
netty的服务端和客户端的代码都是一个模板,代码就这么多,主要做了:
1. ServerBootstrap 是一个引导类,用来配置服务器
2. EventLoopGroup 是一个线程组 也就是线程池,里面的每个线程都有一个selector多路复用器,监听有事件的连接,服务器配置了2个EventLoopGroup线程组,一个专门用来监听新的客户端连接 里面只有1个线程,就相当于公司的前台负责接待陌生来客,一个负责监听已建立连接的客户端 里面有多个线程,相当于公司的专员 负责有业务往来的熟客
3. 定义服务器使用NIO模式
4. 一些各种配置项
5. (重要的来了,服务器要对有事件的客户端做处理了)
添加通道初始化类,我这里把它写成了一个匿名内部类,也可以单独写一个类去继承ChannelInitializer
SocketChannel 是一个有事件的客户端channel,一个channel对应一个ChannelPipeline
ChannelPipeline将所有对有事件的客户端执行的操作都串联了起来,即 一个ChannelPipeline可以有多个handler,netty是事件驱动的,当有事件的客户端channel发送来数据后就会调用ChannelPipeline中定义好的handler进行处理,也就可以把handler看做拦截器,netty提供了大量的handler类方便我们拿来即用,所以我们就只需要编写和自己业务相关的handler就好了
上面首先使用netty提供的HttpServerCodec 对接收和发送的数据进行编解码,做了一些前置处理,最后使用我们自 定义的handler做处理
handler类
import com.alibaba.fastjson.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.util.CharsetUtil;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.codec.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.List;
import java.util.Map;
public class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final Logger logger = LoggerFactory.getLogger(ServerRequestHandler.class);
/**
* 主要处理在channelRead0()中,其他方法都是生命周期中的方法
* @param ctx
* @param req
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
String uri = req.uri();
HttpMethod method = req.method();
Channel channel = ctx.channel();
SocketAddress socketAddress = channel.remoteAddress();
//用浏览器发起 HTTP 请求时,常常会被 uri = "/favicon.ico" 所干扰,因此最好对其特殊处理
if (uri.equals("/favicon.ico")) {
return;
}
if (method.equals(HttpMethod.GET)) {
System.out.println("get request");
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);
Map<String, List<String>> parameters = queryStringDecoder.parameters();
parameters.entrySet().forEach(entry->{
System.out.println("paramName: " + entry.getKey() + " value: " + entry.getValue().toString());
});
}else if (method == (HttpMethod.POST)){
System.out.print("post request ");
String contentType = getContentType(req);
if (contentType.indexOf("application/json") >= 0) {
System.out.println("application/json");
String s = req.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
JSONObject jsonObject = JSONObject.parseObject(s);
for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
System.out.println("paramName: " + entry.getKey() + " value: " + entry.getValue().toString());
}
}else if (contentType.indexOf("application/x-www-form-urlencoded") >= 0){
System.out.println("application/x-www-form-urlencoded");
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req);
List<InterfaceHttpData> bodyHttpDatas = decoder.getBodyHttpDatas();
bodyHttpDatas.forEach(body ->{
Attribute data = (Attribute) body;
try {
System.out.println("paramName: " + data.getName() + " value: " + data.getValue());
} catch (IOException e) {
e.printStackTrace();
}
});
}else if (contentType.indexOf("multipart/form-data") >= 0){
//用于文件上传
System.out.println("multipart/form-data");
}
}
//因为是http所以使用netty提供的DefaultFullHttpResponse来做响应
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,Unpooled.copiedBuffer("请求成功",CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,response.content().readableBytes());
boolean keepAlive = HttpUtil.isKeepAlive(req);
if (keepAlive) {
}else {
response.headers().set(HttpHeaderNames.CONNECTION,keepAlive);
}
//ctx.channel() 流经整个pipeline
ctx.channel().writeAndFlush(response)
.addListener(ChannelFutureListener.CLOSE)//TODO netty用做http服务器需要有这个关闭客户端连接的配置,因为http是短连接,虽然http1.1默认是长连接
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Channel ch = future.channel();
if (future.isSuccess()) {
logger.info("write successful");
}else {
logger.error("write error");
logger.error("cause: " + future.cause().getMessage());
}
}
});
}
public String getContentType(FullHttpRequest request){
String s = request.headers().get("Content-Type");
return s;
}
//服务器读取完成的回调
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
//发生异常的回调
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.error("exception cause: " + cause.getMessage());
ctx.close();//关闭通道,也可以写作:ctx.channel().close()
}
}
自定义的handler需要继承 SimpleChannelInboundHandler 或者 ChannelInboundHandlerAdapter,重写它的channelRead0() 或者 channelRead(),对客户端发来的数据进行处理,其他的方法就是一些生命周期的回调方法
ChannelHandlerContext 是每个handler的上下文
FullHttpRequest 包装了客户端发来的数据
一个ChannelPipeline中除了有多个handler,还有多个ChannelHandlerContext,而每个ChannelHandlerContext对应一个ChannelHandler,可以通过ChannelHandlerContext获取到当前ChannelPipeline、Channel、ChannelHandler
启动http服务器
上面就是http服务器的完整处理流程,现在只需要在main函数中将其启动即可
public class HttpServerPlusApplication {
public static void main(String[] args) {
HttpServerPlus server = new HttpServerPlus(8585);// 服务器端口号
server.start();
}
}
浏览器访问:http://localhost:8585/?id=11&pwd=22,服务器获取到: