第3章_Java仿微信全栈高性能后台+移动客户端
当服务器构建完毕并且启动之后,我们通过网页URL地址就可以访问这台服务器,并且服务器会向网页输出Hello Netty这样几个字。
Netty有三种线程模型:单线程、多线程、主从线程。Netty官方推荐使用主从线程组,因为主从线程组比较高效。因为任何的服务器,不管是tomcat还是Jetty,都会有一个启动的类bootstrap。这样的一个Server类我们也会通过Netty在我们的服务器里面去进行一个设置,去打开去定义。设置完成Sevrer类之后去设置Channel。讲NIO的时候讲过,当客户端和服务端建立连接之后,那么它就会有一个双向的通道。这个通道就是channel。所以我们需要在服务器里面定义channel的类型,这样的channel的类型就是NIO的类型。channel是会有一堆的助手类和handler去对它进行处理,比方说编解码处理,或者说写数据读数据等等这样的操作。这些所有的操作都是要归类到一个助手类的一个初始化器里面。它就是一个类,在这个类里面会添加很多很多的助手类,你可以把它理解为拦截器,你可以配置多个拦截器去拦截我们的channel。当一些相应的内容在Server里面去写完设置之后,就针对我们的Server需要去启动。我们就需要去启动和监听Server。监听要设置某一个端口,启动完了之后我们也是要针对我们的服务器去做一个关闭的监听。因为你可以去关闭服务器,关闭服务器之后你需要去进行一个优雅的关闭。
项目名称Artifact Id:imooc-netty-hello
使用netty先把相应的依赖加入到工程里面来。
https://mvnrepository.com/artifact/io.netty/netty-all/5.0.0.Alpha1
<!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>5.0.0.Alpha1</version> </dependency>
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: HelloServer.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: TODO * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 * @version: V1.0 */ package com.hello.server; /** * @author ZHONGZHENHUA * */ public class HelloServer { /** * @Title:HelloServer * @Description:TODO * @param args * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 */ public static void main(String[] args) { // TODO Auto-generated method stub } }
创建一对线程组,那么它是由两个线程池构建的。一对线程组就是两个线程池。EventLoop是一个线程,Group是一个组。EventLoopGroup的解释:
Special EventExecutorGroup which allows to register Channel's that getprocessed for later selection during the event loop.
它可以允许让channel去进行注册。当有客户端连接到我们服务端之后,我们会通过这样的一个线程组去注册。注册完了之后它会获得它的一个相应的一些客户端的channels然后再直接丢给我们下面的一个线程组去处理。
服务端的启动类叫做ServerBootStrap,它是专门用于去启动的。
Bootstrap sub-class which allows easy bootstrap of ServerChannel
它可以让我们简单地去启动我们的ServerChannel。
前端有一个CSS框架,它也叫做BootStrap,它是完全不一样的。我们的线程模型是一个主从的线程模型,我们的server里面要设置两个线程组,并且它们的任务分配会由Server自动处理,我们开发者不需要额外地关注。
当客户端和Server建立链接之后,我们会有相应的通道的产生。这通道是什么类型呢?我们也是要进行相应的设置。我们使用的是Nio,所以我们会使用NioServerSocketChannel.
通道有了之后,当客户端和从线程组Server建立链接之后,我们的从线程池将一组线程组会对我们相应的通道做处理。做处理的时候针对channel其实它会有一个一个的管道,这个在下节讲初始化器的时候会去说。这个其实就是一个初始化器,这个初始化器的话针对每一个channel都会有。初始化器里面会有很多很多的助手类,很多很多的助手类是针对我们的每一个channel去做不同的处理的,相当于是一个拦截器。这里我们先暂时这样理解,下一节我们会写一个具体的图例来编写初始化器。
<!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency>
childHandler,针对从线程组去做一个相应的操作。让netty它自己的助手类,netty的类很丰富,它有很多很多的助手类,并且助手类可以让我们开发者去自定义去重写,可以去写成我们自己所需要的一个样式。
serverBootStrap.bind(8088).sync();
绑定一个端口8088,绑定是需要耗时需要等待,所以它有一个方法叫做sync()。绑定完一个端口之后设置为一个同步的启动方式。设置完之后netty它会一直在这里等待,等待8088启动完毕。
启动完毕之后需要设置关闭的监听。监听是针对我们当前某一个通道。每一个客户端都会有一个channel。监听这样的channel是否关闭的话,那么我们只需要.channel()就可以了。.channel()就是获取当前某个客户端对应的一个管道。
代码其实是OK了,但是整体的线程组还没有被关闭。当服务器启动完之后,我们要去关闭服务器,关闭完服务器之后那么针对现在的两个线程组我们要去优雅地关闭。netty也提供给我们一个如何去优雅关闭的方式。
这样的一个Server的启动类其实是写完了。下一节我们会针对childHandler设置一个子处理器(初始化器)。
/imooc-netty-hello/src/main/java/com/hello/server/HelloServer.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: HelloServer.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 实现客户端发送一个请求,服务器会返回hello netty * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 * @version: V1.0 */ package com.hello.server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; /** * @author ZHONGZHENHUA * */ public class HelloServer { /** * @Title:HelloServer * @Description:TODO * @param args * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 */ public static void main(String[] args) throws Exception{ // TODO Auto-generated method stub // 定义一对线程组 // 主线程组,用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事 EventLoopGroup bossGroup = new NioEventLoopGroup(); // 从线程组,老板线程组会把任务丢给他,让手下线程组去做任务 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // netty服务器的创建, ServerBootstrap 是一个启动类 ServerBootstrap serverBootStrap = new ServerBootstrap(); serverBootStrap.group(bossGroup, workerGroup)//设置主从线程组 .channel(NioServerSocketChannel.class)//设置nio的双向通道 .childHandler(null);// 子处理器,用于处理workerGroup // 启动server,并且设置8088为启动的端口号,同时启动方式为同步 ChannelFuture channelFuture = serverBootStrap.bind(8088).sync(); // 监听关闭的channel,设置为同步方式 channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
上一节留下一个子处理器没有讲。如何设置子处理器(channel的初始化器)。每一个client和server连接完之后都会有一个channel,每一个channel有一个管道。管道会由很多个handler共同组成。当channel注册完之后,就会有一个管道。管道需要开发者编写,其实就是一个初始化器。管道需要我们设置很多的助手类handler。助手类是针对channel去做一些相应的处理。设置的handler都会在管道里面。当客户端和服务端交互的时候,相应的助手类会针对我们的请求去做相应的处理。你可以把这块内容当做拦截器去理解。管道可以被当做一个大的拦截器,大拦截器里面会有很多的小拦截器。当请求过来的时候我们会逐个逐个地进行拦截。
HelloServerInitializer其实就是把我们的handler逐个去添加。既然是针对channel去初始化,这里我们会使用channel的初始化器。我们的通信是socket通信,使用的是socket类型的channel。Channelnitializer就是对我们的channel进行初始化。
channel里面有管道,我们需要在客户端里面去做一些相应的处理。其实是为我们客户端所对应的channel做一层层的处理,所以channel需要添加相应的handler助手类。
在pipeline里面会有很多的助手类,或者称之为拦截器。我们把它理解为拦截器的话可能会更加的便于理解。
不管是开发者自定义的handler还是netty它所提供的handler,我们都可以一个一个地添加到pipeline管道里面去。比方说我们添加的第一个handler是由netty提供的。在HTTP网络上打开我们的链接,访问我们的服务器之后会返回一个相应的字符串hello netty。既然是HTTP,我们就会使用到HTTP Server的一些相应的编解码器。
用户请求我们的服务端之后,我们要返回一个hello netty这样的一个字符串,所以我们要添加一个自定义的handler。
/imooc-netty-hello/src/main/java/com/hello/server/HelloServerInitializer.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: HelloServerInitializer.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 初始化器,channel注册后,会执行里面的相应的初始化方法 * @author: ZHONGZHENHUA * @date: 2018年11月8日 下午3:05:54 * @version: V1.0 */ package com.hello.server; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpServerCodec; /** * @author ZHONGZHENHUA * */ public class HelloServerInitializer extends ChannelInitializer<SocketChannel>{ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // TODO Auto-generated method stub // 通过SocketChannel去获得对应的管道 ChannelPipeline pipeline = socketChannel.pipeline(); // 通过管道,添加handler // HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器 // 当请求到服务端,我们需要做解码,响应到客户端做编码 pipeline.addLast("HttpServerCodec", new HttpServerCodec()); // 添加自定义的助手类,返回“hello netty~” pipeline.addLast("customHandler", null); } }
/imooc-netty-hello/src/main/java/com/hello/server/HelloServer.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: HelloServer.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 实现客户端发送一个请求,服务器会返回hello netty * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 * @version: V1.0 */ package com.hello.server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; /** * @author ZHONGZHENHUA * */ public class HelloServer { /** * @Title:HelloServer * @Description:TODO * @param args * @author: ZHONGZHENHUA * @date: 2018年11月8日 上午1:53:01 */ public static void main(String[] args) throws Exception{ // TODO Auto-generated method stub // 定义一对线程组 // 主线程组,用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事 EventLoopGroup bossGroup = new NioEventLoopGroup(); // 从线程组,老板线程组会把任务丢给他,让手下线程组去做任务 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // netty服务器的创建, ServerBootstrap 是一个启动类 ServerBootstrap serverBootStrap = new ServerBootstrap(); serverBootStrap.group(bossGroup, workerGroup)//设置主从线程组 .channel(NioServerSocketChannel.class)//设置nio的双向通道 .childHandler(new HelloServerInitializer());// 子处理器,用于处理workerGroup // 启动server,并且设置8088为启动的端口号,同时启动方式为同步 ChannelFuture channelFuture = serverBootStrap.bind(8088).sync(); // 监听关闭的channel,设置为同步方式 channelFuture.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
当一个Server启动完毕之后,它就会针对我们的childHandler,其实就是针对我们的workerGroup做一个初始化,把我们的相应的一些channel进行注册。管道里面放一些编解码器、自定义的处理器等等,这些东西全部都归在一起,形成了我们的一个服务端。这一节现这样,下一节我们把自定义的handler进行编写。
编写属于开发者自己的一个自定义的一个助手类,并且在这里返回一个hello netty这样的一个字符串。针对客户端向服务端发送起请求之后,NIO的原理是,请求过来数据过来,它是把首先数据放在缓冲区,然后服务端再从这个缓冲区里面去读,客户端向服务端写数据是请求的话,其实就是一个入站或者说是一个入境。如果有朋友做过云服务器的配置的话,那么其实针对网关安全组或者是防火墙的话,那么就会有一个入站的概念。那么在这个地方其实也是一个类似的概念,它是入站。我们现在是一个HTTP的请求,过来的话我们会写上HttpObject这样的类型。CustomHandler的channelRead0方法是从缓冲区里面读数据。既然是在handler里面,其实它是一个管道。ChannelHandlerContext是上下文对象,上下文对象可以获取channel。接下来我们要把相应的数据刷到客户端去,在这里我们先打印一下客户端的地址。接下来我们要发送内容消息,发消息我们并不是直接去发,我们要通过缓冲区。我们需要把数据拷贝到缓冲区ByteBuf。Unpooled可以深拷贝ByteBuf。
charset是一个字符值,字符值我们一般都会使用UTF-8。可以使用netty提供的CharsetUtil的UTF-8。copiedBuffer是创建一个新的buf(缓冲区),在NIO的模型里面提到过不管是读数据还是写数据我们都是通过一个缓冲区来进行一个数据的交换/交互。
要把内容content刷到客户端,刷到客户端其实就是一个HTTP的response,它是一个响应。我们可以使用一个新的接口FullHttpResponse,DefaultFullHttpResponse是默认的专门用于处理HTTP的响应。version是HTTP的版本号,HttpResponseStatus.OK其实就是200,
validateHeaders是内容,响应首先是版本号和状态码,然后就是内容,validateHeaders就是content。response的一个基本设置有了。针对数据的类型、长度也是要设置。它是一个HTTP Header。这种编程方式其实就是类似于函数式的编程。第一个需要设置数据的类型。数据类型返回出去是一个文本/字符串,图片和JSON对象也行。
数据类型设置好了就进行一个长度的设置。content.readableBytes(),ByteBuf可读的长度。它是一个可读的长度,它会把整个长度给取出来返回。
我们拿到这样的长度再返回到客户端,先响应出去就可以了。当现在准备就绪之后,我们需要把相应的内容/消息给刷出去,就是我们的response。ctx.write是把response写到缓冲区,但是并不会把消息刷到客户端。ctx.writeAndFlush不仅仅是进行一个写,它还会进行一个刷。它会先把数据写到缓冲区,然后再刷到客户端。
这个就是一个自定义的处理类。
/imooc-netty-hello/src/main/java/com/hello/server/CustomHandler.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: CustomHandler.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 创建自定义助手类 * @author: ZHONGZHENHUA * @date: 2018年11月8日 下午9:55:36 * @version: V1.0 */ package com.hello.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.CharsetUtil; /** * @author ZHONGZHENHUA * */ //SimpleChannelInboundHandler: 对于请求来讲, 其实相当于[入站,入境] public class CustomHandler extends SimpleChannelInboundHandler<HttpObject>{ @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { // TODO Auto-generated method stub // 获取channel Channel channel = ctx.channel(); // 显示客户端的远程地址 System.out.println(channel.remoteAddress()); // 定义发送的数据消息 ByteBuf content = Unpooled.copiedBuffer("Hello netty~", CharsetUtil.UTF_8); // 构建一个http response FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); // 为响应增加数据类型和长度 response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain"); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); // 把响应刷到客户端 ctx.writeAndFlush(response); } }
编写完毕之后我们需要把Handler放到初始化器里面去,让channel一开始注册的时候就要添加到我们的pipeline里面去,相当于重新在这里面注册了一个拦截器。
/imooc-netty-hello/src/main/java/com/hello/server/HelloServerInitializer.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: HelloServerInitializer.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 初始化器,channel注册后,会执行里面的相应的初始化方法 * @author: ZHONGZHENHUA * @date: 2018年11月8日 下午3:05:54 * @version: V1.0 */ package com.hello.server; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpServerCodec; /** * @author ZHONGZHENHUA * */ public class HelloServerInitializer extends ChannelInitializer<SocketChannel>{ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { // TODO Auto-generated method stub // 通过SocketChannel去获得对应的管道 ChannelPipeline pipeline = socketChannel.pipeline(); // 通过管道,添加handler // HttpServerCodec是由netty自己提供的助手类,可以理解为拦截器 // 当请求到服务端,我们需要做解码,响应到客户端做编码 pipeline.addLast("HttpServerCodec", new HttpServerCodec()); // 添加自定义的助手类,返回“hello netty~” //pipeline.addLast("customHandler", null); pipeline.addLast("customHandler", new CustomHandler()); } }
现在服务端和初始化器以及自定义的一个助手类全部都创建完毕。
我们监听的端口是8088。
// 显示客户端的远程地址
System.out.println(channel.remoteAddress());
在自定义的CustomHandler这一块打印客户端的远程地址的时候打印了很多,因为我们的msg接收的时候没有对它做一个类型的判断。
判断msg是不是一个HTTP Request的请求类型,在打印客户端的远程地址之前先判断msg的类型
/imooc-netty-hello/src/main/java/com/hello/server/CustomHandler.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: CustomHandler.java * @Prject: imooc-netty-hello * @Package: com.hello.server * @Description: 创建自定义助手类 * @author: ZHONGZHENHUA * @date: 2018年11月8日 下午9:55:36 * @version: V1.0 */ package com.hello.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.CharsetUtil; /** * @author ZHONGZHENHUA * */ //SimpleChannelInboundHandler: 对于请求来讲, 其实相当于[入站,入境] public class CustomHandler extends SimpleChannelInboundHandler<HttpObject>{ @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { // TODO Auto-generated method stub // 获取channel Channel channel = ctx.channel(); if(msg instanceof HttpRequest) { // 显示客户端的远程地址 System.out.println(channel.remoteAddress()); // 定义发送的数据消息 ByteBuf content = Unpooled.copiedBuffer("Hello netty~", CharsetUtil.UTF_8); // 构建一个http response FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); // 为响应增加数据类型和长度 response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain"); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); // 把响应刷到客户端 ctx.writeAndFlush(response); } } }
刷新一下之后,刷新页面的时候谷歌浏览器会针对服务端发送两次请求, 第一次请求localhost是我们所需要的,
favicon.ico,你请求每一个网站的时候,它其实默认会有这样的一个图标的请求,这个和我们其实没有任何的关系我们不需要去管。我们在请求后端的时候其实我们并没有加相应的路由,就相当于是一个Spring Boot或者说Spring MVC里面的一个RequestMapping。我们没有在后边去加相应的请求的路径,所以它统一了,只要你去访问我们的8088这样的一个端口,那么它就会直接到我们的Handler里面去。
还是会返回Hello netty~,因为它没有去做相应的捕获。如果要屏蔽favicon.ico可以去设置可以去获取请求的路径,然后再去截取再去判断,如果是这样的一个ico那么就直接return不要去做额外的处理。
content-type是后端设置的文本类型text/plain,content-length是content.readableBytes(),也就是这个字符串Hello netty~的长度。
// 构建一个http response
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
HTTP 1_1默认会开启长链接keep-alive,那么这样针对于我们的客户端和服务端请求传输的效率、速度会比HTTP1.0要快很多。
response是后端返回到前端的代码。
讲一个扩展知识,不通过浏览器也可以去访问服务器。需要一个linux服务器,linux服务器和这台主机是需要相互ping通的。linux和本地是可以相互ping通。ping通之后我们使用curl可以和当前这个Hello netty~进行交互。
看来不是处于同一个局域网是很难访问windows主机的。