网络编程包 - Magician 的原理 与 使用

2021-04-20 09:41:21 +08:00
 Joker123456789

Magician 是一个异步非阻塞的网络编程包,用 java 语言实现,支持 Http, WebSocket, UDP 等协议

运行环境

jdk11+

简单的原理介绍

Magician 的底层使用的是 NIO,但是没有用 Selector,因为 Selector 的设计初衷就是为了用单线程来处理高并发,从而减少 因为连接太多而造成线程太多,占用过多的服务器资源。 但是这样做的坏处也很明显,就是无法充分利用 cpu 的多核性能,还有一个就是 如果业务比较耗时,会造成整个循环被堵住。

所以,考虑到这一点,Magician 决定使用 accept,代码如下:

while (true){
    SocketChannel channel = null;
    try {
        /* 这个方法会阻塞,直到有新连接进来 */
        channel = serverSocketChannel.accept();
        channel.configureBlocking(false);

        /* 将任务添加到队列里执行 */
        ParsingThreadManager.addTaskToParsingThread(channel);
    } catch (Exception e){
        logger.error("处理请求出现异常", e);
        ChannelUtil.close(channel);
    }
}

有一个 while 不断的监听 accept,当没有新请求进来的时候 accept 是阻塞的,所以不会有空轮询的问题,当有了新的请求进来,就会把 channel 丢到队列里面去,然后继续监听 accept 。

那么这个队列是什么结构的?他又是如何来执行的呢?

首先,队列的结构是这样的:他是一个 LinkedBlockingDeque,有序 且 长度无限(除非内存爆了),然后这个队列 放在了一个线程中, 线程开启后就会有一个 while 不断的从这个队列里 take 元素,如果队列为空,take 就会阻塞,队列一旦有数据 take 就会按顺序返回里面的元素。

所以,只要这个线程在运行,我们就可以不断的往队列里丢任务,让这个线程来慢慢消化。

如果这样的线程+队列有多个, 我们把收到的请求 通过轮询算法 分配到这些队列,让线程各自消化,是不是就可以实现一个,线程数量可控,同时又具有异步特性的模型?

这个模型就是 Magician 现在所使用的,如下图所示:

在使用 Magician 的时候,可以自己配置 需要几个线程来同时运行。

除了 http,udp 也是采用的这个模型。只不过 udp 是以同步的模式读数据,数据读完了 再丢到队列里 让队列去执行业务逻辑。

如何使用

说了这么多原理,我们接下来说说 如何使用 Magician 来开发各种服务。

首先我们看一下 http 的实现

Magician.createHttpServer().httpHandler("/", req -> {

                        req.getResponse()
                           .sendJson(200, "{'status':'ok'}");

                    }).bind(8080).start();

如果不想把 handler 跟这段代码窝在一起,可以单独建立 handler,在这里添加进去即可

WebSocket 实现

Magician.createHttpServer().bind(8080)
                    .httpHandler("/", new DemoHandler())
                    .webSocketHandler("/websocket", new DemoSocketHandler())
                    .start();

只需要在创建 http 服务的时候,添加一个 WebSocketHandler 即可。

UDP 实现

Magician.createUdpServer()
                .handler(outputStream -> {
                    // outputStream 是 ByteArrayOutputStream 类型的
                    // 它是客户端发过来的数据,自行解析即可
                }).bind(8088).start();

同样的,也可以单独创建 handler,在这里添加进去

了解更多

想了解更多的话,欢迎访问 Magician 官网:http://magician-io.com

2509 次点击
所在节点    推广
54 条回复
guyeu
2021-04-20 14:38:37 +08:00
@Joker123456789 #3 推广的事就不提了。其实上午我怀着相当大的期待阅读了你的部分源码,因为是把它当作 Netty 的替代去看待的,所以不自觉地把它和 Netty 做了一些对比,如果有冒犯的地方向你道歉。emmmmm,那就不提 Netty 说几个有可能提升你这个项目质量的点

1. 对协议的抽象不够,假如某个业务想把 http 换成 udp,看不出有什么平滑切换的可能性;
2. 和线程模型绑得太死,市面上鲜少有这种自带线程模型的网络库;
3. 没有性能测试数据,也无从得知你这个网络库的性能怎么样;
4. 代码风格值得优化,起码把 JavaDoc 按规范写了吧;
5. 缺乏面向对象设计,似乎从未考虑过一个服务的两个组件同时使用你这个网络库的任何可能性;
learningman
2021-04-20 14:58:14 +08:00
@Joker123456789 自己去看看 7 层网络模型,这三个分别应该在哪一层
Joker123456789
2021-04-20 15:50:03 +08:00
@D3EP

1. 队列的 take 方法 会自动检查队列是否为空,如果为空是不返回的 ,直接阻塞在那,所以不存在空转。

