使用Netty开发Restfull应用
Http服务端代码
public class HttpServer {
public static void main(String[] args) {
HttpServer httpServer = new HttpServer();
int port = Integer.valueOf(args[0]);
httpServer.bind(port);
}
private void bind(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//HTTP解码器可能会将一个HTTP请求解析成多个消息对象。
ch.pipeline().addLast(new HttpServerCodec());
//HttpObjectAggregator 将多个消息转换为单一的一个FullHttpRequest
ch.pipeline().addLast(new HttpObjectAggregator(Short.MAX_VALUE));
//
ch.pipeline().addLast(new HttpServerHandler());
}
});
ChannelFuture f = b.bind("127.0.0.1", port).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
/*
* 处理http服务的处理
*/
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (!request.decoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
System.out.println("Http服务器接收请求:" + request);
ByteBuf body = request.content().copy();
//构建响应参数
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, body);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
ctx.writeAndFlush(response).sync();
System.out.println("Http服务器响应请求:" + response);
}
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
System.out.println(response);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
http执行调用的客户端
/**
* args: 127.0.0.1 8080
* Created by lijianzhen on 2019/1/16.
*/
public class HttpClient {
public static void main(String[] args) throws InterruptedException, UnsupportedEncodingException, ExecutionException {
HttpClient httpClient = new HttpClient();
httpClient.connect(String.valueOf(args[0]), Integer.valueOf(args[1]));
ByteBuf body = Unpooled.wrappedBuffer("HttpClient请求消息".getBytes("UTF-8"));
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "http://127.0.0.1/user?id=10&addr=山西", body);
HttpResponse response = httpClient.blockSend(request);
}
/**
* 阻塞发送
*
* @param request
* @return
*/
private HttpResponse blockSend(DefaultFullHttpRequest request) throws ExecutionException, InterruptedException {
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
//获取线程执行的结果信息
DefaultPromise<HttpResponse> respPromise = new DefaultPromise<>(channel.eventLoop());
//设置Promise
handler.setRespPromise(respPromise);
channel.writeAndFlush(request);
HttpResponse response = respPromise.get();
if (response != null) {
System.out.println("客户端请求http响应结果:" + new String(response.body()));
}
return response;
}
private Channel channel;
HttpClientHandler handler = new HttpClientHandler();
private void connect(String host, int port) throws InterruptedException {
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new HttpClientCodec());
ch.pipeline().addLast(new HttpObjectAggregator(Short.MAX_VALUE));
ch.pipeline().addLast(handler);
}
});
ChannelFuture f = b.connect(host, port).sync();
channel = f.channel();
}
}
/**
* 处理http响应参数的的处理Handler
* Created by lijianzhen on 2019/1/16.
*/
public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
DefaultPromise<HttpResponse> respPromise;
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
if (msg.decoderResult().isFailure())
throw new Exception("decoder HttpResponse error:" + msg.decoderResult().cause());
HttpResponse response = new HttpResponse(msg);
respPromise.setSuccess(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
public DefaultPromise<HttpResponse> getRespPromise() {
return respPromise;
}
public void setRespPromise(DefaultPromise<HttpResponse> respPromise) {
this.respPromise = respPromise;
}
}
包装响应对象
/**
* 构建响应参数
* Created by lijianzhen1 on 2019/1/16.
*/
public class HttpResponse {
private HttpHeaders header;
private FullHttpResponse response;
private byte [] body;
public HttpResponse(FullHttpResponse response)
{
this.header = response.headers();
this.response = response;
}
public HttpHeaders header()
{
return header;
}
public byte [] body()
{
//对直接内存操作不支持array方法,底层调用的是PooledUnsafeDirectByteBuf的array方法
return body = response.content() != null ?
response.content().array() : null;
}
}
启动服务并启动客户端请求后出现以下异常信息。
//服务端日志正常
Http服务器接收请求:HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 22, cap: 22, components=1))
GET http://127.0.0.1/user?id=10&addr=山西 HTTP/1.1
content-length: 22
Http服务器响应请求:DefaultFullHttpResponse(decodeResult: success, version: HTTP/1.1, content: PooledUnsafeDirectByteBuf(freed))
HTTP/1.1 200 OK
content-length: 22
//客户端日志出现以下异常信息
Exception in thread "main" java.lang.UnsupportedOperationException: direct buffer
at io.netty.buffer.PooledUnsafeDirectByteBuf.array(PooledUnsafeDirectByteBuf.java:343)
at io.netty.buffer.AbstractUnpooledSlicedByteBuf.array(AbstractUnpooledSlicedByteBuf.java:99)
at io.netty.buffer.CompositeByteBuf.array(CompositeByteBuf.java:596)
at com.janle.std.cases.http.HttpResponse.body(HttpResponse.java:46)
at com.janle.std.cases.http.HttpClient.blockSend(HttpClient.java:50)
at com.janle.std.cases.http.HttpClient.main(HttpClient.java:32)
对HttpResponse代码分析,发现消息体的获取来源是FullHttpResponse的content字段。response.content().array()底层调用的是PooledUnsafeDirectByteBuf的array方法,为了提升性能,Netty默认的I/O buffer使用直接内存DirectByteBuf,可以减少JVM用户态到内核态Socket读写的内存拷贝即“零拷贝”,由于是直接内存,无法直接转换成堆内存,因此并不支持array方法,用户需要自己做内存拷贝操作。
对body方法进行调整
调整HttpResponse类中的body方法,采用字节拷贝的方式将Http body拷贝到byte[]数组中,修改之后的代码
/**
* 构建响应参数
* Created by lijianzhen1 on 2019/1/16.
*/
public class HttpResponse {
private HttpHeaders header;
private FullHttpResponse response;
private byte[] body;
public HttpResponse(FullHttpResponse response)
{
this.header = response.headers();
this.response = response;
}
public byte [] body()
{
//Http body拷贝到byte[]数组中
body=new byte[response.content().readableBytes()];
//将body的数据返回到response中
response.content().getBytes(0,body);
return body;
}
public HttpHeaders header() {
return header;
}
}
启动测试后还是有异常
Exception in thread "main" io.netty.util.IllegalReferenceCountException: refCnt: 0
at io.netty.buffer.AbstractByteBuf.ensureAccessible(AbstractByteBuf.java:1417)
at io.netty.buffer.AbstractByteBuf.checkIndex(AbstractByteBuf.java:1356)
at io.netty.buffer.AbstractByteBuf.checkDstIndex(AbstractByteBuf.java:1376)
at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:854)
at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:44)
at io.netty.buffer.AbstractByteBuf.getBytes(AbstractByteBuf.java:474)
at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:1740)
at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:44)
at com.janle.std.cases.http.HttpResponse.body(HttpResponse.java:46)
at com.janle.std.cases.http.HttpClient.blockSend(HttpClient.java:50)
at com.janle.std.cases.http.HttpClient.main(HttpClient.java:32)
io.netty.util.IllegalReferenceCountException: refCnt: 0 说明是操作了已经被释放的对象。具体代码如下。ByteBuf实现ReferenceCounted接口,所以每次操作Bytebuf之前,都需要对ByteBuf的生命周期状态进行判断。
/**
* Should be called by every method that tries to access the buffers content to check
* if the buffer was released before.
*/
protected final void ensureAccessible() {
if (checkAccessible && refCnt() == 0) {
throw new IllegalReferenceCountException(0);
}
}
对业务代码进行分析,在收到一个完整的HTTP响应消息之后,调用respPromise的setSuccess方法,唤醒业务线程继续执行,相关代码如下:
在setSuccess前看见response.body()是能拿到数据的。
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
if (msg.decoderResult().isFailure())
throw new Exception("decoder HttpResponse error:" + msg.decoderResult().cause());
HttpResponse response = new HttpResponse(msg);
//这里打印出返回的参数
System.out.println("response unchannlRead "+ new String(response.body()));
respPromise.setSuccess(response);
}
但是在执行完 respPromise.setSuccess(response);后数据就没有了,这就回到了之前我们知道继承SimpleChannelInboundHandler是会自己释放内存的。channelRead0方法调用完成后,Netty会自动释放FullHttpResponse。源代码在SimpleChannelInboundHandler#channelRead之前已经讨论过了。
由于执行完channelRead0方法之后,线程就会调用ReferenceCountUtil.release(msg);释放内存,所有后续业务调用方的线程再访问FullHttpResponse就会出现非法引用问题。
再调整body的代码
public class HttpResponse {
private HttpHeaders header;
private FullHttpResponse response;
private byte[] body;
public HttpResponse(FullHttpResponse response) {
this.header = response.headers();
this.response = response;
//调整为在构造时候就直接放入body中
if (response.content() != null) {
body = new byte[response.content().readableBytes()];
response.content().getBytes(0, body);
}
}
public byte[] body() {
return body;
}
public HttpHeaders header() {
return header;
}
}
再次测试验证Http客户端运行正常
总结:
- 跨线程操作ByteBuf操作,要防止Netty NioEventLoop线程与应用线程并发操作Bytebuf
- ByteBuf的申请和释放,避免忘记释放,重复释放,以及释放之后继续访问。需要注意ByteBuf隐式释放的问题。
- 在get操作时候不要做复杂的操作,比如内存拷贝,会带来严重的性能问题。