P2PServer设计:可靠udp和并发请求处理

大纲

P2PServer是我们设计的两大核心类之一。 它要能以高效率接收、发送udp请求,并且要能处理可靠和不可靠两种udp请求模式。为了方便使用,它还要易于扩展、自定义,所以我们模仿了asp .net core的设计,并且给它加入了构造函数依赖注入功能。
源代码

设计细节

细节设计内容

处理类注册

为了让使用者可以高度自定义对不同udp请求的处理方法,我们设计了规范的请求接口。并且采用了注册处理类的模式。用户只需要自己写一个处理类,并且注册进来,请求到来的时候就会自动调用处理类里的函数。
处理类就和普通的类一样,设计上对应asp .net core中的Controller,其中的处理函数必须带上HandlerAttribute,这个Attribute需要接受一个CallMethod参数,该参数决定了它会用来处理哪样的请求。 处理方法同时需要接收一个UdpContext类型的参数,这里边包含了书记请求的数据。处理类的构造函数可以包含参数,这些参数每次请求到来的时候会自动从依赖注入容器中获得。
注册处理类使用AddHandler<T>方法。

底层

注册处理类的时候,我们会通过反射获取它的元数据信息并且保存在字典中。之后每次请求到来的时候使用元数据构造这个类,并且调用相应的处理方法。
所以不宜在Handler的构造函数中加入大运算量的操作。如果必须用到,请考虑通过依赖注入的方式来传入运算结果。

依赖注入

依赖注入和asp .net core的依赖注入基本一样,用ConfigureServices方法来配置。P2PServer默认会将自身注册入容器。

Udp接收

接收的时候,使用了一个死循环,用ReceiveAsync方法不断地接收他人的消息。接收到之后,会使用线程池里的线程来进行后续处理,接收线程会直接心如下一个循环。也就是说多个处理函数可能会并发地执行。所以如果大消息分片,需要考虑处理函数的线程安全问题。

可靠udp

可靠的udp信息会携带一个ReqId参数,一个可靠udp的传输过程如下:

  1. 发送方:

    1. 设定重传初始次数为0,生成一个ReqId

    2. ReqId带在发送数据里发送,等待ACK

    3. 若一定时间内收到ACK,则结束发送的Task<bool>,返回true

    4. 若超过设定的最大等待时间,则重传次数++

    5. 若重传次数大于最大重传次数,则结束发送的Task<bool>,返回false。否则回到1.2

  2. 接收方:

    1. 初始化一个字典,键是Guid类型,值是DateTime类型

    2. 等待接收发送方的信息

    3. 接收到信息

    4. 删除字典中所有值记录的时间早于现在10秒的键值对

    5. 若带有ReqId参数,返回ACK信息,带上ReqId

    6. 检查键为ReqId的信息是否在字典中

    7. 若不存在,则将键值对ReqId:DateTime存入字典,调用对应的Handler。

    8. 若存在,则不进行任何操作

    9. 返回第二步

Udp发送

需要注意,UdpClient类中的方法并不是线程安全的。它能够同时接收且发送,但是不能同时在多个线程中使用发送或者接收。
所以我设计了一个发送队列,使用一个MsgQueue实现了死循环的异步遍历。最大限度地增加发送速度而保证了线程安全。
所以,实际上发送信息的操作只是将需要发送的消息enqueue。
关于MsgQueue的细节参考此处


本文章使用limfx的vscode插件快速发布