2. 多路复用,那是系统层面的,到了应用层还是一个线程在消费。Selector 确实是一个线程 可以处理多个 TCP 连接,但是他是在 while 里排队一个个处理的。 这跟我的模型区别不大吧? 我也是每一个线程 都在处理排着队的多个 tcp 。

3. 配置几个线程 就是几个线程同时跑,不会因为请求多了就线程暴增,怎么会打满 CPU ?

4. 你说的线程没有隔离 可否详细一点? 对于这一条,我是真的想虚心请教的。
Joker123456789
2021-04-20 15:50:51 +08:00
@learningman 我只是做了一个支持 这三个协议的包,你跟我扯 网络模型干嘛? 没东西喷了 就来吐槽我的目录结构吗?
Joker123456789
2021-04-20 15:55:14 +08:00
@guyeu

说实话,你这段话有点感动到我了。真的。

我会认真考虑你的建议,并在后面尽可能优化上。
Joker123456789
2021-04-20 16:12:06 +08:00
@guyeu 不过,第 5 点 是支持的, 监听两个端口即可。
huang119412
2021-04-20 17:17:16 +08:00
感觉这个线程模型很像 tomcat 的 nio,tomcat 的 nio 的 ServerSocketChannel 就是用的 accept 。netty boss 线程只处理连接,worker 处理其他事件应该算是经典了。netty 兼容性,稳定性,安全性,便捷性久经考验。性能早就不少 netty 强项。是个轻量级的 nio,aio 都能和 netty 性能媲美。但是生产中敢随便用吗?
blackboom
2021-04-20 17:28:24 +08:00
建议换个头像
GuuJiang
2021-04-20 19:04:35 +08:00
估计你今天情绪上很难接受,为此还专门另开一个贴抱怨下,以你现在对网络 IO 的认知,我很难跟你一一讲明白你的错误在哪里,但是明眼人都看得出来,真心建议你静下心来好好找点资料看看网络基础,各种多路复用方案到底在解决什么问题,等你看完了如果还是不服气,欢迎你回来对线
你可以先带着这个问题去看
“当客户端没有发数据时,你的程序在干什么,而其他正确实现了的网络框架,包括( BIO 、NIO 、netty 等)又在干什么”

到时候你就会明白,你这个东西称之为“一个异步非阻塞的网络编程包”,是多么的荒谬
Joker123456789
2021-04-20 21:41:44 +08:00
@GuuJiang 好的, 我会去看的。

然后回答一下你的问题, 当没有数据进来的时候,我这个程序是等待状态,accept 自动阻塞住了 不会返回任何东西 就停在那。 队列的 take 方法也阻塞住了,就停在那。 整个程序除了主线程挂在那,就没任何动作了。

然后我问你一个问题, 消费 selectionkey 的那个 while 是不是有被业务阻塞的可能? 你可能会说 在 while 里开线程 让业务线程去处理。 但问题是 在业务线程跑完之前 channel 是不能关的。 因为 http 客户端需要等待响应, 必须等业务跑完 并把响应写入 channel 才能关吧。 而 channel 不关的话,while 会进入下一次循环,select 方法会再次为这个连接生成 selectkey, 这样一来就出现重复处理了。 这个问题是我真实遇到的问题。 如果你有办法解决, 那你可以说出来。

多路复用器 是系统层面的, 到了应用层 就是单线程在一个个消费 selectkey, 如果这个你觉得不对也欢迎 指正。并说出具体哪里不对。

最后你可以去看一下 nio 的 Reator 的分发模型, 是不是跟我这个有点像。
Joker123456789
2021-04-20 22:02:28 +08:00
@GuuJiang 接着上一条回复,

我重新回答一下这个问题吧: [在一个循环里依次对一堆 channel 进行 read”这个做法,相比起被你否定掉的 NIO,优势在哪里?]

你可以把我的模型 理解成,可以用配置来 决定同时有几个循环在 以此消费 selectionkey 。 把单线程变成了 可以指定数量的多线程。

说白了,NIO 的 while 相当于单机, 而我这个相当于负载均衡。 这个理解吧?

虽然 没有彻底异步, 也不可能彻底异步,否则就会出现一个请求一个线程 带来上下午切换的问题。 但他确实 解决了一些问题吧?

当然了,如果 NIO 有自带的 api 来做这件事, 你可以说出来反驳我。。

我有情绪是针对喷子的, 对于你这种正确交流的人 我是很友好的,你可以放心说话。
GuuJiang
2021-04-20 22:09:46 +08:00
@Joker123456789
第一句话就错了,你再好好看看 tcp 里 accept 到底是在哪个阶段
至于你说的 select 重复处理的问题,在看到代码之前不好下结论,不过我猜十有八九是没有调 remove,因为这个是个很典型的错误
早期版本的 jdk 安装后有个 samples.zip ,里面就有一个用 nio 实现的 echo server 的例子,建议你多读几遍,而实际应用到真实项目中时,只需要把直接 reply 的那一行代码换成投递到工作线程池即可,这也是业界通用的做法
我在 16 楼的那句话可能刺伤了你,对此我道歉,但是等你弄明白所有这些问题之后相信你能体会当时的心情,换作你也会无话可说的
Joker123456789
2021-04-20 22:35:42 +08:00
@GuuJiang

