网络踩坑小记

摘要

最近做需求时,碰到几个和网络相关的问题,特此记录,也算是对错误的一个总结。

1.多线程执行顺序问题

现象:

考拉商品详情页请求时,需要多个接口同时发出请求,出现请求丢失或者重复请求的状况。

现状:

目前考拉网络库底层改造,采用了aidl的方式,将网络请求抛到另一个进程里,减少主进程的CPU、内存等消耗,并且可以有效的防止网络进程的崩溃影响到主进程使用。

为了解决问题,我们先查看代码的调用顺序:

  1. HttpManager#executeRequest
  2. RemoteCallHelper.mNetServiceProxyWrapper(IKaolaNetAidlInterface)#execute
  3. RemoteCallHelper#postToNetService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private IKaolaNetAidlInterface mNetServiceProxyWrapper = new IKaolaNetAidlInterface.Stub() {
@Override
public void execute(String method, String host, String path, String jsonBody, IKaolaRequestListener listener)
throws RemoteException {
postToNetService(() -> {
try {
mNetServiceProxy.execute(method, host, path, jsonBody, listener);
} catch (Exception e) {
ExceptionUtils.uploadCatchedException(e);
}
});
}
};
public void postToNetService(final Runnable onConnectRunnable) {
mOnConnectRunnables.add(onConnectRunnable);
if (mNetServiceProxy == null) {
boolean bound = mContext.bindService(new Intent(mContext,
mRemoteNetCallEnable ? KaolaNetService.class : KaolaNetService.KaolaNetLocalService.class),
mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
if (!bound) {
mHandler.postDelayed(this::testNetService, BIND_TO_NET_SERVICE_RETRY_DELAY);
}
} else {
runAndFlushOnConnectRunnables();
}
}
private void runAndFlushOnConnectRunnables() {
for (Runnable r : mOnConnectRunnables) {
r.run();
}
mOnConnectRunnables.clear();
}

不了解通过AIDL实现Binder流程的,可以查看Android中AIDL的创建流程

通过下面这张Binder机制图,

我们知道生成的IKaolaNetAidlInterface文件中onTransact方法是在运行在线程池中。但在以上代码中没有看到关于线程同步的任何代码。

因为没有线程同步,无法保证mOnConnectRunnables集合add、遍历、clear的执行顺序:

  1. add后立刻被clear,导致请求丢失;
  2. 遍历后再次遍历,导致请求重复;

找到问题原因,那么解决起来就很容易了。考虑到使用synchronized关键字可能导致的阻塞问题,我们创建只有1个线程的线程池,来保证对mOnConnectRunnables集合的有序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private ExecutorService mThreadExecutor = Executors.newSingleThreadExecutor();
public void postToNetService(final Runnable onConnectRunnable) {
mThreadExecutor.execute(new Runnable() {
@Override
public void run() {
mOnConnectRunnables.add(onConnectRunnable);
}
});
if (mNetServiceProxy == null) {
boolean bound = mContext.bindService(new Intent(mContext,
mRemoteNetCallEnable ? KaolaNetService.class : KaolaNetService.KaolaNetLocalService.class),
mServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
if (!bound) {
mHandler.postDelayed(this::testNetService, BIND_TO_NET_SERVICE_RETRY_DELAY);
}
} else {
runAndFlushOnConnectRunnables();
}
}
private void runAndFlushOnConnectRunnables() {
mThreadExecutor.execute(new Runnable() {
@Override
public void run() {
for (Runnable r : mOnConnectRunnables) {
r.run();
}
mOnConnectRunnables.clear();
}
});
}

2.POST请求重定向问题

charles抓https请求偶尔抽风,为了开发方便,我们一般会把https请求强制变为http请求,如下图所示,是一个https的post请求:

这个请求要求必须使用https,所以当它变成http请求时就会报301,在请求的Response的header里,Location给我们指定了一个新的https的请求路径,发出请求后,这个post请求竟然变成了get请求:

再三确定代码无误后,开启debug之旅,最终发现,在okhttp中存在一个RetryAndFollowUpInterceptor拦截器,其中关键方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private Request followUpRequest(Response userResponse, Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
....
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:// 301
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
....
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse, url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
....
default:
return null;
}
}

我们可以看到就是requestBuilder.method("GET", null);这行代码将post请求变成了get请求,我们可以看看它的两个前置条件:

1
2
3
4
5
6
7
8
public static boolean permitsRequestBody(String method) {
return !(method.equals("GET") || method.equals("HEAD"));
}
public static boolean redirectsToGet(String method) {
// All requests but PROPFIND should redirect to a GET request.
return !method.equals("PROPFIND");
}

很明显,此次请求的method为post,两个方法都返回true导致method从post变成了get。

3.POST请求body设置问题


在环境、参数、请求都相同的情况下,请求依然一直报错。使用charles做对比,发现body的内容和content-type类型无法匹配,导致出错。

我们默认使用content-type为application/json; charset=utf-8作为请求响应头。

因为这次请求并没有设置body内容,所以调用底层网络框架时,传入参数null。但底层框架库将传入的参数直接通过json转换为字符串null导致出错:

找到问题,只要将body内容为空的请求默认设置为{}即可。