启动springboot的主类
@SpringBootApplication
@Configuration
@EnableAutoConfiguration
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
通过@Configuration注解在springboot服务启动时注入,
@Bean(initMethod = "start", destroyMethod = "stop")
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "netty.server",value = "enabled",havingValue = "true")
NettyServer nettyServer (){
return new NettyServer( properties);
}
这段代码中initMethod代表此bean对象初始化创建时调用该对象的start方法destroyMethod= "stop"表示该bean对象销毁时调用该bean的stop对象@ConditionalOnMissingBean注解表示如果在spring容器中有该bean对象则不创建,如果没有该对象则初始化创建一个新的该bean对象 ConditionalOnProperty(prefix = "netty.server",value = "enabled",havingValue = "true")表示从resource文件夹下面的application.property文件中读取属性名以netty.server开头的属性@Value注解表示从application.property文件中读取向对应的属性。所以以后项目中就可以把一下在项目中容易修改的属性值放在application.property文件中。这样即使项目打包了也可以修改application.property文件中的属性来配置项目。简单灵活。
@Configuration
public class WebSocketConfiguration {
@Autowired
private NettyServerProperties properties;
@Value("${httpclient.connecttimeout:1500}")
private int connectTimeout;
@Value("${httpclient.requesttimeout:1500}")
private int readTimeout;
@Value("${httpclient.requesttimeout:3000}")
private int requestTimeout;
@Bean(initMethod = "start", destroyMethod = "stop")
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "netty.server",value = "enabled",havingValue = "true")
NettyServer nettyServer (){
return new NettyServer( properties);
/**
* 后续的HTTP 访问完全依靠这个类生成 AsyncHttpClient 对象
*
* @param factory
* @return
*/
@Bean(destroyMethod="close")
public AsyncHttpClient asyncHttpClient(AsyncHttpClientConfig config) {
return new DefaultAsyncHttpClient(config);
}
@Bean
public AsyncHttpClientConfig clientConfig() {
AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
.setWebSocketMaxFrameSize(properties.getMaxSize())
.setConnectTimeout(connectTimeout)
.setReadTimeout(readTimeout)
.setRequestTimeout(requestTimeout)
.build();
return config;
}
}
在前面当NettyServer的对象bean初始化时启动start方法,start利用java8的lambda表达式来开启一个线程去调用startServer方法开启netty服务。
其中的ApplicationContextAware接口用来 为了让Bean获取它所在的Spring容器,可以让该Bean实现ApplicationContextAware接口 为了让Bean获取它所在的Spring容器,可以让该Bean实现ApplicationContextAware接口
public class NettyServer implements ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);
private NettyServerProperties properties;
private Channel serverChannel;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public NettyServer(NettyServerProperties properties) {
this.properties = properties;
}
public void start() {
//需要开启一个新的线程来执行netty server 服务器
Thread t = new Thread(()->startServer());
//t.setDaemon(true);
t.setName("netty thread");
t.start();
}
public void stop() {
if(serverChannel != null) {
try {
serverChannel.close().get(2L, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException |TimeoutException e) {
}
serverChannel = null;
}
}
此方法用来配置开启netty服务其中EventLoopGroup表示netty的线程池,bootstrap.group(boss,work)表示绑定线程池channel表示// 指定使用的channel
.localAddress( port )// 绑定监听端口 .childHandler(new
ChannelInitializer<SocketChannel>() { // 用来绑定客户端连接时候触发操作
ChannelFuture cf =
sb.bind().sync(); // 服务器异步创建绑定 cf.channel().closeFuture().sync();
// 关闭服务器通道
private void startServer() {
//服务端需要2个线程组 boss线程组处理客户端连接 work线程组进行客服端连接之后的处理
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
//服务器 配置
//绑定线程池 //指定使用的channel bootstrap.group(boss,work).channel(NioServerSocketChannel.class) .childHandler(newChannelInitializer<SocketChannel>() { //初始化ChannelIntialize
protected void initChannel(SocketChannel socketChannel) throws Exception {
// HttpServerCodec:将请求和应答消息解码为HTTP消息
socketChannel.pipeline().addLast("http-codec",new HttpServerCodec());
// HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
socketChannel.pipeline().addLast("aggregator",new HttpObjectAggregator(properties.getMaxSize()));
// ChunkedWriteHandler:向客户端发送HTML5文件
socketChannel.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
// 进行设置心跳检测
socketChannel.pipeline().addLast(new IdleStateHandler(properties.getIdleTime(),
properties.getIdleTime(), properties.getIdleTime()*10, TimeUnit.SECONDS));
// 配置通道处理 来进行业务处理
//指定客户端触发操作 WebSocketServerHandler 类 socketChannel.pipeline().addLast(applicationContext.getBean(WebSocketServerHandler.class));
}
}).option(ChannelOption.SO_BACKLOG,1024).childOption(ChannelOption.SO_KEEPALIVE,true);
//绑定端口 开启事件驱动
LOGGER.info("【服务器启动成功========端口:" + properties.getPort() + "】");
serverChannel = bootstrap.bind(properties.getPort()).sync().channel();
serverChannel.closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭资源
boss.shutdownGracefully();
work.shutdownGracefully();
}
}
}
SimpleChannelInboundHandler类
@Component
@Scope("prototype") // 必须是prototype!!!
public class WebSocketServerHandler extends
SimpleChannelInboundHandler<Object> implements ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServerHandler.class);
private ApplicationContext applicationContext;
private Map<ChannelId, WebSocketSessionHandler> handlers = new
ConcurrentHashMap<>();
@Autowired
NettyServerProperties properties;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override //业务的入口 ctx是channel的关键用来将channel关键起来
public void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest)
msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
// Handle a bad request.
boolean iswebSocket = "websocket".equals(req.headers().get("Upgrade"));
if (!req.decoderResult().isSuccess() || !iswebSocket) {
sendHttpResponse(ctx, req, new
DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
String uri = req.uri();
if(iswebSocket) {
WebSocketServerHandshakerFactory wsFactory = new
WebSocketServerHandshakerFactory(uri, null, true, properties.getMaxSize());
WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
ChannelFuture channelFuture = handshaker.handshake(ctx.channel(), req);
channelFuture.addListener(f->{
// 握手成功之后,业务逻辑
if(f.isSuccess()) {
newWebSocketSession(ctx, req, handshaker);
} else {
sendHttpResponse(ctx, req, new
DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
}
});
try {
channelFuture.await(0);
} catch (InterruptedException e) {
}
}
} else {
processHttp(ctx, req);
}
}
private void newWebSocketSession(ChannelHandlerContext ctx, FullHttpRequest req,
WebSocketServerHandshaker handshaker) {
URI uri = null
try {
uri = new URI(req.uri());
} catch (URISyntaxException e) {
LOGGER.error(e.getMessage(), e);
return;
}
//这里的代码比较丑陋,先这样处
//后续看有没有优雅一些的方
WebSocketSessionHandler handler;
if(uri.getPath().startsWith("/dds/v1")) {
handler = applicationContext.getBean(DDSSessionHandler.class, ctx, handshaker);
} else if(uri.getPath().startsWith("/asr/v1")) {
handler = applicationContext.getBean(DummyAsrSessionHandler.class, ctx, handshaker);
} else {
handler = applicationContext.getBean(DefaultSessionHandler.class, ctx, handshaker);
}
handlers.put(ctx.channel().id(), handler);
}
private void processHttp(ChannelHandlerContext ctx, FullHttpRequest req) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof CloseWebSocketFrame) {
WebSocketSessionHandler
handler = handlers.get(ctx.channel().id());
if(handler != null) {
handler.closeSession((CloseWebSocketFrame)frame);
}
return;
}
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
} else {
WebSocketSessionHandler
handler = handlers.get(ctx.channel().id());
if(handler != null) {
handler.processFrame(frame);
}
else {
ctx.channel().writeAndFlush(new
TextWebSocketFrame("{}"));
}
}
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() !=
200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
WebSocketSessionHandler
handler = handlers.remove(ctx.channel().id());
if(handler != null) {
handler.clear();
}
}
}
Netty 服务器的通信步骤为:
1.创建两个NIO线程组,一个专门用于接收来自客户端的连接,另一个则用于处理已经被接收的连接。
2.创建一个ServerBootstrap对象,配置Netty的一系列参数,例如接受传出数据的缓存大小等。
3.创建一个用于实际处理数据的类ChannelInitializer,进行初始化的准备工作,比如设置接受传出数据的字符集、格式以及实际处理数据的接口。
4.绑定端口,执行同步阻塞方法等待服务器端启动即可。
在Netty里,Channel是通讯的载体,而ChannelHandler负责Channel中的逻辑处理。一个Channel包含一个ChannelPipeline,所有ChannelHandler都会注册到ChannelPipeline中,并按顺序组织起来
每个ChannelHandler 被添加到ChannelPipeline 后,都会创建一个ChannelHandlerContext 并与之创建的ChannelHandler 关联绑定
ChannelHandler通过ChannelHandlerContext来操作channel和channelpipeline
1.Channel:
1 工作原理
Channel是Netty的核心概念之一,它是Netty网络通信的主体,由它负责同对端进行网络通信、注册和数据操作等功能。
一旦用户端连接成功,将新建一个channel同该用户端进行绑定,channel从EventLoopGroup获得一个EventLoop,并注册到该EventLoop,channel生命周期内都和该EventLoop在一起(注册时获得selectionKey)
channel同用户端进行网络连接、关闭和读写,生成相对应的event(改变selectinKey信息),触发eventloop调度线程进行执行,如果是读事件,执行线程调度pipeline来处理用户业务逻辑。
2线程
多个channel可以注册到一个eventloop上,所有的操作都是顺序执行的,eventloop会依据channel的事件调用channel的方法进行相关操作,每个channel的操作和处理在eventloop中都是顺序的。
2. ChannelPipeline和ChannelHandler
ChannelPipeline和ChannelHandler用于channel事件的拦截和处理,Netty使用类似责任链的模式来设计ChannelPipeline和ChannelHandler
ChannelPipeline相当于ChannelHandler的容器,channel事件消息在ChannelPipeline中流动和传播,相应的事件能够被ChannelHandler拦截处理、传递、忽略或者终止。
基于ChannelHandler开发业务逻辑,基本不需要关心网络通讯方面的事情,专注于编码/解码/逻辑处理就可以了。Handler也是比较方便的开发模式,在很多框架中都有用到。
Channel启动初始化的时候,默认是DefaultChannelPipeline,Channel总是对应一个pipeline
在讲Inbound事件传播机制的时候,说过每次传播会遍历Pipeline中的Handler然后找到Inbound类型进行调用
2.1 INBOUD和OUTBOUND事件
inbound:当发生某个I/O操作时由IO线程流向用户业务处理线程的事件,如链路建立、链路关闭或者读完成等
outbound:由用户线程或者代码发起的IO操作事件
2.2 ChannelHandlerContext
每个ChannelHandler 被添加到ChannelPipeline 后,都会创建一个ChannelHandlerContext 并与之创建的ChannelHandler 关联绑定
ChannelHandler通过ChannelHandlerContext来操作channel和channelpipeline
ChannelHandler负责I/O事件或者I/O操作进行拦截和处理,用户可以通过ChannelHandlerAdapter来选择性的实现自己感兴趣的事件拦截和处理。
由于Channel只负责实际的I/O操作,因此数据的编解码和实际处理都需要通过ChannelHandler进行处理。
ChannelPipeline是线程安全的,多个业务线程可以并发的操作ChannelPipeline;ChannelHandler不是线程安全的,用户需要自己保重ChannelHandler的线程安全
ChannelHandlerContext保存了Netty与Handler相关的的上下文信息。而咱们这里的DefaultChannelHandlerContext,则是对ChannelHandler的一个包装。
一个DefaultChannelHandlerContext内部,除了包含一个ChannelHandler,还保存了"next"和"prev"两个指针,从而形成一个双向链表。
3. ChannelFuture与ChannelPromise
在Netty中,所有的I/O操作都是异步的,因此调用一个I/O操作后,将继续当前线程的执行,但I/O操作的结果怎么获得?——ChannelFuture。
回调方法通过监听的形式实现:ChannelFutureListener。
Channel
是连接的通道,是ChannelEvent的产生者,而ChannelPipeline
可以理解为ChannelHandler的集合。
一个Channel在创建的时候就会创建一个对应的ChannelPipeline,
通过上面的分析,可以看到ChannelPipeline的设计是线程安全的,有很多地方的操作就是为了这个线程安全做了很多的操作,例如addLast调用handlerAdded会转换成EventLoop队列任务,Netty中很多地方都是类似的,为了避免这种多线程操作的问题都是先转成队列的任务,从而转换成单线程的操作,这种设计需要好好琢磨
Netty中随着一个Channel的创建,会连带创建一个ChannelPipeline,这个ChannelPipeline就像一个处理各种事件的管道,负责去处理Channel上发生的事件,例如连接事件,读事件,写事件等。
更深入的说,处理的并不是ChannelPipeline,而是ChannelPipeline中一个个的ChannelHandler,其结构如下
ChannelPipeline中有很多Handler(其实是Context类型,Context封装了Handler),组成了一个双向的链表,同时初始化的时候就会带有一个头结点和尾结点,自定义的ChannelHandler都会添加到Head和Tail之间。
同时Netty定义了两种事件:
inbound:事件从Head往Tail方向传递,实现ChannelInboundHandler的ChannelHandler为处理inbound事件的ChannelHandler
outbound:事件从Tail往Head方向传递,实现ChannelOutboundHandler的ChannelHandler为处理inbound事件的ChannelHandlerContext和Pipeline、Handler、Channel、EventLoop是一对一的关系。
将一个自定义的ChannelHandler加入到了ChannelPipeline中
直接使用Channel.writeAndFlush会从Tail开始传播,而使用ctx.writeAndFlush则是从当前Handler开始往前传播
ChannelPipeline的设计是线程安全的,有很多地方的操作就是为了这个线程安全做了很多的操作,例如addLast调用handlerAdded会转换成EventLoop队列任务,Netty中很多地方都是类似的,为了避免这种多线程操作的问题都是先转成队列的任
在Netty 中, 一个 NioEventLoop 通常需要肩负起两种任务, 第一个是作为 IO 线程, 处理 IO 操作; 第二个就是作为任务线程, 处理 taskQueue 中的任务.
netty的程序的启动(在服务端一般是两个NioEventLoopGroup线程池,一个boss, 一个worker; 对于客户端一般是一个线程池)。我们一般是使用NIO,所以本文也全部是基于NIO来分析的,当然netty也支持BIO。
netty之所以是高性能NIO框架,其中主要贡献之一就是netty的线程模型的高性能,我们都知道netty的线程模型是基于Reactor线程模型
Reactor线程模型
Reactor线程模型有三种
- 单线程模型
- 多线程模型
- 主从Reactor线程模型
NIOEventLoopGroup就是一个线程池实现,通过设置不同的NIOEventLoopGroup方式就可以对应三种不同的Reactor线程模型。
这里我只给出服务端的配置,对于客户端都是一样的。单线程模型:
-
NIO中则直接将buffer这个概念封装成了对象,其中最常用的大概是ByteBuffer了。于是使用方式变为了:将数据写入Buffer,flip()一下,然后将数据读出来。于是,buffer的概念更加深入人心了!
在Netty里,Channel
是通讯的载体,而ChannelHandler
负责Channel中的逻辑处理。一个Channel包含一个ChannelPipeline,所有ChannelHandler都会注册到ChannelPipeline中,并按顺序组织起来。在Netty中,ChannelEvent
是数据或者状态的载体,例如传输的数据对应MessageEvent
,状态的改变对应ChannelStateEvent
。当对Channel进行操作时,会产生一个ChannelEvent,并发送到ChannelPipeline
。ChannelPipeline会选择一个ChannelHandler进行处理。这个ChannelHandler处理之后,可能会产生新的ChannelEvent,并流转到下一个ChannelHandler。Netty的ChannelPipeline包含两条线路:Upstream和Downstream。Upstream对应上行,接收到的消息、被动的状态改变,都属于Upstream。Downstream则对应下行,发送的消息、主动的状态改变,都属于Downstream。ChannelPipeline
接口包含了两个重要的方法:sendUpstream(ChannelEvent e)
和sendDownstream(ChannelEvent e)
,就分别对应了Upstream和Downstream。ChannelPipeline
的主要的实现代码在DefaultChannelPipeline
类里。ChannelHandlerContext保存了Netty与Handler相关的的上下文信息。而咱们这里的DefaultChannelHandlerContext
,则是对ChannelHandler
的一个包装。一个DefaultChannelHandlerContext
内部,除了包含一个ChannelHandler
,还保存了"next"和"prev"两个指针,从而形成一个双向链表。ChannelHandler的编程模型,基于ChannelHandler开发业务逻辑,基本不需要关心网络通讯方面的事情,专注于编码/解码/逻辑处理就可以了。Handler也是比较方便的开发模式,在很多框架中都有用到。每一个channel都需要有相应的channelPipeline,当为channel设置了channelPipeline后就不能再为channel重新设置 channelPipeline。应该最好是通过Channels 这个帮助类来生成ChannelPipeline 而不是自己去构建ChannelPipeline 。
ChannelGroup :channel的组集合,他包含一个或多个open的channel,closed channel会自动从group中移除,一个channel可以在一个或者多个channelGroup 如果想将一个消息广播给多个channel,可以利用group来实现
ChannelFuture :在netty中,所有的io传输都是异步,所有那么在传送的时候需要数据+状态来确定是否全部传送成功,而这个载体就是ChannelFuture
Channel
是连接的通道,是ChannelEvent的产生者,而ChannelPipeline
可以理解为ChannelHandler的集合在使用传统的ServerSocket和Socket的时候 很多时候程序是会阻塞的
比如 serversocket.accept() , socket.getInputStream().read() 的时候都会阻塞 accept()方法除非等到客户端socket的连接或者被异常中断 否则会一直等待下去
read()方法也是如此 除非在输入流中有了足够的数据 否则该方法也会一直等待下去知道数据的到来.在ServerSocket与Socket的方式中 服务器端往往要为每一个客户端(socket)分配一个线程,而每一个线程都有可能处于长时间的阻塞状态中.而过多的线程也会影响服务器的性能.在JDK1.4引入了非阻塞的通信方式,这样使得服务器端只需要一个线程就能处理所有客户端socket的请求.
下面是几个需要用到的核心类
ServerSocketChannel: ServerSocket 的替代类, 支持阻塞通信与非阻塞通信.
SocketChannel: Socket 的替代类, 支持阻塞通信与非阻塞通信.
Selector: 为ServerSocketChannel 监控接收客户端连接就绪事件, 为 SocketChannel 监控连接服务器就绪, 读就绪和写就绪事件.
SelectionKey: 代表 ServerSocketChannel 及 SocketChannel 向 Selector 注册事件的句柄. 当一个 SelectionKey 对象位于Selector 对象的 selected-keys 集合中时, 就表示与这个 SelectionKey 对象相关的事件发生了.在SelectionKey 类中有几个静态常量。