目录
一、简介
开发中我们肯定会遇到一个系统需要调用另外一个系统接口的情况,此时如果让我们自己来手写整个调用工具,想必是非常麻烦的。
OkHttp3就是为了解决这样场景的一个默认高效的HTTP工具包。它有以下特点:
- HTTP/2支持允许对同一主机的所有请求共享一个套接字;
- 连接池减少了请求延迟(如果HTTP/2不可用);
- 透明的GZIP压缩了下载文件的大小;
- 响应缓存完全避免了网络中的重复请求。
当网络出现问题时,OkHttp会不会中断:它会从常见的连接问题中悄悄地恢复过来。如果服务有多个IP地址,第一个连接失败,OkHttp将尝试备用地址。这对于IPv4+IPv6和托管在冗余数据中心中的服务是必要的。OkHttp支持现代TLS特性(TLS 1.3、ALPN、证书固定)。可以将其配置为回退以实现广泛的连接。
使用OkHttp很容易。它的请求/响应API是用构建器Builder和不变性设计的。它同时支持同步阻塞调用和异步调用。
二、使用实例
自己写的一个使用实例代码:
public class HttpUtils {
private static final char QUESTION_MARK = '?';
private static final char EQUALS_SIGN = '=';
private static final char AND_SIGN = '&';
private static final MediaType JSON_UTF8 =
MediaType.get("application/json; charset=utf-8");
private static final String EMPTY_JSON = "{}";
private static final OkHttpClient CLIENT = new OkHttpClient();
/**
* 使用GET方法请求url,返回string
*
* @param url
* @param param
* @return
* @throws Exception
*/
public static String getStringReturnFromMap(String url,
Map<String, Object> param) throws Exception {
return executeClientCall(buildGetRequest(url, param));
}
/**
* 使用GET方法请求url,返回map
*
* @param url
* @param param
* @return
* @throws Exception
*/
public static Map<String, Object> getMapReturnFromMap(String url,
Map<String, Object> param) throws Exception {
String result = getStringReturnFromMap(url, param);
if (StringUtils.isEmpty(result)) {
return new HashMap<>(2);
}
return JSONObject.parseObject(result, Map.class);
}
/**
* 使用POST方法请求url,返回string
*
* @param url
* @param param
* @return
* @throws Exception
*/
public static String postStringReturnFromMap(String url,
Map<String, Object> param) throws Exception {
return executeClientCall(buildPostRequest(url,
CollectionUtils.isEmpty(param) ?
EMPTY_JSON : JSONObject.toJSONString(param)));
}
/**
* 使用POST方法请求url,返回map
*
* @param url
* @param param
* @return
* @throws Exception
*/
public static Map<String, Object> postMapReturnFromMap(String url,
Map<String, Object> param) throws Exception {
String result = postStringReturnFromMap(url, param);
if (StringUtils.isEmpty(result)) {
return new HashMap<>(2);
}
return JSONObject.parseObject(result, Map.class);
}
/**
* 构造GET请求
*
* @param url
* @param param
* @return
*/
private static Request buildGetRequest(String url,
Map<String, Object> param) {
if (CollectionUtils.isEmpty(param)) {
return new Request.Builder().url(url).build();
}
// 判断?
url = url.charAt(url.length() - 1) == QUESTION_MARK ?
url : url + QUESTION_MARK;
StringBuilder getUrl = new StringBuilder(url);
// 拼装GET参数
param.forEach((k, v) -> getUrl.append(k).append(EQUALS_SIGN)
.append(v).append(AND_SIGN));
url = getUrl.substring(0, getUrl.length() - 1);
return new Request.Builder().url(url).build();
}
/**
* 构造POST请求
*
* @param url
* @param json
* @return
*/
private static Request buildPostRequest(String url, String json) {
return new Request.Builder().url(url)
.post(RequestBody.create(JSON_UTF8, json)).build();
}
/**
* 执行request
*
* @param request
* @return
* @throws Exception
*/
private static String executeClientCall(Request request)
throws Exception{
Response response = CLIENT.newCall(request).execute();
String bodyString;
if (response.isSuccessful() && response.body() != null &&
!StringUtils.isEmpty(bodyString = response.body().string())) {
return bodyString;
}
return null;
}
}
三、使用细节
OkHttp3使用时有个坑需要特别注意,因为OkHttp3使用时每个OkHttpClient类里面是维护了一个ConnectionPool线程池的,因此使用时没必要为该类去再使用池管理它。特别注意:使用时每一个接口链接不能使用一次OkHttpClient就实例化一遍,这样并发量大的时候很容易造成Socket套接字用完以及内存溢出导致宕机。
1.错误使用示例
错误的使用方法:
private static String executeClientCall(Request request) throws Exception{
Response response = new OkHttpClient().newCall(request).execute();
String bodyString;
if (response.isSuccessful() && response.body() != null
&& !StringUtils.isEmpty(bodyString = response.body().string())) {
return bodyString;
}
return null;
}
每次调用都创建一个新的OkHttpClient对象,这样使用时JVM的状态如下图:
这张图是使用JVM自带的JConsole可视化得到的,程序模拟了500个线程每个线程只请求1000次的情景,可以明显的看到线程数量和CPU占有率是分三个阶段的:
- 第一个阶段为刚开始请求时,程序还尚能处理的过来,但是当OkHttpClient实例化的数量过多以及其对象存活时间(默认五分钟)尚未结束依然保留着Connection链接,占用套接字资源,导致线程和CPU占用率会突然升高;
- 第二个阶段便是CPU占用率突然降到0,线程数量也趋于平缓,此时由于套接字资源全部被占用,且没有被释放,会导致无法访问到外面的机器,连接超时,因此CPU占用率会完全降下来,而线程数却保持不变;
- 当第一批的OkHttpClient实例由于超时被处理后,第二批便会开始执行,但是此时Socket套接字资源已经被完全占用完,没有新的资源,此时就会一直抛异常No buffer space available (maximum connections reached?): connect甚至是Address Alreay in use异常。导致不停打印异常栈,再加上刚刚的资源没有被完全释放,最终导致了CPU占用率飙升,况且到此为止,程序处理的数量才完成了30/500,如果一直下去服务器宕机是必然的的。
2.正确使用示例
2.1 比较粗暴的方式
会造成内存溢出进而宕机是因为每个Connection存活的时间实在是太长了,因此占用了很多的系统资源,那么我们直接把每个Connection的时间改成1s,是不是就能解决这个问题了呢?
使用代码如下:
private static String executeClientCall(Request request) throws Exception{
Response response = new OkHttpClient()
.newBuilder()
.connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS))
.build().newCall(request).execute();
String bodyString;
if (response.isSuccessful() && response.body() != null
&& !StringUtils.isEmpty(bodyString = response.body().string())) {
return bodyString;
}
return null;
}
每次依然是创建一个新的OkHttpClient,只是存活时间改成了1s,效果图如下:
可以看到,这次运行了4min钟CPU占用率一直在50%左右,而线程数经过第一次的飙升到2500+之后伺候就一直没超过1000,最关键的是执行完1/5左右的请求数之后,系统虽然也被占用了很多资源,但是机器还能够稳定运行,至少不会宕机,因此这种策略还是可取的。但是这种方式还是会造成Address already in use以及套接字缓存数量被用光的异常。
2.2 正确的方式
要正确使用OkHttp3也很简单:将OkHttpClient换成单例存在工具类中即可。如下:
private static final OkHttpClient CLIENT = new OkHttpClient();
private static String executeClientCall(Request request) throws Exception{
Response response = CLIENT.newCall(request).execute();
String bodyString;
if (response.isSuccessful() && response.body() != null
&& !StringUtils.isEmpty(bodyString = response.body().string())) {
return bodyString;
}
return null;
}
效果图如下:
可以看到500个线程,每个线程1000次请求对于这种使用方式而言完全就是小问题,1min+完成,并且CPU占用率一直维持在15%左右,线程也是在45个左右,效率和性能可以说是非常高。
后话:由此不难看出,程序的性能问题可以说大部分都是和代码细节相关的,谁能想到把一个类变成单例模式后便能够取得如此巨大的性能突破?这次对OkHttp3的性能摸索可以说让我意识到了代码优化以及平时编码的习惯都是从小慢慢积累的,这才是真正的细节决定成败啊。