remove 是有的, 不是 selectionkey 这个集合里 有重复数据, 而是再次执行 selector.select () 方法后 重新获取的 selectionkey 集合里 有正在被处理的 channel,

while ( true ){
selector.select ();

while (){
如果这里开了线程,内部这个 while 没问题, 但是等内部 while 结束后, 外部 while 开始下一次循环后, 获取到的那个新的 selectionkey 里会有正在处理的 channel



一开始我就是用的 选择器, 如果在 while 里开线程能解决问题,我还这么大费周章干嘛呢。

而且 UDP 我也是用的选择器, 因为不用响应,我可以同步读完后,用线程去处理 handler 。

accept 是在有连接进来的阶段。 选择器里如果发现了 这个状态的 key 不就会立刻注册成 op_read 状态吗? 代表可以读了

然后你也不用道歉,我那个帖子是怼那两个喷子的, 并不是针对你,对于正确讨论的人 我很少闹情绪。 今天白天 主要是你一上来看错了代码,我才那样的。
GuuJiang
2021-04-20 22:51:11 +08:00
“selector 里有正在处理的 channel”这个没有任何问题啊,你可能误解了 selector 的含义,每一次 select 操作返回的是处于就绪状态的 channel,所谓的就绪对于 read 操作来说就是有数据可读,那你读就行啊,内层 while 是错误的,你还停留在 bio 的思想,有数据可读不代表能完整地读到一个上层协议所需要的结构,你能读到多少读多少,至于什么时候得到上层所需要的完整数据这又是另一个话题了,暂时按下不表,所以一个 channel 多次出现在 select 的结果中是再正常不过的行为
你画个时序图,模拟下多个连接同时存在,然后以随机的间隔发送下一个数据片段,然后对比下在这种情况下你的程序和 nio 的区别
要知道,一个 channel 不是你想读就能读到数据的,而 select 的存在就是保证了你只在真正有数据可读时才去读
Joker123456789
2021-04-20 22:54:57 +08:00
@GuuJiang

accept 确实是阻塞住的, 对于防止空转 我还是很重视的, 自测过好几次。

因为 block 设置为 false 我是设置在 socketChannel 上的,

而我监听的是 serverSocketChannel 的 accept 方法 这个的 block 我设置的是 true 。
Joker123456789
2021-04-20 23:05:22 +08:00
@GuuJiang 问题是 已经有个业务线程在读写他了, 如果再读写一次直接抛异常, 而且也不该 被多个线程读写吧。

请求来了,业务线程工作了, 结果你又叫别的业务线程来处理这个 channel, 有点不合逻辑吧。

不过这个思路我可以研究一下。

然后,http 的读 我是根据 content-length 来判断读没读完的,所以一旦开始了 read 就会读到结束。 否则就会一直读。

websocket 也是根据长度判断的,但是同一个通道 会有很多数据进来,如果发送太快会发生粘包问题, 这个我也在想办法处理。
GuuJiang
2021-04-20 23:06:41 +08:00
accept 阻塞的是 connect 阶段,我说的是客户端 connect 后没有发数据,假设你总共有 n 个线程,现在有 n 个客户端连接上但是只发了一个完整 http 请求的一部分数据,这时候第 n+1 个以后的客户端是不是永远被卡住得不到处理了?你这个完全就是同时抛弃了 bio 和 nio 的优势,结合了二者的劣势,bio 虽说占用线程数多点吧,好歹还能继续处理后续的客户端,并且没数据可读时是在阻塞,而你这个本质上还是一个线程服务一个连接,并且没数据时也在空转,而且线程数还是有上限的,所以连最原始的 bio 都不如
Joker123456789
2021-04-20 23:19:11 +08:00
@GuuJiang 对, 如果数据没有发送确实是个问题。

我再来优化一下吧。 非常感谢。
bengol
2021-04-21 01:08:48 +08:00
说个题外话 bio/nio 这些东西都没啥意思 模型都很成熟了,聊来聊去就那些知识点

基于不太稳定的 io uring 实现 aio 以及附加的 syscall batch 可能还有点意思

配合网络智能卡或者 RDMA 做底层网络方面优化比较好玩

我们当前在追求一个完全的 zerocopy network
programming framework
guyeu
2021-04-21 10:17:01 +08:00
@Joker123456789 #26 然后两个组件的网络消息共用一个线程池。。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/771813

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX