Okio入门系列三,图片操作和套接字通信

382 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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. 您还可以为所有套接字操作配置超时。您不需要引用套接字来调整超时:SourceSink直接公开超时。即使流被修饰,此 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..1270..255int u = s & 0xff;
短的-32,768..32,7670..65,535int u = s & 0xffff;
整数-2,147,483,648..2,147,483,6470..4,294,967,295long u = s & 0xffffffffL;

Java 没有可以表示无符号长整数的原始类型。