遇到一个问题: 需要给所有的请求加签名校验以防刷接口; 传入请求 url 及 body 生成一个文本串作为一个 header 传给服务端; 已经有现成的签名检验方法 String doSignature(String url, byte[] body);
当前网络库基于com.squareup.okhttp3:okhttp:3.14.2
.
这很简单了, 当然是写一个 interceptor
然后将 request 对象的 url 及 body 传入就好. 于是有:
public class SignInterceptor implements Interceptor {
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {Request request = chain.request();
RequestBody body = request.body();
byte[] bodyBytes = null;
if (body != null) {final Buffer buffer = new Buffer();
body.writeTo(buffer);
bodyBytes = buffer.readByteArray();}
Request.Builder builder = request.newBuilder();
HttpUrl oldUrl = request.url();
final String url = oldUrl.toString();
final String signed = doSignature(url, bodyBytes));
if (!TextUtils.isEmpty(signed)) {builder.addHeader(SIGN_KEY_NAME, signed);
}
return chain.proceed(builder.build());
}
}
okhttp 的 ReqeustBody
是一个抽象类, 内容输出只有 writeTo
方法, 将内容写入到一个 BufferedSink
接口实现体里, 然后再将数据转成 byte[]
也就是内存数组. 能达到目的的类只有 Buffer
, 它实现了BufferedSink
接口并能提供转成内存数组的方法readByteArray
. 这貌似没啥问题呀, 能造成 OOM?
是的, 要看请求类型, 如果是一个上传文件的接口呢? 如果这个文件比较大呢? 上传接口有可能会用到 public static RequestBody create(final @Nullable MediaType contentType, final File file)
方法, 如果是针对文件的实现体它的 writeTo
方法是 sink.writeAll(source);
而我们传给签名方法时用到的 Buffer.readByteArray
是将缓冲中的所有内容转成了内存数组, 这意味着文件中的所有内容被转成了内存数组, 就是在这个时机容易造成 OOM! RequestBody.create
源码如下:
public static RequestBody create(final @Nullable MediaType contentType, final File file) {if (file == null) throw new NullPointerException("file == null");
return new RequestBody() {@Override public @Nullable MediaType contentType() {return contentType;}
@Override public long contentLength() {return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {try (Source source = Okio.source(file)) {sink.writeAll(source);
}
}
};
}
可以看到实现体持有了文件,Content-Length
返回了文件的大小, 内容全部转给了 Source
对象。
这确实是以前非常容易忽略的一个点, 很少有对请求体作额外处理的操作, 而一旦这个操作变成一次性的大内存分配, 非常容易造成 OOM. 所以要如何解决呢? 签名方法又是如何处理的呢? 原来这个签名方法在这里偷了个懒——它只读取传入 body 的前 4K 内容, 然后只针对这部分内容进行了加密, 至于传入的这个内存数组本身多大并不考虑, 完全把风险和麻烦丢给了外部(优秀的 SDK!).
快速的方法当然是罗列白名单, 针对上传接口服务端不进行加签验证, 但这容易挂一漏万, 而且增加维护成本, 要签名方法 sdk 的人另写合适的接口等于要他们的命, 所以还是得从根本解决. 既然签名方法只读取前 4K 内容, 我们便只将内容的前 4K 部分读取再转成方法所需的内存数组不就可了? 所以我们的目的是: 期望 RequestBody
能够读取一部分而不是全部的内容 . 能否继承RequestBody
重写它的 writeTo
? 可以, 但不现实, 不可能全部替代现有的RequestBody
实现类, 同时 ok 框架也有可能创建私有的实现类. 所以只能针对 writeTo
的参数 BufferedSink
作文章, 先得了解 BufferedSink
又是如何被 okhttp 框架调用的.
BufferedSink
相关的类包括 Buffer, Source
, 都属于 okio 框架,okhttp 只是基于 okio 的一坨, okio 没有直接用 java 的 io 操作, 而是另行写了一套 io 操作, 具体是数据缓冲的操作. 接上面的描述, Source
是怎么创建, 同时又是如何操作 BufferedSink
的? 在 Okio.java
中:
public static Source source(File file) throws FileNotFoundException {if (file == null) throw new IllegalArgumentException("file == null");
return source(new FileInputStream(file));
}
public static Source source(InputStream in) {return source(in, new Timeout());
}
private static Source source(final InputStream in, final Timeout timeout) {return new Source() {@Override public long read(Buffer sink, long byteCount) throws IOException {
try {timeout.throwIfReached();
Segment tail = sink.writableSegment(1);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
if (bytesRead == -1) return -1;
tail.limit += bytesRead;
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
@Override public void close() throws IOException {in.close();
}
@Override public Timeout timeout() {return timeout;}
};
}
Source
把文件作为输入流 inputstream
进行了各种读操作, 但是它的 read
方法参数却是个 Buffer
实例,它又是从哪来的,又怎么和 BufferedSink
关联的?只好再继续看 BufferedSink.writeAll
的实现体。
BufferedSink
的实现类就是 Buffer
,然后它的writeAll
方法:
@Override public long writeAll(Source source) throws IOException {if (source == null) throw new IllegalArgumentException("source == null");
long totalBytesRead = 0;
for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {totalBytesRead += readCount;}
return totalBytesRead;
}
原来是显式的调用了 Source.read(Buffer,long)
方法,这样就串起来了,那个 Buffer
参数原来就是自身。
基本可以确定只要实现 BufferedSink
接口类, 然后判断读入的内容超过指定大小就停止写入就返回就可满足目的, 可以名之FixedSizeSink
.
然而麻烦的是 BufferedSink
的接口非常多, 将近 30 个方法,不知道框架会在什么时机调用哪个方法,只能全部都实现! 其次是接口方法的参数有很多 okio 的类, 这些类的用法需要了解, 否则一旦用错了效果适得其反. 于是对一个类的了解变成对多个类的了解, 没办法只能硬着头皮写.
第一个接口就有点蛋疼: Buffer buffer();
BufferedSink
返回一个 Buffer
实例供外部调用, BufferedSink
的实现体即是 Buffer
, 然后再返回一个Buffer
?! 看了半天猜测BufferedSink
是为了提供一个可写入的缓冲对象, 但框架作者也懒的再搞接口解耦的那一套了 (唉, 大家都是怎么简单怎么来). 于是FixedSizeSink
至少需要持有一个 Buffer
对象, 它作实际的数据缓存,同时可以在需要 Source.read(Buffer ,long)
的地方作为参数传过去.
同时可以看到 RequestBody
的一个实现类 FormBody
, 用这个Buffer
对象直接写入一些数据:
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {buffer = new Buffer();
} else {buffer = sink.buffer();
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {if (i > 0) buffer.writeByte('&');
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {byteCount = buffer.size();
buffer.clear();}
return byteCount;
}
有这样的操作就有可能限制不了缓冲区大小变化!不过数据量应该相对小一些而且这种用法场景相对少,我们指定的大小应该能覆盖的了这种情况。
接着还有一个接口 BufferedSink write(ByteString byteString)
, 又得了解ByteString
怎么使用, 真是心力交瘁啊 …
@Override public Buffer write(ByteString byteString) {byteString.write(this);
return this;
}
Buffer
实现体里可以直接调用 ByteString.write(Buffer)
因为是包名访问,自己实现的 FixedSizeSink
声明在和同一包名 package okio;
也可以这样使用,如果是其它包名只能先转成 byte[]
了, ByteString
应该不大不然也不能这么搞(没有找到 ByteString 读取一段数据的方法):
@Override
public BufferedSink write(@NotNull ByteString byteString) throws IOException {byte[] bytes = byteString.toByteArray();
this.write(bytes);
return this;
}
总之就是把这些对象转成内存数组或者 Buffer
能够接受的参数持有起来!
重点关心的 writeAll
反而相对好实现一点, 我们连续读取指定长度的内容直到内容长度达到我们的阈值就行.
还有一个蛋疼的点是各种对象的 read/write 数据流方向:Caller.read(Callee)/Caller.write(Callee)
, 有的是从 Caller 到 Callee, 有的是相反,被一个小类整的有点头疼……
最后上完整代码, 如果发现什么潜在的问题也可以交流下~:
public class FixedSizeSink implements BufferedSink {
private static final int SEGMENT_SIZE = 4096;
private final Buffer mBuffer = new Buffer();
private final int mLimitSize;
private FixedSizeSink(int size) {this.mLimitSize = size;}
@Override
public Buffer buffer() {return mBuffer;}
@Override
public BufferedSink write(@NotNull ByteString byteString) throws IOException {byte[] bytes = byteString.toByteArray();
this.write(bytes);
return this;
}
@Override
public BufferedSink write(@NotNull byte[] source) throws IOException {this.write(source, 0, source.length);
return this;
}
@Override
public BufferedSink write(@NotNull byte[] source, int offset,
int byteCount) throws IOException {long available = mLimitSize - mBuffer.size();
int count = Math.min(byteCount, (int) available);
android.util.Log.d(TAG, String.format("FixedSizeSink.offset=%d,"
"count=%d,limit=%d,size=%d",
offset, byteCount, mLimitSize, mBuffer.size()));
if (count > 0) {mBuffer.write(source, offset, count);
}
return this;
}
@Override
public long writeAll(@NotNull Source source) throws IOException {this.write(source, mLimitSize);
return mBuffer.size();}
@Override
public BufferedSink write(@NotNull Source source, long byteCount) throws IOException {final long count = Math.min(byteCount, mLimitSize - mBuffer.size());
final long BUFFER_SIZE = Math.min(count, SEGMENT_SIZE);
android.util.Log.d(TAG, String.format("FixedSizeSink.count=%d,limit=%d"
",size=%d,segment=%d",
byteCount, mLimitSize, mBuffer.size(), BUFFER_SIZE));
long totalBytesRead = 0;
long readCount;
while (totalBytesRead < count && (readCount = source.read(mBuffer, BUFFER_SIZE)) != -1) {totalBytesRead = readCount;}
return this;
}
@Override
public int write(ByteBuffer src) throws IOException {final int available = mLimitSize - (int) mBuffer.size();
if (available < src.remaining()) {byte[] bytes = new byte[available];
src.get(bytes);
this.write(bytes);
return bytes.length;
} else {return mBuffer.write(src);
}
}
@Override
public void write(@NotNull Buffer source, long byteCount) throws IOException {mBuffer.write(source, Math.min(byteCount, mLimitSize - mBuffer.size()));
}
@Override
public BufferedSink writeUtf8(@NotNull String string) throws IOException {mBuffer.writeUtf8(string);
return this;
}
@Override
public BufferedSink writeUtf8(@NotNull String string, int beginIndex, int endIndex)
throws IOException {mBuffer.writeUtf8(string, beginIndex, endIndex);
return this;
}
@Override
public BufferedSink writeUtf8CodePoint(int codePoint) throws IOException {mBuffer.writeUtf8CodePoint(codePoint);
return this;
}
@Override
public BufferedSink writeString(@NotNull String string,
@NotNull Charset charset) throws IOException {mBuffer.writeString(string, charset);
return this;
}
@Override
public BufferedSink writeString(@NotNull String string, int beginIndex, int endIndex,
@NotNull Charset charset) throws IOException {mBuffer.writeString(string, beginIndex, endIndex, charset);
return this;
}
@Override
public BufferedSink writeByte(int b) throws IOException {mBuffer.writeByte(b);
return this;
}
@Override
public BufferedSink writeShort(int s) throws IOException {mBuffer.writeShort(s);
return this;
}
@Override
public BufferedSink writeShortLe(int s) throws IOException {mBuffer.writeShortLe(s);
return this;
}
@Override
public BufferedSink writeInt(int i) throws IOException {mBuffer.writeInt(i);
return this;
}
@Override
public BufferedSink writeIntLe(int i) throws IOException {mBuffer.writeIntLe(i);
return this;
}
@Override
public BufferedSink writeLong(long v) throws IOException {mBuffer.writeLong(v);
return this;
}
@Override
public BufferedSink writeLongLe(long v) throws IOException {mBuffer.writeLongLe(v);
return this;
}
@Override
public BufferedSink writeDecimalLong(long v) throws IOException {mBuffer.writeDecimalLong(v);
return this;
}
@Override
public BufferedSink writeHexadecimalUnsignedLong(long v) throws IOException {mBuffer.writeHexadecimalUnsignedLong(v);
return this;
}
@Override
public void flush() throws IOException {mBuffer.flush();
}
@Override
public BufferedSink emit() throws IOException {mBuffer.emit();
return this;
}
@Override
public BufferedSink emitCompleteSegments() throws IOException {mBuffer.emitCompleteSegments();
return this;
}
@Override
public OutputStream outputStream() {return mBuffer.outputStream();
}
@Override
public boolean isOpen() {return mBuffer.isOpen();
}
@Override
public Timeout timeout() {return mBuffer.timeout();
}
@Override
public void close() throws IOException {mBuffer.close();
}
}