/*
* (C) Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* ohun@live.cn (夜色)
*/
package com.mpush.netty.http;
import com.mpush.api.service.BaseService;
import com.mpush.api.service.Listener;
import com.mpush.tools.config.CC;
import com.mpush.tools.thread.NamedThreadFactory;
import com.mpush.tools.thread.ThreadNames;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
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.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.util.AttributeKey;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import static com.mpush.tools.config.CC.mp.thread.pool.http_work;
import static com.mpush.tools.thread.ThreadNames.T_HTTP_TIMER;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
/**
* Netty的一个Bootstrap是可以关联多个channel的,
* 本Client采用的就是这种模式,在种模式下如果Handler添加了@ChannelHandler.Sharable
* 注解的话,要特殊处理,因为这时的client和handler是被所有请求共享的。
* <p>
* Created by ohun on 2016/2/15.
*
* @author ohun@live.cn
*/
public class NettyHttpClient extends BaseService implements HttpClient {
private static HttpClient I;
private static final Logger LOGGER = LoggerFactory.getLogger(NettyHttpClient.class);
private static final int maxContentLength = (int) CC.mp.http.max_content_length;
/*package*/ final AttributeKey<RequestContext> requestKey = AttributeKey.newInstance("request");
/*package*/ final HttpConnectionPool pool = new HttpConnectionPool();
private Bootstrap b;
private EventLoopGroup workerGroup;
private Timer timer;
public static HttpClient I() {
if (I == null) {
synchronized (NettyHttpClient.class) {
if (I == null) {
I = new NettyHttpClient();
}
}
}
return I;
}
@Override
public void request(RequestContext context) throws Exception {
URI uri = new URI(context.request.uri());
String host = context.host = uri.getHost();
int port = uri.getPort() == -1 ? 80 : uri.getPort();
//1.设置请求头
context.request.headers().set(HOST, host);//映射后的host
context.request.headers().set(CONNECTION, KEEP_ALIVE);//保存长链接
//2.添加请求超时检测队列
timer.newTimeout(context, context.readTimeout, TimeUnit.MILLISECONDS);
//3.先尝试从连接池里取可用链接,去取不到就创建新链接。
Channel channel = pool.tryAcquire(host);
if (channel == null) {
final long startCreate = System.currentTimeMillis();
LOGGER.debug("create new channel, host={}", host);
ChannelFuture f = b.connect(host, port);
f.addListener((ChannelFutureListener) future -> {
LOGGER.debug("create new channel cost={}", (System.currentTimeMillis() - startCreate));
if (future.isSuccess()) {//3.1.把请求写到http server
writeRequest(future.channel(), context);
} else {//3.2如果链接创建失败,直接返回客户端网关超时
context.tryDone();
context.onFailure(504, "Gateway Timeout");
LOGGER.warn("create new channel failure, request={}", context);
}
});
} else {
//3.1.把请求写到http server
writeRequest(channel, context);
}
}
private void writeRequest(Channel channel, RequestContext context) {
channel.attr(requestKey).set(context);
pool.attachHost(context.host, channel);
channel.writeAndFlush(context.request).addListener((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
RequestContext info = future.channel().attr(requestKey).getAndSet(null);
info.tryDone();
info.onFailure(503, "Service Unavailable");
LOGGER.debug("request failure request={}", info);
pool.tryRelease(future.channel());
}
});
}
@Override
protected void doStart(Listener listener) throws Throwable {
workerGroup = new NioEventLoopGroup(http_work, new DefaultThreadFactory(ThreadNames.T_HTTP_CLIENT));
b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.TCP_NODELAY, true);
b.option(ChannelOption.SO_REUSEADDR, true);
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4000);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("decoder", new HttpResponseDecoder());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(maxContentLength));
ch.pipeline().addLast("encoder", new HttpRequestEncoder());
ch.pipeline().addLast("handler", new HttpClientHandler(NettyHttpClient.this));
}
});
timer = new HashedWheelTimer(new NamedThreadFactory(T_HTTP_TIMER), 1, TimeUnit.SECONDS, 64);
listener.onSuccess();
}
@Override
protected void doStop(Listener listener) throws Throwable {
pool.close();
workerGroup.shutdownGracefully();
timer.stop();
listener.onSuccess();
}
}