當服務器構建完畢而且啓動以後,咱們經過網頁URL地址就能夠訪問這臺服務器,而且服務器會向網頁輸出Hello Netty這樣幾個字。前端
Netty有三種線程模型:單線程、多線程、主從線程。Netty官方推薦使用主從線程組,由於主從線程組比較高效。由於任何的服務器,不論是tomcat仍是Jetty,都會有一個啓動的類bootstrap。這樣的一個Server類咱們也會經過Netty在咱們的服務器裏面去進行一個設置,去打開去定義。設置完成Sevrer類以後去設置Channel。講NIO的時候講過,當客戶端和服務端創建鏈接以後,那麼它就會有一個雙向的通道。這個通道就是channel。因此咱們須要在服務器裏面定義channel的類型,這樣的channel的類型就是NIO的類型。channel是會有一堆的助手類和handler去對它進行處理,比方說編解碼處理,或者說寫數據讀數據等等這樣的操做。這些全部的操做都是要歸類到一個助手類的一個初始化器裏面。它就是一個類,在這個類裏面會添加不少不少的助手類,你能夠把它理解爲攔截器,你能夠配置多個攔截器去攔截咱們的channel。當一些相應的內容在Server裏面去寫完設置以後,就針對咱們的Server須要去啓動。咱們就須要去啓動和監聽Server。監聽要設置某一個端口,啓動完了以後咱們也是要針對咱們的服務器去作一個關閉的監聽。由於你能夠去關閉服務器,關閉服務器以後你須要去進行一個優雅的關閉。java
項目名稱Artifact Id:imooc-netty-hellolinux
使用netty先把相應的依賴加入到工程裏面來。編程
https://mvnrepository.com/artifact/io.netty/netty-all/5.0.0.Alpha1bootstrap
<!-- 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的解釋:windows
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。tomcat
前端有一個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主機的。