什么是netty
对于netty概念的定义,我们可以去netty的官方网站查看
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
netty是一个NIO(no-blocking-io,即非阻塞io)的服务端 客户端的框架, 它可以简单快速的部署一个协议服务端和客户端的应用.它极大地简化了和流线化了类似TCP和UDP的网络编程
'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.
简单且快捷并不意味着它的性能或者可维护性很差,netty是经过精心设计的并且从FTP,SMTP,HTTP和其他的二进制以及文本协议中吸取了很多经验,最终,netty成功的实现了同时保证开发,性能稳定性和灵活性.
可以看到netty是一个NIO的网络编程框架,实际上netty并不只支持websocket,包括HTTP,UDP,等等协议也都在netty的支持范围中,并且作为http的服务器,它的性能实际上要比Tomcat,jetty等等web容器的性能要高出许多,那么我们为什么不用netty作为http的服务器呢,主要原因还是因为netty本身不基于servlet规范,而且netty的服务生态没有类似SpringMVC这种框架,所以市面上大部分还是以Tomcat等servlet容器为web框架
初始化框架
项目创建好了以后在pom.xml中添加以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
这里推荐大家选择netty4.1以后版本的,不要选择3.+或者5.+的.因为netty官方将5.+版本废弃掉了所以4.1.+版本既是最新的
创建netty服务端
@Component
public class NettyServer {
//调度线程组,负责接收请求,不实际处理业务
EventLoopGroup bossGroup = new NioEventLoopGroup();
//工作线程组,负责实际处理请求
EventLoopGroup workGroup = new NioEventLoopGroup();
private Logger log = LoggerFactory.getLogger(NettyServer.class);
//自定义服务器通道初始化器,后面创建该类
@Autowired
private ServerChannelInitializer serverChannelInitializer;
public void start() {
//定义websocket 端口
InetSocketAddress socketAddress = new InetSocketAddress(9999);
//创建主线程一个主线程
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
//指定通道类型
.channel(NioServerSocketChannel.class)
//指定初始化器
.childHandler(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) {
e.printStackTrace();
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
@PreDestroy
public void destory() throws InterruptedException {
bossGroup.shutdownGracefully().sync();
workGroup.shutdownGracefully().sync();
log.info("关闭Netty");
}
}
我们将所有的类都交给spring托管,在springboot启动类中启动我们的netty,不要采用@PostContruct的方式启动,会使spring初始化bean的线程阻塞. 启动类如下
@SpringBootApplication
public class NettyDemoApplication 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 {
nettyServer.start();
}
}
接下来我们创建netty初始化通道类 即上面的ServerChannelInitializer
@Component
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Autowired
private NettyServerHandler nettyServerHandler;
@Autowired
private CloseServerHandler closeServerHandler;
@Autowired
private HttpServerHandler httpServerHandler;
@Autowired
private MessageServerHandler messageServerHandler;
@Override
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(65536));
// ChunkedWriteHandler:向客户端发送HTML5文件
socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
//关闭后的消息处理
socketChannel.pipeline().addLast("closeHandler", closeServerHandler);
//初始化连接的处理handler
socketChannel.pipeline().addLast("httpHandler", httpServerHandler);
//消息处理handler
socketChannel.pipeline().addLast("messageHandler", messageServerHandler);
}
}
要理解上面的代码首先我们要知道websocket的原理,首先websocket本身是基于http协议,在第一次请求实际上是发起了一次http请求,然后协议升级成为了websocket,维持了socket连接,所以在实际处理socket请求前可以通过处理http请求的方式来进行身份的校验
接下来我们编写对应的handler处理类
编写HttpServerHandler
@Component
@ChannelHandler.Sharable
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
System.out.println("我开始连接啦");
String uri = req.uri();
// 构造握手响应返回,本机测试
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://" + req.headers().get(HttpHeaderNames.HOST) + uri,
null, false);
WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx
.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
}
编写MessageServerHandler
@Component
@ChannelHandler.Sharable
public class MessageServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
System.out.println("我收到消息啦,收到了->" + textWebSocketFrame.text());
//返回给服务端消息
TextWebSocketFrame tws = new TextWebSocketFrame("我收到了");
channelHandlerContext.channel().writeAndFlush(tws);
}
}
编写CloseServerHandler
@Component
@ChannelHandler.Sharable
public class CloseServerHandler extends SimpleChannelInboundHandler<CloseWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, CloseWebSocketFrame closeWebSocketFrame) throws Exception {
System.out.println("我要关闭啦");
}
}
要注意的是,将netty的handler托管给spring管理时要注意加上@ChannelHandler.Shareble注解,因为spring的bean默认是单例的,但是netty并不能接受单例的handler
启动项目后可以看到打印出的监听端口的日志
2019-11-26 17:17:29.062 INFO 125108 --- [ main] c.xiaoazhai.nettydemo.netty.NettyServer : 服务器启动开始监听端口: 9999
编写html测试页面
服务端的编写基本就到这里了,接下来我们编写一个测试页面
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.min.js"></script>
<div id="app">
<div>
<div>
<textarea v-model="content" > </textarea>
</div>
<button @click.prevent="send">发送</button>
</div>
<div >
<div class="col-sm-11">
<textarea v-model="message" > </textarea>
</div>
</div>
</div>
<script>
new Vue({
el: "#app",
mounted: function () {
this.ws = new WebSocket("ws://localhost:9999/im")
this.ws.onopen = function () {
console.log("连接成功")
}
var _this = this
this.ws.onmessage = function (ev) {
_this.message=ev.data
}
},
data: {
ws: null,
content: "",
message: "",
},
methods: {
send() {
//将信息发送到后端
this.ws.send(this.content)
}
}
})
</script>
</body>
</html>
这是利用单页面的vue写的测试样例,可以检测页面的执行