WebSocket长连接
简介
WebSocket 是 HTML5 中的协议,是构建在 HTTP 协议之上的一个网络通信协议,其以长连接的方式实现了客户端与服务端的全双工通信。HTTP/1.1 版本协议中具有 keep-alive 属性,实现的是半双工通信。
握手原理

- Clinet端生产一个随机的Key,保存到浏览器和请求头中。
- Clinet发送请求给Server端。
- Server从请求头属性中判断出这事一个ws(WebSocket)请求,读到请求头中的key属性。
- Server对key加密,放到响应头accept属性中。
- Server将响应回应给Clinet端。
- Clinet端从头中判断是一个ws(WebSocket)响应,读取头中的accpet属性。
- Clinet端进行解密后,将结果保存在浏览中的key进行比较,如果匹配,则连接。否则不连接。
场景需求分析
在页面上有两个左右并排的文本域,它们的中间有一个“发送”按钮。在左侧文本域中输入文本内容后,单击发送按钮,会显示到右侧文本域中。
客户端页面定义
在 src/main 下定义一个目录 webapp。在其中定义 html 页面。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>index</title></head><script type="text/javascript">// 当前页面一打开就会执行的代码var socket;if(window.WebSocket) {// 创建一个WebSocket连接socket = new WebSocket("ws://localhost:8888/gc");// 当与服务端的ws连接创建成功后会触发onopen的执行socket.onopen = function (ev) {// 在右侧文本域中显示连接建立提示var ta = document.getElementById("responseText");ta.value = "连接已建立";}// 当接收到服务端发送的消息时会触发onmessage的执行socket.onmessage = function (ev) {// 将服务端发送来的消息在右侧文本域中显示,在原有内容基础上进行拼接var ta = document.getElementById("responseText");ta.value = ta.value + "\n" + ev.data;}// 当与服务端的ws连接断开时会触发onclose的执行socket.onclose = function (ev) {// 将连接关闭消息在右侧文本域中显示,在原有内容基础上进行拼接var ta = document.getElementById("responseText");ta.value = ta.value + "\n连接已关闭";}} else {alert("浏览器不支持WebSocket");}// 定义发送按钮的发送方法function send(msg) {// 若当前浏览器不支持WebSocket,则直接结束if(!window.WebSocket) return;// 若ws连接已打开,则向服务器发送消息if(socket.readyState == WebSocket.OPEN) {// 通过ws连接向服务器发送消息socket.send(msg);}}</script><body><form><textarea id="message" style="width: 150px; height: 150px"></textarea><input type="button" value="发送" onclick="send(this.form.message.value)"><textarea id="responseText" style="width: 150px; height: 150px"></textarea></form></body></html>
服务端定义
package com.gc.socket.server;import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.DelimiterBasedFrameDecoder;import io.netty.handler.codec.FixedLengthFrameDecoder;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.http.HttpObjectAggregator;import io.netty.handler.codec.http.HttpServerCodec;import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import io.netty.handler.logging.LogLevel;import io.netty.handler.logging.LoggingHandler;import io.netty.handler.stream.ChunkedWriteHandler;import io.netty.util.CharsetUtil;/*** @description:* @author: GC* @create: 2020-11-10 16:11**/public class Server {public static void main(String[] args) {//定义用于处理客户端连接的EventLoopGroupEventLoopGroup parentEventLoop = new NioEventLoopGroup();//定义用于处理客户端请求的EventLoopGroupEventLoopGroup childEventLoop = new NioEventLoopGroup();try{//服务端专用Bootstrap.主要用于将属绑定起来。建立关系ServerBootstrap bootstrap = new ServerBootstrap();//绑定处理客户端的EventLoopGroup对象bootstrap.group(parentEventLoop, childEventLoop)//使用NIO的方式.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.ERROR))//绑定消息收发时的编码/解码器。 和自定义的Handler绑定.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();//HttpRequestDecoder和HttpResponseEncoder的组合使pipeline.addLast(new HttpServerCodec());//可以毫无困难地发送大型数据流。pipeline.addLast(new ChunkedWriteHandler());//聚合处理器,聚合请求或者响应pipeline.addLast(new HttpObjectAggregator(111));//处理程序为你运行一个websocket服务器做了所有繁重的工作。// 它负责websocket握手以及控制框架的处理(Close,Ping,Pong)。// 文本和二进制数据帧被传递到管道中的下一个处理程序(由您执行)进行处理。pipeline.addLast(new WebSocketServerProtocolHandler("/gc"));//自定义业务处理器pipeline.addLast(new ServerHandler());}});//声明服务端绑定的端口ChannelFuture future = bootstrap.bind(8888).sync();//当Channel调用了close()方法,并且成功关闭之后,才会调用此代码future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {//优雅的关闭方式parentEventLoop.shutdownGracefully();childEventLoop.shutdownGracefully();}}}
业务处理器定义
package com.gc.socket.server;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import java.util.Date;import java.util.Random;import java.util.UUID;import java.util.concurrent.TimeUnit;public class ServerHandler extends ChannelInboundHandlerAdapter {public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {String text = ((TextWebSocketFrame)msg).text();ctx.channel().writeAndFlush(new TextWebSocketFrame("客户端:"+text));System.out.println("本地地址为:"+ctx.channel().localAddress());System.out.println("远程地址为:"+ctx.channel().remoteAddress()+" ---> 收到消息:"+msg);}//当发生Throwable异常时,会调用该方法public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.fireExceptionCaught(cause);ctx.close();}}
Socket实现群聊
简介
本例要实现一个网络群聊工具。参与聊天的客户端消息是通过服务端进行广播的。
服务端定义
package com.gc.socket.server;import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.DelimiterBasedFrameDecoder;import io.netty.handler.codec.FixedLengthFrameDecoder;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.http.HttpObjectAggregator;import io.netty.handler.codec.http.HttpServerCodec;import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import io.netty.handler.logging.LogLevel;import io.netty.handler.logging.LoggingHandler;import io.netty.handler.stream.ChunkedWriteHandler;import io.netty.util.CharsetUtil;/*** @description:* @author: GC* @create: 2020-11-10 16:11**/public class Server {public static void main(String[] args) {//定义用于处理客户端连接的EventLoopGroupEventLoopGroup parentEventLoop = new NioEventLoopGroup();//定义用于处理客户端请求的EventLoopGroupEventLoopGroup childEventLoop = new NioEventLoopGroup();try{//服务端专用Bootstrap.主要用于将属绑定起来。建立关系ServerBootstrap bootstrap = new ServerBootstrap();//绑定处理客户端的EventLoopGroup对象bootstrap.group(parentEventLoop, childEventLoop)//使用NIO的方式.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.ERROR))//绑定消息收发时的编码/解码器。 和自定义的Handler绑定.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();//因为要传输Clinet端的消息,所以肯定是先收到消息,所以解码器放前面pipeline.addLast(new LineBasedFrameDecoder(2024));pipeline.addLast(new StringDecoder());pipeline.addLast(new StringEncoder());//自定义业务处理器pipeline.addLast(new ServerHandler());}});//声明服务端绑定的端口ChannelFuture future = bootstrap.bind(8888).sync();System.out.println("Server 启动成功");//当Channel调用了close()方法,并且成功关闭之后,才会调用此代码future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {//优雅的关闭方式parentEventLoop.shutdownGracefully();childEventLoop.shutdownGracefully();}}}
服务端业务处理器定义
package com.gc.socket.server;import io.netty.channel.Channel;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import io.netty.util.concurrent.EventExecutor;import io.netty.util.concurrent.GlobalEventExecutor;import java.util.Date;import java.util.Random;import java.util.UUID;import java.util.concurrent.TimeUnit;public class ServerHandler extends ChannelInboundHandlerAdapter {//消息组,聊天的人都在这个组里面。类似社交软件的群聊private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);/*** 读取到发送的消息* @param ctx* @param msg* @throws Exception*/public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {Channel channel = ctx.channel();channelGroup.forEach(ch -> {if(ch != channel)//不是自己,显示别人的地址ch.writeAndFlush(channel.remoteAddress()+":"+msg+"\n");else//是自己,不需要显示ch.writeAndFlush(msg+"\n");});}/*** 用户上线,会触发此方法* @param ctx* @throws Exception*/public void channelActive(ChannelHandlerContext ctx) throws Exception {Channel channel = ctx.channel();channelGroup.writeAndFlush(channel.remoteAddress()+"上线"+"\n");channelGroup.add(channel);}/*** 用户下线,会触发此犯法* @param ctx* @throws Exception*/public void channelInactive(ChannelHandlerContext ctx) throws Exception {Channel channel = ctx.channel();channelGroup.writeAndFlush(channel.remoteAddress()+"下线。当前在线人数:"+channelGroup.size()+"\n");//当触发此方法时,会自动从channelGroup中删除。以下代码是当channel在线时,将它踢出channelGroup//channelGroup.remove(channel);}//当发生Throwable异常时,会调用该方法public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.fireExceptionCaught(cause);ctx.close();}}
客户端定义
package com.gc.socket.clinet;import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.codec.FixedLengthFrameDecoder;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import io.netty.util.CharsetUtil;import java.io.*;import java.net.SocketAddress;public class Clinet {public static void main(String[] args) throws Exception {//客户端处理服务端的EventLoopGroupEventLoopGroup clint = new NioEventLoopGroup();//Bootstrap专门用于客户端的属性设置以及关系绑定Bootstrap bootstrap = new Bootstrap();bootstrap.group(clint)//使用NIO客户端的Channel.channel(NioSocketChannel.class)//绑定编码器/解码器。以及自定义的Handler.handler(new ChannelInitializer<SocketChannel>(){@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();//当上线后,Client端会收到Server端的提示信息,所以这里解码器放前面pipeline.addLast(new LineBasedFrameDecoder(2048));pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));pipeline.addLast(new ClinetHandler());}});//指定连接服务端的端口ChannelFuture future = bootstrap.connect("127.0.0.1",8888).sync();System.out.println("Clinet 启动成功");Channel channel = future.channel();InputStream in;InputStreamReader is = new InputStreamReader(System.in , "UTF-8");BufferedReader br = new BufferedReader(is);while (true) {channel.writeAndFlush(br.readLine()+"\n");}//这里不关闭资源,因为客户端要持续输入。}}
客户端业务处理器定义
package com.gc.socket.clinet;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.channel.SimpleChannelInboundHandler;import java.util.UUID;import java.util.concurrent.TimeUnit;/*** ChannelInboundHandlerAdapter 类中的 channelRead方法不会自动释放资源。所以会不断的发送消息* SimpleChannelInboundHandler 类中的 channelRead0()方法不会自动释放资源。所以只要Clinet端触发了消息发送, 双方两段就会一直死循环发送消息。**/public class ClinetHandler extends SimpleChannelInboundHandler<String> {/*** 读取服务端发送的消息* @param ctx* @param msg* @throws Exception*/public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {System.err.println("Clinet:"+msg);}//当发生异常,捕获并关闭@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}}
读写空闲监测
服务端修改

服务端业务处理器修改

心跳机制
简介
所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还“活着”, 以确保 TCP 连接的有效性。
客户端定义
package com.gc.socket.clinet;import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.codec.FixedLengthFrameDecoder;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import io.netty.util.CharsetUtil;import java.io.*;import java.net.SocketAddress;public class Clinet {public static void main(String[] args) throws Exception {//客户端处理服务端的EventLoopGroupEventLoopGroup clint = new NioEventLoopGroup();//Bootstrap专门用于客户端的属性设置以及关系绑定Bootstrap bootstrap = new Bootstrap();bootstrap.group(clint)//使用NIO客户端的Channel.channel(NioSocketChannel.class)//绑定编码器/解码器。以及自定义的Handler.handler(new ChannelInitializer<SocketChannel>(){@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();//当上线后,Client端会收到Server端的提示信息,所以这里解码器放前面pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));pipeline.addLast(new ClinetHandler(bootstrap));}});//指定连接服务端的端口ChannelFuture future = bootstrap.connect("127.0.0.1",8888).sync();System.out.println("Clinet 启动成功");}}
客户端业务处理器
package com.gc.socket.clinet;import io.netty.bootstrap.Bootstrap;import io.netty.channel.Channel;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.util.concurrent.GenericFutureListener;import io.netty.util.concurrent.ScheduledFuture;import java.util.Random;import java.util.concurrent.TimeUnit;/*** ChannelInboundHandlerAdapter 类中的 channelRead方法不会自动释放资源。所以会不断的发送消息* SimpleChannelInboundHandler 类中的 channelRead0()方法不会自动释放资源。所以只要Clinet端触发了消息发送, 双方两段就会一直死循环发送消息。**/public class ClinetHandler extends ChannelInboundHandlerAdapter {//用于添加任务定时触发private ScheduledFuture<?> scheduled;//用于监听任务过期private GenericFutureListener listener;//用于客户端断链重连private Bootstrap bootstrap;public ClinetHandler(Bootstrap bootstrap){this.bootstrap = bootstrap;}/*** 用户上线后,触发此方法* @param ctx* @throws Exception*/public void channelActive(ChannelHandlerContext ctx) throws Exception {setRandom(ctx.channel());}/*** 当用户下线后,触发此方法* @param ctx* @throws Exception*/public void channelInactive(ChannelHandlerContext ctx) throws Exception {//下线后,删除监听任务。 否则数据量大了,可能会导致OOMscheduled.removeListener(listener);System.out.println("已断链, 准备重连。");bootstrap.connect("localhost", 8888).sync();}/*** 心跳监测时间* @throws Exception*/public void setRandom(Channel ctx) throws Exception {//随机下次像Server端发送心跳的时间int random = new Random().nextInt(5)+1;System.out.println(random+"S后发送心跳");scheduled = ctx.eventLoop().schedule(()->{//当前channel处于活动状态, 说明没死,发送心跳监测if(ctx.isActive()){System.out.println("向服务端发送心跳");ctx.writeAndFlush("ping");}else{//已经死了System.out.println("心跳超时,连接断开");}},random, TimeUnit.SECONDS);//给当前方法定义监听器listener = future1 -> {setRandom(ctx);};//添加一个定时监听scheduled.addListener(listener);}//当发生异常,捕获并关闭@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}}
服务端定义
package com.gc.socket.server;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import io.netty.handler.logging.LogLevel;import io.netty.handler.logging.LoggingHandler;import io.netty.handler.timeout.IdleStateHandler;/*** @description:* @author: GC* @create: 2020-11-10 16:11**/public class Server {public static void main(String[] args) {//定义用于处理客户端连接的EventLoopGroupEventLoopGroup parentEventLoop = new NioEventLoopGroup();//定义用于处理客户端请求的EventLoopGroupEventLoopGroup childEventLoop = new NioEventLoopGroup();try{//服务端专用Bootstrap.主要用于将属绑定起来。建立关系ServerBootstrap bootstrap = new ServerBootstrap();//绑定处理客户端的EventLoopGroup对象bootstrap.group(parentEventLoop, childEventLoop)//使用NIO的方式.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.ERROR))//绑定消息收发时的编码/解码器。 和自定义的Handler绑定.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {ChannelPipeline pipeline = socketChannel.pipeline();pipeline.addLast(new StringDecoder());pipeline.addLast(new StringEncoder());/*** @prarm1:在指定的时间内,服务端没发生读操作(时间单位:S)* @prarm2:在指定的时间内,服务端没发生写操作(时间单位:S)* @prarm3:在指定的时间内,服务端即要发生读操作,也要发生写操作。如果只出现了两个操作中的一个,时间到达时,还是会被连接中断(时间单位:S)*/pipeline.addLast(new IdleStateHandler(3,0,0));//自定义业务处理器pipeline.addLast(new ServerHandler());}});//声明服务端绑定的端口ChannelFuture future = bootstrap.bind(8888).sync();System.out.println("Server 启动成功");//当Channel调用了close()方法,并且成功关闭之后,才会调用此代码future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {//优雅的关闭方式parentEventLoop.shutdownGracefully();childEventLoop.shutdownGracefully();}}}
服务端业务处理器定义
package com.gc.socket.server;import io.netty.channel.Channel;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.handler.timeout.IdleState;import io.netty.handler.timeout.IdleStateEvent;public class ServerHandler extends ChannelInboundHandlerAdapter {/*** 读取客户端发送的消息* @param ctx* @param msg* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("接收到Client发送的消息:" + msg);}/*** 异常捕捉* @param ctx* @param cause* @throws Exception*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();}/*** 用于捕获当前Server中的各种事件* @param ctx* @param evt* @throws Exception*/@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent) {IdleState state = ((IdleStateEvent) evt).state();if (state == IdleState.READER_IDLE) {System.out.println("将要断开连接");ctx.close();} else {super.userEventTriggered(ctx, evt);}}}}
手写Tomcat
简介
确切地说,这里要手写的是一个 Web 容器,一个类似于 Tomcat 的容器,用于处理 HTTP请求。该Web 容器没有实现 JavaEE 的 Servlet 规范,不是一个 Servlet 容器。但其是模拟着Tomcat 来写的,这里定义了自己的请求、响应及 Servlet,分别命名为了 NettyRequest,NettyResponse 与 Servnet。我们这里要定义一个 Tomcat,这个 Web 容器提供给用户后,用户只需要按照使用步骤就可以将其自定义的 Servnet 发布到该 Tomcat 中。我们现在给出用户对于该 Tomcat 的使用步骤:
- 用户只需将自定义的 Servnet 放入到指定的包中。例如,com.abc.webapp 包中。
- 用户在访问时,需要将自定义的 Servnet 的简单类名全小写后的字符串作为该 Servnet的 Name 进行访问。
- 若没有指定的 Servnet,则访问默认的 Servnet。
思路定义
Servnet规范包
NettyRequest
包含了传统HttpServletRequest中的方法。如:获取请求路径,获取URL中的参数,获取方法名称,获取参数等等。NettyResponse
包含了传统HttpServletResponse中的方法。如:读/写操作。Servnet
和Servlet一样,有doPost(),doGet()
Tomcat内核包
main()
用于加载指定目录下的接口。DefaultNettyRequest,DefaultNettyResponse
我们自定义NettyResponse,NettyResponse的具体实现。DefaultServnet
我们自定义的Servnet 具体实现。TomcatHandler
请求解析业务处理器,主要用于解析接收请求。TomcatServer
当启动容器时,初始化资源到本地缓存。
webapp
要访问的接口,都在该目录下。
项目结构

编码实现
定义NettyRequest规范
package com.gc.servnet;import com.sun.deploy.net.HttpRequest;import java.util.List;import java.util.Map;/*** @description:* @author: GC* @create: 2020-11-13 15:56**/public interface NettyRequest {//获取URL中的参数String getUri();//获取方法名称String getMethod();//获取请求路径String getPath();//获取本次请求中所有的参数Map<String, List<String>> getParameters();//获取指定名称的参数值List<String> getParameters(String name);//指定参数名称获取参数值(第一个值)String getParameter(String name);}
定义NettyResponse规范
package com.gc.servnet;/*** @description:* @author: GC* @create: 2020-11-13 15:56**/public interface NettyResponse {void write(String content) throws Exception;}
定义Servnet规范
package com.gc.servnet;public abstract class Servnet {//定义Servlet中的doGetpublic abstract void doGet(NettyRequest request, NettyResponse response) throws Exception;//定义Servlet中的doPostpublic abstract void doPost(NettyRequest request, NettyResponse response) throws Exception;}
DefaultNettyRequest具体实现
package com.gc.tomcat;import com.gc.servnet.NettyRequest;import io.netty.handler.codec.http.HttpRequest;import io.netty.handler.codec.http.QueryStringDecoder;import java.util.List;import java.util.Map;/*** @description:* @author: GC* @create: 2020-11-13 16:25**/public class DefaultNettyRequest implements NettyRequest {HttpRequest request;public DefaultNettyRequest(HttpRequest request){this.request = request;}@Overridepublic String getUri() {return request.uri();}@Overridepublic String getMethod() {return request.method().name();}@Overridepublic String getPath() {QueryStringDecoder decoder = new QueryStringDecoder(request.uri());return decoder.path();}@Overridepublic Map<String, List<String>> getParameters() {QueryStringDecoder decoder = new QueryStringDecoder(getUri());return decoder.parameters();}private int count = 0;@Overridepublic List<String> getParameters(String name) {Map<String, List<String>> map = getParameters();return map.get(name);}@Overridepublic String getParameter(String name) {List<String> list = getParameters(name);if(null == list){System.out.println("要获取的参数不存在");return null;}return list.get(0);}}
DefaultNettyResponse具体实现
package com.gc.tomcat;import com.gc.servnet.NettyResponse;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.http.*;import io.netty.util.internal.StringUtil;/*** @description:* @author: GC* @create: 2020-11-13 16:34**/public class DefaultNettyResponse implements NettyResponse {private HttpRequest request;private ChannelHandlerContext context;public DefaultNettyResponse(HttpRequest request, ChannelHandlerContext context) {this.request = request;this.context = context;}@Overridepublic void write(String content) throws Exception {// 处理content为空的情况if (StringUtil.isNullOrEmpty(content)) {return;}// 创建响应对象FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK,// 根据响应体内容大小为response对象分配存储空间Unpooled.wrappedBuffer(content.getBytes("UTF-8")));// 获取响应头HttpHeaders headers = response.headers();// 设置响应体类型headers.set(HttpHeaderNames.CONTENT_TYPE, "text/json");// 设置响应体长度headers.set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());// 设置缓存过期时间headers.set(HttpHeaderNames.EXPIRES, 0);// 若HTTP请求是长连接,则响应也使用长连接if (HttpUtil.isKeepAlive(request)) {headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);}context.writeAndFlush(response);}}
DefaultServnet具体实现
package com.gc.tomcat;import com.gc.servnet.NettyRequest;import com.gc.servnet.NettyResponse;import com.gc.servnet.Servnet;/*** @description:* @author: GC* @create: 2020-11-13 16:40**/public class DefaultServnet extends Servnet {//定义Servlet中的doGet@Overridepublic void doGet(NettyRequest request, NettyResponse response) throws Exception {String sernetName = request.getUri().split("/")[1];response.write("404 - no this servnet : " + sernetName);}//定义Servlet中的doPost@Overridepublic void doPost(NettyRequest request, NettyResponse response) throws Exception {doGet(request, response);}}
编写自定义TomcatHandler处理器
package com.gc.tomcat;import com.gc.servnet.NettyRequest;import com.gc.servnet.NettyResponse;import com.gc.servnet.Servnet;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.handler.codec.http.HttpRequest;import java.util.Map;/*** @description:* @author: GC* @create: 2020-11-13 16:47**/public class TomcatHandler extends ChannelInboundHandlerAdapter {//启动时候加载到这个Mapprivate Map<String, Servnet> nameToServnetMap;//如果代码被放射的方式或者其它的方式修改过,则重写加载private Map<String, String> nameToClassNameMap;//定义一个构造器,当Netty启动时候加载到Netty的Handler中被Netty管理public TomcatHandler(Map<String, Servnet> nameToServnetMap, Map<String, String> nameToClassNameMap){this.nameToServnetMap = nameToServnetMap;this.nameToClassNameMap = nameToClassNameMap;}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//如果是Http请求,则进入解析if(msg instanceof HttpRequest){HttpRequest request = (HttpRequest) msg;//拿到Servnet名称String serverName = request.uri().split("/")[1];serverName = serverName.substring(0,serverName.indexOf("?"));//获得客户端要访问的ServnetServnet servnet = new DefaultServnet();//一级缓存是否有这个keyif(nameToServnetMap.containsKey(serverName)){//直接拿出去servnet = nameToServnetMap.get(serverName);}else if(nameToClassNameMap.containsKey(serverName)){//一级缓存没有,二级缓存是否有这个key//双重检测,防止并发请求时,重复创建if(!nameToServnetMap.containsKey(serverName)) {synchronized (this) {if(!nameToServnetMap.containsKey(serverName)) {//有,获取到类的全路径限定路径String classPath = nameToClassNameMap.get(serverName);servnet = (Servnet) Class.forName(classPath).newInstance();nameToServnetMap.put(serverName, servnet);}}}}//开始根据路径处理请求//用于获取当前请求中的参数,路径等信息NettyRequest defultRequest = new DefaultNettyRequest(request);//用于获取当前请求中的上下文数据NettyResponse defultResponse = new DefaultNettyResponse(request, ctx);// 根据不同的请求类型,调用servnet实例的不同方法String methodName = request.method().name();if (methodName.equalsIgnoreCase("GET")) {servnet.doGet(defultRequest, defultResponse);} else if(methodName.equalsIgnoreCase("POST")) {servnet.doPost(defultRequest, defultResponse);}ctx.close();}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}}
编写容器启动时初始化TomcatServer
TomcatServerpackage com.gc.tomcat;import com.gc.servnet.Servnet;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.http.HttpServerCodec;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import java.io.File;import java.io.UnsupportedEncodingException;import java.net.URL;import java.util.HashMap;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/*** @description:* @author: GC* @create: 2020-11-13 17:24**/public class TomcatServer {// key为servnet的简单类名,value为对应servnet实例private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();// key为servnet的简单类名,value为对应servnet类的全限定性类名private Map<String, String> nameToClassNameMap = new HashMap<>();//加载该路径下的类为Servnetprivate String basePackage;public TomcatServer(String basePackage) {this.basePackage = basePackage;}// 启动tomcatpublic void start() throws Exception {// 加载指定包中的所有Servnet的类名cacheClassName(basePackage);//资源准备完毕,启动NettyrunServer();}private void cacheClassName(String basePackage) throws UnsupportedEncodingException {//获取指定路径下的类URL resource = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));if(null == resource){System.out.println("没有找到访问资源");return;}//将取到的资源转成一个FileFile dir = new File(resource.getPath());//找出该路径下所有的类for (File file : dir.listFiles()) {if (file.isDirectory()) {// 若当前遍历的file为目录,则递归调用当前方法cacheClassName(basePackage + "." + file.getName());} else if (file.getName().endsWith(".class")) {//读到了将文件后缀去掉String simpleClassName = file.getName().replace(".class", "").trim();nameToClassNameMap.put(simpleClassName.toLowerCase(), basePackage+"."+simpleClassName);}}System.out.println(nameToClassNameMap);}private void runServer() {EventLoopGroup parentLoopGroup = new NioEventLoopGroup();EventLoopGroup childLoopGroup = new NioEventLoopGroup();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(parentLoopGroup, childLoopGroup)//当出现并发时,设置这个队列的上限长度.option(ChannelOption.SO_BACKLOG, 1024)// 指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性.childOption(ChannelOption.SO_KEEPALIVE, true).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer() {@Overrideprotected void initChannel(Channel channel) throws Exception {ChannelPipeline channelPipeline = channel.pipeline();channelPipeline.addLast(new HttpServerCodec());channelPipeline.addLast(new TomcatHandler(nameToServnetMap, nameToClassNameMap));}});ChannelFuture future = serverBootstrap.bind(8888).sync();System.out.println("启动成功");future.channel().closeFuture().sync();}catch (Exception e){e.printStackTrace();}finally {parentLoopGroup.shutdownGracefully();childLoopGroup.shutdownGracefully();}}}
编写需要访问接口(Servnet)
package com.gc.webapp;import com.gc.servnet.NettyRequest;import com.gc.servnet.NettyResponse;import com.gc.servnet.Servnet;/*** @description:* @author: GC* @create: 2020-11-13 17:44**/public class GC1 extends Servnet {@Overridepublic void doGet(NettyRequest request, NettyResponse response) throws Exception {String uri = request.getUri();String path = request.getPath();String method = request.getMethod();String name = request.getParameter("name");String content = "uri = " + uri + "\n" +"path = " + path + "\n" +"method = " + method + "\n" +"param = " + name;response.write(content);}@Overridepublic void doPost(NettyRequest request, NettyResponse response) throws Exception {doGet(request, response);}}
启动类
package com.gc.tomcat;/*** @description:* @author: GC* @create: 2020-11-13 16:24**/public class RunTomcat {public static void main(String[] args) throws Exception {TomcatServer tomcatServer = new TomcatServer("com.gc.webapp");tomcatServer.start();}}
手写Tomcat总结
在写之前,我们把思路整理了一下,然后把思路落地。
Servnet规范包
在Servnet包下,我们把传统的Servlet的基本规则和常用的方法功能以接口的形式定义出来了。包括Request中的获取参数,方法名称,访问路径等基本的操作。以及Response向客户端的写操作定义。还有Servnet中的doGet和doPos,我们这里只定义具体的声明,把具体的业务逻辑实现交给用户自己灵活编写。
Tomcat核心包
这里面主要是重写各个接口和抽象类的方法,把它们的定义的方法变成我们自己的具体业务实现。
- 我们使用DefaultNettyRequest和DefaultNettyResponse实现了NettyRequest和NettyResponse,在这里我们把定义实现了具体的操作。
- 我们使用DefaultServnet继承了Servent,分别处理get货post请求。让用户可以在自己的Servent中灵活的做业务逻辑。
我们编写了TomcatHandler来用于接受客户端的请求解析并处理。在TomcatHandler类中我们具体做的事情:
- 接受用户的http请求。
- 从一级缓存中拿到用户要访问的Servnet实例,如果没有则去二级缓存拿到类的全路径限定名,使用反射机制来动态创建出一个Servnet实例提供给本次请求需要用到的实例,创建好后把实例放到一级缓存。值得一提的是,我们怕当客户端出现并发访问出现线程安全问题可能会导致Map中的Servnet实例被重复创建,所以使用了双重检查锁来预防这个问题。
- 当Servnet实例创建好后,我们将组装Request和Response,其中Respone中我们传递了本次客户端请求的上下文,从该上下文中拿到相关信息组装成一个请求头。
- 然后获取到请求方式,分别执行响应的方法。
TomcatServer的主要作用有两点:
- 当容器启动时,模仿真正的Tomcat加载指定目录下的类(我们也可以称为具体的接口地址),到Tomcat的二级缓存中。其中Map的Key为具体的Servnet实例名称,Value为Servent源文件的具体路径。
第二件事是我们在这个类中定义了Netty的启动代码。主要定义了:
- 当出现并发时,队列可缓存的最大成功。
- 启用心跳机制来检测客户端的存活性,及时的释放链接,减轻服务器的负载压力。
- 定义了Http的编码/解码为一体的编解码器。以及实现我们自己的Handler。
- 最后,我们定义了启动方法main() 。在这里面我们指定了需要扫描加载成Servlet实例的具体包名路径。
