功能实现:使用netty构建一个类似于tomcat的web服务器,服务端监听8899端口,当访问8899端口的时候,服务器端给客户端hello world的响应。
服务端代码
启动主程序
public class TestServer {
public static void main(String[] args) throws Exception {
/*
定义两个事件循环组
NioEventLoopGroup 就是一个死循环,不断接受客户端发起的连接并处理,和tomcat这种服务器一样
bossGroup 用于接受客户端的连接但是不做处理,会把连接转给workerGroup
workerGroup 会对连接进行处理,进行对应的业务处理,最后把结果返回给客户端
*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//ServerBootstrap用于启动服务端
ServerBootstrap serverBootstrap = new ServerBootstrap();
/*
group() 事件循环组
channel() 用到的管道 使用反射的方式创建的
childHandler() 子处理器,自己编写的处理器,
请求到来之后由我们自己编写的处理器进行真正的处理
*/
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
//绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
//关闭的监听
channelFuture.channel().closeFuture().sync();
}finally {
//关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
初始化器 (Initializer)
客户端与服务端一旦连接之后,TestServerInitializer
就会被创建 initChannel()
方法就会被调用
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
/**
* 连接(Channel)一旦被注册之后该方法就会被调用
* 回调方法
*/
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//一个管道,一个管道当中可以有多个ChannelHandler(拦截器),
// 每个拦截器做的事情就是针对自己本身的请求或业务情况,完成相应的处理
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("httpServerCodec",new HttpServerCodec());
pipeline.addLast("testHttpServerHandler",new TestHttpServerHandler());
}
}
自定义处理器 (Handler)
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
/**
* channelRead0 读取客户端发过来的请求,并且向客户端返回响应的方法
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
System.out.println(msg.getClass());
//打印出远程的地址,/0:0:0:0:0:0:0:1: 52434,本地线程的49734端口的线程和netty进行通信
System.out.println(ctx.channel().remoteAddress());
//Thread.sleep(8000); 用于测试持续连接(keepalive)
if(msg instanceof HttpRequest){
HttpRequest httpRequest = (HttpRequest)msg;
System.out.println("请求方法名:"+httpRequest.method().name());
URI uri = new URI(httpRequest.uri());
//使用浏览器访问localhost:8899会发送二次请求,
//其中有一次是localhost:8899/favicon.ico 这个url请求访问网站的图标
if("/favicon.ico".equals(uri.getPath())){
System.out.println("请求favicon.ico");
return;
}
//向客户端返回的响应内容
ByteBuf content =
Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8);
//构建一个响应
//DefaultFullHttpResponse(http协议的版本,返回的状态码,响应内容);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,content);
//设置response相应的头信息
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
//返回客户端
ctx.writeAndFlush(response);
//手动关闭连接 如果要测试持续连接需要注掉 ctx.channel().close();
//其实更合理的close连接应该判断是http1.O还是1.1来进行判断请求超时时间来断开channel连接。
ctx.channel().close();
}
}
/**通道注册时调用*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel registered");
super.channelRegistered(ctx);
}
/**通道活跃时调用*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel active");
super.channelActive(ctx);
}
/**处理程序已添加时调用*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("handler added");
super.handlerAdded(ctx);
}
/**通道停止活跃时调用*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel inactive");
super.channelInactive(ctx);
}
/**通道取消注册时调用*/
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel unregistered");
super.channelUnregistered(ctx);
}
/**连接断开之后*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
super.handlerRemoved(ctx);
}
}
请求测试
curl访问
❯ curl 'localhost:8899'
Hello World%
服务端显示
handler added //通道添加
channel registered //通道注册
channel active //通道活跃
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46082
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46082
channel inactive //通道不活跃
channel unregistered //通道取消注册
使用curl工具请求服务端,当请求结束结果返回之后通道/连接马上就被close掉了,服务端使用http1.1 同样如此,curl只是一个单次请求响应的工具,并没有使用到http1.1的keepalive 持续连接特性
Google浏览器访问
服务器终端显示
//第一次请求
handler added
handler added
channel registered
channel registered
channel active
channel active
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46500
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46500
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46500
请求方法名:GET
请求favicon.ico
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46500
//第二次请求
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46502
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46502
channel inactive //第一个通道因为在保持时间内没有第二个请求复用该连接,被close
channel unregistered //第一个通道因为在保持时间内没有第二个请求复用该连接,被close
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46502
请求方法名:GET
请求favicon.ico
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46502
Google浏览器的访问使用到了持续连接的特性,第一次请求 浏览器分别请求了localhost:8899 和localhost:8899/favicon.ico两个路经,所以上面开头就添加/注册了两次通道
- 不是持续连接吗?为什么会添加/注册两次通道,个人理解是第一个通道还没添加注册成为可用通道状态之前,第二个连接就到达了,就又创建了一个新的通道,可见输出信息也是这样
第一次请求的localhost:8899和localhost:8899/favicon.ico两个请求的通道/连接用的都是同一个建立在46500端口之上的通道,第二次请求的全部操作都使用了另一个通道(46502)
lsof查看端口绑定关系
请求之前
❯ lsof -i:8899
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 21617 sakura 241u IPv6 530665 0t0 TCP *:ospf-lite (LISTEN) //程序本身
第一次请求之后
❯ lsof -i:8899
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
chrome 1858 sakura 47u IPv6 516947 0t0 TCP localhost:46500->localhost:ospf-lite (ESTABLISHED)
chrome 1858 sakura 53u IPv6 516948 0t0 TCP localhost:46502->localhost:ospf-lite (ESTABLISHED)
java 21617 sakura 241u IPv6 530665 0t0 TCP *:ospf-lite (LISTEN)
java 21617 sakura 243u IPv6 527749 0t0 TCP localhost:ospf-lite->localhost:46500 (ESTABLISHED)
java 21617 sakura 244u IPv6 524734 0t0 TCP localhost:ospf-lite->localhost:46502 (ESTABLISHED)
第二次请求之后
❯ lsof -i:8899
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
chrome 1858 sakura 53u IPv6 516948 0t0 TCP localhost:46502->localhost:ospf-lite (ESTABLISHED)
java 21617 sakura 241u IPv6 530665 0t0 TCP *:ospf-lite (LISTEN)
java 21617 sakura 244u IPv6 524734 0t0 TCP localhost:ospf-lite->localhost:46502 (ESTABLISHED)