如何编写一个 SendFile 服务器

如何编写一个 SendFile 服务器

前言

以前讨论零拷贝的时候,咱们知道,两台机器之间传输文件,最快的方式就是 send file,众所周知,在 Java 中,该技术对应的则是 FileChannel 类的 transferTo 和 transferFrom 方法。react

在平时使用服务器的时候,好比 nginx ,tomcat ,都有 send file 的选项,利用此技术,可大大提升文件传输效能。nginx

另外,可能也有人谈论 send file 的缺点,例如不能利用 gzip 压缩,不能加密。这里本文不作探讨。git

纸上得来终觉浅,绝知此事要躬行。github

那么,如何使用这两个 api 实现一个 send file 服务器和客户端呢?api

想象一下,你写的 send file 服务器利用 send file 技术,利用万兆网卡,从各个 client 端 copy 海量文件,瞬间打爆你那 1TB 的磁盘和 48核的 CPU。而且,注意:只需很小的 JVM 内存就能够实现这样一台强悍的服务器。为何?若是你知道 send file 的原理,就会知道,使用 send file 技术时, 在用户态中,是不须要多少内存的,数据都在内核态。数组

是否是颇有成就感?什么?没有?那打扰了 🤣。缓存

另外,关于 send file,咱们都知道,因为是直接从内核缓冲区进入到网卡驱动,咱们几乎能够称之为 “零拷贝”,他的性能十分强劲。tomcat

可是。安全

除了这个,还有其余的吗?答案是有的,send file 利用 DMA 的方式 copy 数据,而不是利用 CPU。注意,不利用 CPU 意味着什么?意味着数据不会进入“缓存行”,进一步,不会进入缓存行,表明着缓存行不会由于这个被污染,再进一步,就是不须要维护缓存一致性。性能优化

还记得咱们由于这个特性搞的那些关于 “伪共享” 的各类黑科技吗?是否是又学到了一点呢?😎

理念

做为一个纯粹的,高尚的,有趣的 sendFile 服务器或者客户端,使用场景是嵌入到某个服务中,或者某个中间件中,不须要搞成夸张的容器。咱们能够借鉴一下,客户端能够作成 Jedis 那样的,若是你想搞个链接池也不是不能够,但 client 自身实例,仍是单链接的。服务端能够作成 sun 的 httpServer 那种轻量的,随时启动,随时关闭。

同时, 支持 oneway 的高性能发送,由于,只要机器不宕机,发送到网卡就意味着发送成功,这样能大幅提升发送速度,减小客户端阻塞时间。

另外,也支持带有 ack 的稳定发送,即只有返回 ack 了,才能确认数据已经写到目标服务器磁盘了。

server 端支持海量链接,必须得是 reactor 网络模型,但咱们不想在这么小的组件里用 netty,过重了,还容易和使用方有 jar 冲突。因此,咱们能够利用 Java 的 selector + nio 本身实现 Reactor 模型。

设计

IO 模型设计

设计图:

如上图,Server 端支持海量客户端链接。

server 端含有 多个处理器,其中包括 accept 处理器,read 处理器 group, write 处理器 group。

accept 处理器将 serverSocketChannel 做为 key 注册到一个单独的 selector 上。专门用于监听 accept 事件。相似 netty 的 boss 线程。

当 accept 处理器成功链接了一个 socket 时,会随机将其交给一个 readProcessor(netty worker 线程?) 处理器,readProcessor 又会将其注册到 readSelector 上,当发生 read 事件时,readProcessor 将接受数据。

能够看到,readProcessor 能够认为是一个多路复用的线程,利用 selector 的能力,他高效的管理着多个 socket。

readProcessor 在读到数据后,会将其写入到磁盘中(DMA 的方式,性能炸裂)。

而后,若是 client 在 RPC 协议中声明“须要回复(id 不为 -1)” 时,那就将结果发送到 Reply Queue 中,反之没必要。

当结果发送到 Reply Queue 后,writer 组中的 写线程,则会从 Queue 中拉取回复包,而后将结果按照 RPC 协议,写回到 client socket 中。

client socket 也会监听着 read 事件,注意:client 是不须要 select 的,由于不必,selector 只是性能优化的一种方式——即一个线程管理海量链接,若是没有 select, 应用层没法用较低的成本处理海量链接,注意,不是不能处理,只是不能高效处理。

回过来,当 client socket 获得 server 的数据包,会进行解码反序列化,并唤醒阻塞在客户端的线程。从而完成一次调用。

线程模型

设计图:

image-20191029093524267

如上图所示。

在 client 端:

每一个 Client 实例,维护一个 TCP 链接。该 Client 的写入方法是线程安全的。

当用户并发写入时,可并发写的同时并发回复,由于写和回复是异步的(此时可能会出现,线程 A 先 send ,线程 B 后 send,但因为网络延迟,B 先返回)。

在 server 端:

server 端维护着一个 ServerSocketChannel 实例,该实例的做用就是接收 accep 事件,且由一个线程维护这个 accept selector 。

当有新的 client 链接事件时,accept selector 就将这个链接“交给“ read 线程(默认 server 有 4 个 read 线程)。

什么是“交给”?

注意:每一个 read 线程都维护着一个单独的 selector。 4 个 read 线程,就维护了 4 个 selector。

当 accept 获得新的客户端链接时,先从 4 个read 线程组里 get 一个线程,而后将这个 客户端链接 做为 key 注册到这个线程所对应的 read selector 上。从而将这个 Socket “交给” read 线程。

而这个 read 线程则使用这个 selector 轮询事件,若是 socket 可读,那么就进行读,读完以后,利用 DMA 写进磁盘。

RPC 协议

Server RPC 回复包协议

字段名称 字段长度(byte) 字段做用
magic_num 4 魔数校验,fast fail
version 1 rpc 协议版本
id 8 Request id, TCP 多路复用 id
length 8 rpc 实际消息内容的长度
Content length rpc 实际消息内容(JSON 序列化协议)

Client RPC 发送包协议

字段名称 字段长度(byte) 字段做用
magic_num 4 魔数校验,fast fail
id 8 Request id, TCP 多路复用 id, 默认 -1,表示不回复
nameContent 2 Request id, TCP 多路复用 id
bodyLength 8 rpc 实际消息内容的长度
nameContent bodyLength 文件名 UTF-8 数组

为何 发送包和返回包协议不一样?为了高效。

总结

注意:这是一个能用的,性能不错的,轻量的 SendFile 服务器实现,本地测试时, IO写盘达到 824MB/S,4c 4.2g inter i7 CPU 满载。

image-20191029120446781

代码地址:https://github.com/stateIs0/send_file

同时,欢迎你们 star, pr,issue。我来改进。

相关文章
相关标签/搜索