Netty简介
前段时间要实现与一些物联网设备的通信的需求,设备一般都是使用TCP协议进行通信,经过调研觉得Netty确实是实现这一需求的最好框架,经过一番研究觉得Netty这个框架确实太强大了,自己只是使用了其中的一部分功能,先记录一下以后慢慢品味吧。
Netty官网(https://netty.io/)给出的定义是:Netty 是 NIO 客户端/服务器框架,可快速轻松地开发网络应用程序,如协议服务器和客户端。它大大简化和简化了网络编程的难度,如实现一个TCP和UDP的服务端程序。
Netty原理
从官网的介绍可以看出Netty是基于NIO的实现,那么NIO又是什么呢?谈起NIO就不得不说他的前一代BIO(Blocking IO)
- BIO(Blocking IO)阻塞式IO
通俗一点说就是一个客户端和一个服务端通信时同一时间内只能处理一个请求,此时如果有新请求,那么就被阻塞了,只有等待上一个请求完成了才能进行下一个请求的处理。这样的结构导致如果要实现多客户端的处理就要不断增加线程数。而线程数的增加会给上下文切换带来很大的开销,你想也是人多了不好管理就是这个道理。
BIO模型
- NIO(Non Blocking IO)非阻塞式IO
从NIO的名字就可以知道NIO就是为了解决BIO的这种弊端而产生的,以下是NIO的模型
从图中可以看出NIO多了一个Selector大管家,所有的读写请求先到Selector报道,然后由它分配给相应的线程进行处理。这样就会避免大量线程的堆积,用少量的线程就可以进行大量连接的处理,线程复用了嘛。
那么Netty就是基于NIO的模型进一步封装而实现,再加之一些性能优化和面向对象的编程思想使之在易用性,高并发性能以及安全性上都有很大的提升,以下是Netty大致的工作流程以及组件。
netty工作流程图
BossGroup: 主线程组,主要负责调度
WorkerGroup: 工作线程组,主要是处理具体工作
Channel:管道,用来连接两端,它的实例可以是一个Socket
Handler:处理器,可以进行编解码啊,消息处理等等具体工作
Springboot整合Netty
- 新建一个Springboot工程,并引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.17.Final</version>
</dependency>
- 创建NettyServer
public class NettyServer implements Runnable{
@Override
public void run() {
//指定端口服务这里使用13001端口
InetSocketAddress socketAddress = new InetSocketAddress( 13001);
this.start(socketAddress);
}
/**
* 开启服务
* @param socketAddress
*/
public void start(InetSocketAddress socketAddress) {
//new 一个主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//new 一个工作线程组
EventLoopGroup workGroup = new NioEventLoopGroup(200);
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
//设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,开始接收进来的连接
try {
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("服务器开启失败", e);
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
}
1、这里实现了Runnable接口单独起一个线程为了和springboot的端口分开
2、Netty服务的初始化ServerBootstrap采用Builder模式需要指定线程组,管道,管道处理器,端口以及一些其他参数,这里不再赘述。
3、bootstrap绑定后会有回调ChannelFuture,Future是再线程编程中非常常用也非常好用同学们有兴趣可以研究一下它的具体实现
- Channel的实现NioServerSocketChannel
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringDecoder(CharsetUtil.UTF_8));
//添加处理器
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
1、ServerBootstrap.init()方法中,负责accept新链接的Channel的pipeline被添加了一个ChannelInitializer
2、在此类中可以加入很多处理,这里只加了字符编解码器和自定义的处理器,还可以增加一个防止TCP的粘包和拆包的处理,这个以后再讲。
- Handler的实现NettyServerHandler
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
public static Map<String, ChannelHandlerContext> ctxMap = new ConcurrentHashMap<String, ChannelHandlerContext>(16);
/**
* 客户端连接会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String clientIp = getClientIp(ctx);
ctxMap.put(clientIp,ctx);
log.info("有客户端进行连接:{}",clientIp);
}
/**
* 客户端发消息会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(ctx.channel().remoteAddress());
String ip = getClientIp(ctx);
System.out.printf("客户端IP:"+ip);
String body = msg.toString();
log.info("服务器收到消息: {}", body);
ctx.flush();
}
/**
* 发生异常触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
/**
* 主动发送消息
*
* @param key
* @param cmd
*/
public static void sendMessage(String key,String cmd){
ChannelHandlerContext ctx = ctxMap.get(key);
if(null!=ctx){
ctx.write(cmd);
ctx.flush();
}else{
log.error("客户端已离线:"+key);
}
}
/**
* 获取客户端ip
* @param ctx
* @return
*/
private String getClientIp(ChannelHandlerContext ctx){
InetSocketAddress ipSocket = (InetSocketAddress)ctx.channel().remoteAddress();
String clientIp = ipSocket.getAddress().getHostAddress();
return clientIp;
}
}
1、channelActive是连接成功后的回调
2、channelRead可以读取经过处理的消息了
3、exceptionCaught发生异常时处理
- 启动Netty
@Slf4j
@SpringBootApplication
public class NettyDemoApplication extends SpringBootServletInitializer implements CommandLineRunner {
@Autowired
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(NettyDemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
//开启Netty服务
ThreadUtil.newExecutor().submit(nettyServer);
log.info("======服务端服务已经启动========");
}
}
1、在主启动类中注入NettyServer因为我们的NettyServer已经注册成Component里可以归Spring管理
2、用线程池启动NettyServer
可以看到Springboot启动了在8080端口,而Netty启动在13001端口,代表我们的Netty服务器模式也启动了。
接下来可以测试一下。
我们用TCP客户端进行连接13001端口,可以看到可以连接成功,服务端会输出
2021-09-11 14:40:22.615 INFO 16256 --- [ntLoopGroup-3-1] com.pp.coder.netty.NettyServerHandler : 有客户端进行连接:127.0.0.1
/127.0.0.1:28000
服务器成功收到了消息,我们的初次使用netty就成功了,后面还有高阶的使用,比如一些常见的处理方法以及断线重连和心跳包的处理,有时间分享给大家。