使用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操作時候不要做複雜的操作,比如內存拷貝,會帶來嚴重的性能問題。