网络编程包 - 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

2492 次点击
所在节点    推广
54 条回复
zifangsky
2021-04-20 10:36:31 +08:00
看起来不错,后面有机会可以试用一下
guyeu
2021-04-20 10:46:35 +08:00
1. 推广就发推广;
2. 看来看去看不出来比 netty 高在哪儿
Joker123456789
2021-04-20 10:52:56 +08:00
@guyeu

1. 推广当然要发在人多的地方,推广节点有人看吗? 你会去一个全是广告的节点翻帖子??? 如果你有意见可以举报一波,让管理员来删帖。轮不到你喷我。
2. 我好像全文都没跟 netty 对比吧?你突然冒出一句 看不出来比 netty 高级在哪,我很懵逼啊。
Joker123456789
2021-04-20 10:54:19 +08:00
@guyeu 而且,这个节点是 java,我发的也是 java 项目,而且是开源非商业, 我不觉得有啥大问题。 如果真的冒犯到了这里的规矩,我接受管理员的删帖 甚至封号。
GuuJiang
2021-04-20 12:09:27 +08:00
折腾的精神可嘉,但是。。。容我给估计还沉浸在造完轮子的喜悦中的你泼点冷水,相信我,再过几年,你会希望自己没有留下过这段黑历史的
首先,你所谓的 Selector 的“缺点”,证明你并没有真正理解 Selector 的意义,并不是说 configureBlocking(false)就可以称为 NIO 了,恰恰相反,Selector 才是 NIO 的灵魂,让我们来看下你是怎么做的吧,用一个每 100ms 执行的定时任务,循环对每一个 channel 进行 read,这点实在是槽点太多无从吐起,建议你自己先去看看 select 、epoll 、iocp 等的基本原理,如果看完还有疑问的话欢迎回来讨论
至于“如果业务比较耗时,会造成整个循环被堵住”,这个跟是否用 Selector 根本没关系,而是使用者自己应该保证不在 IO 线程里处理耗时业务,再看看你是怎么解决这个所谓的“问题”的,我猜这个 BlockingQueue 应该是你最引以为傲的一部分了吧,恭喜你,你重新发明了线程池……的雏形版
总的来说,这个东西作为一个关于 NIO 以及线程池的概念验证的课后实验还是可以的,但是实际应用的话,价值为零
D3EP
2021-04-20 12:14:01 +08:00
同意楼上。看上去解决了一个不存在的问题。
learningman
2021-04-20 12:15:01 +08:00
@livid 推广
learningman
2021-04-20 12:15:39 +08:00
Http, WebSocket, UDP
这三个玩意儿是应该并列的吗?
Livid
2021-04-20 12:26:51 +08:00
@learningman 谢谢。这个主题已经已经被移动。

@Joker123456789 请阅读 V2EX 的节点使用说明:

https://www.v2ex.com/help/node
Joker123456789
2021-04-20 12:51:02 +08:00
@GuuJiang

100 毫秒的 那个你是不是看错了? 那个是 websocket 。 连接上了以后 需要实时监控 channel 里有没有消息。
而且这个一看就知道 不是 http 了啊,跟我文章是描述的原理完全不搭噶,你是怎么理解到一起的?

建议你重新看一下 http 的实现部分。

还有这句: [至于“如果业务比较耗时,会造成整个循环被堵住”,这个跟是否用 Selector 根本没关系,而是使用者自己应该保证不在 IO 线程里处理耗时业务]

你可以试一下 在 select 的 while 里开线程试试,你去试一下就知道了。

第三线程池确实是这个原理,但是这有什么问题?
Joker123456789
2021-04-20 12:51:40 +08:00
@learningman

来,说出你的观点。 不要直接一句反问,因为我不懂你的意思。
Joker123456789
2021-04-20 12:58:01 +08:00
@GuuJiang
哦对了,还有一点,我分享原理 是想让大家对这个东西了解多一些,从而决定自己要不要使用。并不是拿来炫耀的。

你所谓的没价值,只是你认为,有没有价值取决于 每个人的 实际需求。

最后呢,建议你看代码的时候仔细一点,你点的那个 package 明明就是 websocket 而不是 http,这都没看出来吗?
Joker123456789
2021-04-20 13:06:38 +08:00
@GuuJiang selector 确实是精髓,但是精髓 就在于 并发高了不会造成线程太多啊,因为他是用一个线程在消费 selectionkey,我不是在文章里说过了吗?

如果你觉得可以在 while 里开线程,那你就去试试,试完了再说话。

这句代码会把当前的连接 放到 selectionkeys 里
```java
int select = selector.select();
```

而 http 需要将连接保持到响应结束,如果你开线程去做别的事,这件事做完之前 连接是不会关闭的,会导致这里将 channel 再一次的取出来,造成重复消费。不信可以自己尝试。

不过话说回来,while 里开线程,和我这个模型有啥区别呢? 不都是把 channel 丢给线程去处理,有区别吗?
GuuJiang
2021-04-20 13:11:27 +08:00
@Joker123456789
这个包结构本身就是另一个槽点了,证明了你对 tcp/http/websocket 三者关系的理解都是混乱的,这个暂且不表
你只需要回答一个问题,“在一个循环里依次对一堆 channel 进行 read”这个做法,相比起被你否定掉的 NIO,优势在哪里?
Joker123456789
2021-04-20 13:15:11 +08:00
@GuuJiang 优势在于可以控制线程数量啊。 使用者可以自己配置 要几个线程来进行消费。 而不是只有一个 while 在那消费 selectionskey 。

还有,http,websocket 都是给予 tcp 的吧,websocket 甚至需要先发一个 http 来建立连接,所以 http 和 websocket 都放在 tcp 的包里 没什么问题吧?

upd 是另一种协议,我也是放到另一个单独的包里了。而且 udp 我就是用的 selector,因为他不需要保持连接到响应结束,因为他不需要响应,所以我可以在数据读完以后 再开线程去消费,这样就不存在重复操作 channel 的问题了。
GuuJiang
2021-04-20 13:19:19 +08:00
你开心就好
Joker123456789
2021-04-20 13:25:41 +08:00
@GuuJiang 找了个 槽点出来,吐槽了一番,后来发现自己看错了代码, 然后 就吐槽我对协议的理解有问题。

当我解释了优势 以及协议的关系后, 来一句“你开心就好”。

哎~,真的是。。。一言难尽。
Joker123456789
2021-04-20 13:36:11 +08:00
@Livid 如果真的要按规矩来,这个帖子应该在 分享创造 节点。 谢谢。
D3EP
2021-04-20 14:27:29 +08:00
一般异步非阻塞的网络编程框架起码能 C10K 。但当前框架一个线程同时只能处理一个 TCP 连接,而且在没有数据时一直空转,非阻塞 IO 不和多路复用一起使用,CPU 打满的几率非常高。设计上就存在问题,只能说自娱自乐了...
D3EP
2021-04-20 14:32:27 +08:00
此外,IO 线程和任务处理线程不做隔离,那更容易出问题了。可以参考 Tomcat 、Netty 的设计。

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

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

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

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

© 2021 V2EX