一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
BMP文件操作
BMP文件编码演示 BMP file format.
void encode(Bitmap bitmap, BufferedSink sink) throws IOException {
int height = bitmap.height();
int width = bitmap.width();
int bytesPerPixel = 3;
int rowByteCountWithoutPadding = (bytesPerPixel * width);
int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4;
int pixelDataSize = rowByteCount * height;
int bmpHeaderSize = 14;
int dibHeaderSize = 40;
// BMP Header
sink.writeUtf8("BM");
// ID.
sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize);
// File size.
sink.writeShortLe(0);
// Unused.
sink.writeShortLe(0);
// Unused.
sink.writeIntLe(bmpHeaderSize + dibHeaderSize);
// Offset of pixel data.
// DIB Header
sink.writeIntLe(dibHeaderSize);
sink.writeIntLe(width);
sink.writeIntLe(height);
sink.writeShortLe(1);
// Color plane count.
sink.writeShortLe(bytesPerPixel * byte.SIZE);
sink.writeIntLe(0);
// No compression.
sink.writeIntLe(16);
// Size of bitmap data including padding.
sink.writeIntLe(2835);
// Horizontal print resolution in pixels/meter. (72 dpi).
sink.writeIntLe(2835);
// Vertical print resolution in pixels/meter. (72 dpi).
sink.writeIntLe(0);
// Palette color count.
sink.writeIntLe(0);
// 0 important colors.
// Pixel data.
for (int y = height - 1; y >= 0; y--) {
for (int x = 0; x < width; x++) {
sink.writebyte(bitmap.blue(x, y));
sink.writebyte(bitmap.green(x, y));
sink.writebyte(bitmap.red(x, y));
}
// Padding for 4-byte alignment.
for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) {
sink.writebyte(0);
}
}
}
该程序最棘手的部分是格式所需的填充。BMP 格式要求每一行从 4 字节边界开始,因此需要添加零以保持对齐。
编码其他二进制格式通常非常相似。一些技巧:
- 用黄金值编写测试!确认您的程序发出预期的结果可以使调试更容易。
- 用于
Utf8.size()计算编码字符串的字节数。这对于以长度为前缀的格式至关重要。 - 使用
Float.floatToIntBits()和Double.doubleToLongBits()对浮点值进行编码。
在套接字上通信
请注意,Okio 尚不支持 Kotlin/Native 或 Kotlin/JS 上的套接字。
通过网络发送和接收数据有点像写入和读取文件。我们 BufferedSink用来编码输出和BufferedSource解码输入。与文件一样,网络协议可以是文本、二进制或两者的混合。但网络和文件系统之间也存在一些实质性差异。
使用文件,您可以读取或写入,但使用网络,您可以同时进行这两种操作!一些协议轮流处理这个问题:写一个请求,读一个响应,重复。您可以使用单个线程来实现这种协议。在其他协议中,您可以同时读取和写入。通常,您需要一个专用线程来阅读。对于编写,您可以使用专用线程或使用synchronized多个线程可以共享一个接收器。Okio 的流对于并发使用是不安全的。
接收器缓冲出站数据以最小化 I/O 操作。这很有效,但这意味着您必须手动调用flush()才能传输数据。通常,面向消息的协议在每条消息之后刷新。请注意,当缓冲数据超过某个阈值时,Okio 将自动刷新。这是为了节省内存,您不应该依赖它来进行交互式协议。
Okiojava.io.Socket以连接为基础。将您的套接字创建为服务器或客户端,然后用于Okio.source(Socket)读取和Okio.sink(Socket)写入。这些 API 也适用于 SSLSocket. 除非您有充分的理由不使用 SSL,否则您应该使用 SSL!
通过调用从任何线程取消套接字Socket.close();这将导致其源和接收器立即失败,并带有IOException. 您还可以为所有套接字操作配置超时。您不需要引用套接字来调整超时:Source并Sink直接公开超时。即使流被修饰,此 API 也可以工作。
作为使用 Okio 联网的完整示例,我们编写了一个基本的 SOCKS 代理 服务器。一些亮点:
Socket fromSocket = ...
BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));
BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));
为套接字创建源和接收器与为文件创建它们相同。一旦为套接字创建了 Sourceor Sink,就不能分别使用它的InputStreamor OutputStream。
Buffer buffer = new Buffer();
for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) {
sink.write(buffer, byteCount);
sink.flush();
}
上面的循环将数据从源复制到接收器,每次读取后刷新。如果我们不需要刷新,我们可以用一个调用来替换这个循环BufferedSink.writeAll(Source)。
to的8192参数read()是返回前要读取的最大字节数。我们可以在这里传递任何值,但我们喜欢 8 KiB,因为这是 Okio 在单个系统调用中可以做的最大值。大多数时候应用程序代码不需要处理这样的限制!
nt addressType = fromSource.readByte() & 0xff;
int port = fromSource.readShort() & 0xffff;
Okio 使用像byteand这样的有符号类型short,但协议通常需要无符号值。位运算&符是 Java 将有符号值转换为无符号值的首选习惯用法。这是字节、短裤和整数的备忘单:
| 类型 | 有符号范围 | 无符号范围 | 已签名到未签名 |
|---|---|---|---|
| 字节 | -128..127 | 0..255 | int u = s & 0xff; |
| 短的 | -32,768..32,767 | 0..65,535 | int u = s & 0xffff; |
| 整数 | -2,147,483,648..2,147,483,647 | 0..4,294,967,295 | long u = s & 0xffffffffL; |
Java 没有可以表示无符号长整数的原始类型。