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) {
//定义用于处理客户端连接的EventLoopGroup
EventLoopGroup parentEventLoop = new NioEventLoopGroup();
//定义用于处理客户端请求的EventLoopGroup
EventLoopGroup 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>() {
@Override
protected 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) {
//定义用于处理客户端连接的EventLoopGroup
EventLoopGroup parentEventLoop = new NioEventLoopGroup();
//定义用于处理客户端请求的EventLoopGroup
EventLoopGroup 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>() {
@Override
protected 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 {
//客户端处理服务端的EventLoopGroup
EventLoopGroup clint = new NioEventLoopGroup();
//Bootstrap专门用于客户端的属性设置以及关系绑定
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(clint)
//使用NIO客户端的Channel
.channel(NioSocketChannel.class)
//绑定编码器/解码器。以及自定义的Handler
.handler(new ChannelInitializer<SocketChannel>(){
@Override
protected 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);
}
//当发生异常,捕获并关闭
@Override
public 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 {
//客户端处理服务端的EventLoopGroup
EventLoopGroup clint = new NioEventLoopGroup();
//Bootstrap专门用于客户端的属性设置以及关系绑定
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(clint)
//使用NIO客户端的Channel
.channel(NioSocketChannel.class)
//绑定编码器/解码器。以及自定义的Handler
.handler(new ChannelInitializer<SocketChannel>(){
@Override
protected 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 {
//下线后,删除监听任务。 否则数据量大了,可能会导致OOM
scheduled.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);
}
//当发生异常,捕获并关闭
@Override
public 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) {
//定义用于处理客户端连接的EventLoopGroup
EventLoopGroup parentEventLoop = new NioEventLoopGroup();
//定义用于处理客户端请求的EventLoopGroup
EventLoopGroup 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>() {
@Override
protected 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
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收到Client发送的消息:" + msg);
}
/**
* 异常捕捉
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
/**
* 用于捕获当前Server中的各种事件
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public 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中的doGet
public abstract void doGet(NettyRequest request, NettyResponse response) throws Exception;
//定义Servlet中的doPost
public 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;
}
@Override
public String getUri() {
return request.uri();
}
@Override
public String getMethod() {
return request.method().name();
}
@Override
public String getPath() {
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
return decoder.path();
}
@Override
public Map<String, List<String>> getParameters() {
QueryStringDecoder decoder = new QueryStringDecoder(getUri());
return decoder.parameters();
}
private int count = 0;
@Override
public List<String> getParameters(String name) {
Map<String, List<String>> map = getParameters();
return map.get(name);
}
@Override
public 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;
}
@Override
public 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
@Override
public void doGet(NettyRequest request, NettyResponse response) throws Exception {
String sernetName = request.getUri().split("/")[1];
response.write("404 - no this servnet : " + sernetName);
}
//定义Servlet中的doPost
@Override
public 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 {
//启动时候加载到这个Map
private 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;
}
@Override
public 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("?"));
//获得客户端要访问的Servnet
Servnet servnet = new DefaultServnet();
//一级缓存是否有这个key
if(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();
}
}
@Override
public 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<>();
//加载该路径下的类为Servnet
private String basePackage;
public TomcatServer(String basePackage) {
this.basePackage = basePackage;
}
// 启动tomcat
public void start() throws Exception {
// 加载指定包中的所有Servnet的类名
cacheClassName(basePackage);
//资源准备完毕,启动Netty
runServer();
}
private void cacheClassName(String basePackage) throws UnsupportedEncodingException {
//获取指定路径下的类
URL resource = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));
if(null == resource){
System.out.println("没有找到访问资源");
return;
}
//将取到的资源转成一个File
File 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() {
@Override
protected 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 {
@Override
public 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);
}
@Override
public 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实例的具体包名路径。