孩子对象与父母的沟通模式

发布于 2024-11-29 05:03:58 字数 1267 浏览 0 评论 0原文

我实际上正在用 C# 开发一个多线程游戏服务器。我正在尝试将其创建为可单元测试的。

到目前为止,我开始使用的模式类似于这样:

实例化 Server 类:

  • 用于启动和关闭服务器

OnStart 服务器实例化 ConnnectionManager

  • 包含ConnectionPool 类的实例
  • 用于接受异步连接(通过 TCPListener.BeginAcceptSocket)
  • 当接受套接字时 ClientConnection 的实例创建并添加到 ConnectionsPool 中的列表中

ConnectionsPool 类:

  • 负责向池中添加和删除活动连接,作为列表进行管理

ClientConnection strong> :

  • 负责通过 Socket.BeginReceive 接收数据
  • 当接收到所有数据时,将创建 SocketHandler 的实例(基本上解析套接字内容并运行操作)

架构应如下所示:
服务器->连接管理器->连接池 ->客户端连接-> SocketHandler

问题
当我在 SocketHandler 内部时,我如何影响其他玩家? (例如,玩家 A 击中玩家 B:我需要在 ConnectionsPool 中获取玩家 B 的实例并更新他的 HP 属性),甚至更新服务器本身(比如调用 Server 类上的 shutdown 方法)

我假设有 3 choice :

  • 将所有重要的类(Server + ConnectionsPool)转换为 静态类>缺点:无法进行单元测试
  • 将所有重要的类转换为 Singleton 类 >缺点: 难以测试
  • 在子构造函数中注入重要类的实例> 缺点:由于传递了大量信息,可读性较差。

这里的目标是使用最佳实践使所有单元都可测试,同时保持简单的方法。

我收到了向 childs 注入委托的建议,这与第三种方法类似,但我不确定是否对此进行单元测试以及真正的效果,因为当然一切都是多线程的。

这里最好的架构选择是什么?

I am actually developping a multithreaded game server in C#. I am trying to create it as unit-testable as possible.

As of now, the pattern I started using resemble to something like this :

A Server class is instanciated:

  • Serves to start and shutdown the server

OnStart the Server instanciate the ConnnectionManager :

  • Contains an instance of the class ConnectionPool
  • Serves to accept asynchronously connections (via TCPListener.BeginAcceptSocket)
  • When a socket is accepted an instance of ClientConnection is created ans added to the list in ConnectionsPool

The class ConnectionsPool :

  • Responsible to add and remove active connections to the pool, managed as a list

The class ClientConnection :

  • Responsible to receive the data via Socket.BeginReceive
  • When all is received, an instance of SocketHandler is created (basically parse the socket content and run actions)

The schema should look like this :
Server -> ConnectionManager -> ConnectionsPool -> ClientConnection -> SocketHandler

The problem :
When I am inside the SocketHandler, how can I affect another player ? (for example, player A hits player B: I need to get the instance of the player B inside ConnectionsPool and update his HP property) or even update the server itself (say call the shutdown method on the Server class)

I assume to have 3 choices :

  • Transform all important classes (Server + ConnectionsPool) into
    static classes > drawback: can't be unit tested
  • Transform all important classes into Singleton classes > drawback :
    difficult to test
  • Inject inside child constructor the instances of important classes >
    drawback : less readable code since a lot of information is passed

The goal here is to make this all unit testable using best practices while keeping a simple approach.

I received a suggestion to inject a delegate to childs which is similar to the 3rd method, but I am unsure about unit testing this and the real effect since everything is multithreaded of course.

What would be the best architecture choice here ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

雨的味道风的声音 2024-12-06 05:03:58

我发现的主题是网络代码并不是真正可进行单元测试的,并且应该与您希望测试的代码(例如 Player 对象)隔离。对我来说,您似乎将 Player 与 ClientConnection 紧密耦合,这可能会使其难以测试。我还认为将玩家与他的连接耦合可能违反了 SRP,因为他们的职责非常不同。

我编写了一个看起来与您希望实现的系统类似的系统,并且我通过仅通过委托和接口将玩家链接到连接来实现这一点。我有一个单独的 World 和 Server 类库,以及第三个项目 Application,它引用这两个项目,并创建 Server 和 World 的实例。我的 Player 对象位于世界中,连接位于服务器中 - 这两个项目不互相引用,因此您可以考虑 Player,而无需任何网络条款。

我使用的方法是将SocketHandler抽象化,并在Application项目中实现一个具体的处理程序。处理程序具有对世界对象的引用,可以在创建时将其传递给它。 (我通过工厂类来实现这一点 - 例如,ClientConnectionFactory(在应用程序中),其中服务器只知道抽象连接和工厂。)

public class ClientConnectionFactory : IConnectionFactory
{
    private readonly World world;

    public ClientConnectionFactory(World world) {
        this.world = world;
    }

    Connection IConnectionFactory.Create(Socket socket) {
        return new ClientConnection(socket, world);
    }
}

然后,ClientConnection 可以将 World 引用转发给它的处理程序,以及处理程序所需的任何信息Connection 本身,尤其是 Send 方法。这里我只是将 Connection 对象本身传递给 Handler。

public ClientConnection(Socket socket, World world)
    : base(socket) {
    this.handler = new ClientHandler(this, world);
    ...
}

游戏逻辑和网络之间的大部分协调都包含在 ClientHandler 中,也许还包含它引用的任何其他姐妹对象。

class ClientHandler : SocketHandler
{
    private readonly Connection connection;
    private readonly World world;

    public ClientHandler(Connection connection, World world) {
        this.connection = connection;
        this.world = world;
        ...
    }

    //overriden method from SocketHandler
    public override void HandleMessage(Byte[] data) {
        ...
    }
}

剩下的部分涉及 ClientHandler 创建它的 Player 对象,分配委托或接口来更新操作,然后将玩家添加到 World 中的玩家池中。最初,我使用 Player 中的事件列表来完成此操作,我使用 ClientHandler 中的方法(它知道 Connection)来订阅这些事件,但事实证明 Player 对象中有数十个事件,这使得维护变得一团糟。

我选择的选项是在播放器的世界项目中使用抽象通知程序。例如,对于运动,我将在 Player 中拥有一个 IMovementNotifier 对象。在应用程序中,我将创建一个 ClientNotifier,它实现此接口并向客户端发送相关数据。 ClientHandler 将创建 ClientNotifier 并将其传递给它的 Player 对象。

class ClientNotifier : IMovementNotifier //, ..., + bunch of other notifiers
{
    private readonly Connection connection;

    public ClientHandler(Connection connection) {
        this.connection = connection;
    }

    void IMovementNotifier.Move(Player player, Location destination) {
        ...
        connection.Send(new MoveMessage(...));
    }
}

可以更改 ClientHandler 构造函数来实例化此通知程序。

public ClientHandler(Connection connection, World world) {
    this.connection = connection;
    this.world = world;
    ...
    var notifier = new ClientNotifier(this);
    this.player = new Player(notifier);
    this.world.Players.Add(player);
}

因此,最终的系统是 ClientHandler 负责所有传入消息和事件,而 ClientNotifier 处理所有传出内容。这两个类很难测试,因为它们包含很多其他垃圾。网络无论如何都无法真正进行单元测试,但是这两个类中的 Connection 对象可以被模拟。 World 库完全可以进行单元测试,无需考虑网络组件,这正是我在设计中想要实现的目标。

这是一个很大的系统,我在这里没有介绍太多,但希望这能给你一些提示。如果您想了解更多具体信息,请询问我。

如果我能提供更多建议,那就是避免任何静态或单身人士 - 他们会回来咬你。如果您需要作为替代方案,请支持增加复杂性,但要在需要的地方进行记录。我的系统对于维护人员来说可能看起来很复杂,但有详细的文档记录。对于用户来说,它使用起来特别简单。主要本质上是

var world = new World();
var connectionFactory = new ClientConnectionFactory(world);
var server = new Server(settings.LocalIP, settings.LocalPort, connectionFactory);

The theme I find is that networking code is not really unit testable, and should be isolated from the code you do wish to test (the Player object, for example). To me it seems you have tightly coupled a Player to a ClientConnection, which might make it less testable. I also think that coupling a Player with his connection is probably violating the SRP, since they have very distinct responsibilities.

I've written a system which seems similar to what you wish to acheive, and I done so by linking a player to a connection only via delegates and interfaces. I have a separate World and Server class library, and a third project, Application, which references the two, and creates an instance of Server and World. My Player object is in the World, and a Connection in the Server - these two projects do not reference one another, so you can consider Player without any terms of networking.

The method I use is to make SocketHandler abstract, and implement a concrete handler in the Application project. The handler has a reference to the world object, which can be passed to it on creation. (I do so by means of factory classes - eg, ClientConnectionFactory (in Application), where the Server knows only of abstract Connections and Factories.)

public class ClientConnectionFactory : IConnectionFactory
{
    private readonly World world;

    public ClientConnectionFactory(World world) {
        this.world = world;
    }

    Connection IConnectionFactory.Create(Socket socket) {
        return new ClientConnection(socket, world);
    }
}

The ClientConnection can then forward the World reference to it's handler, along with any information the handler requires about the Connection itself, such as the Send methods in particular. Here I just pass the Connection object itself to the Handler.

public ClientConnection(Socket socket, World world)
    : base(socket) {
    this.handler = new ClientHandler(this, world);
    ...
}

Most of the coordination between the game logic and the networking is then contained inside the ClientHandler, and perhaps any other sister objects it references.

class ClientHandler : SocketHandler
{
    private readonly Connection connection;
    private readonly World world;

    public ClientHandler(Connection connection, World world) {
        this.connection = connection;
        this.world = world;
        ...
    }

    //overriden method from SocketHandler
    public override void HandleMessage(Byte[] data) {
        ...
    }
}

The rest involves the ClientHandler creating it's Player object, assigning delegates or interfaces to update actions, and then adding the player to the pool of players in World. Initially I done this with a list of events in Player, which I subscribe to with methods in the ClientHandler (which knows of the Connection), but it turned out that there were dozens of events in the Player object which became a mess to maintain.

The option I went for was to use abstract notifiers in the World project on the player. For example, for movement I would have an IMovementNotifier object in Player. In Application, I would create a ClientNotifier which implements this interface and does the relevant sending of data to the client. The ClientHandler would create the ClientNotifier and pass it to it's Player object.

class ClientNotifier : IMovementNotifier //, ..., + bunch of other notifiers
{
    private readonly Connection connection;

    public ClientHandler(Connection connection) {
        this.connection = connection;
    }

    void IMovementNotifier.Move(Player player, Location destination) {
        ...
        connection.Send(new MoveMessage(...));
    }
}

The ClientHandler ctor can be changed to instantiate this notifier.

public ClientHandler(Connection connection, World world) {
    this.connection = connection;
    this.world = world;
    ...
    var notifier = new ClientNotifier(this);
    this.player = new Player(notifier);
    this.world.Players.Add(player);
}

So the resulting system is one where ClientHandler is responsible for all incoming messages and events, and ClientNotifier handles all outgoing stuff. These two classes are quite difficult to test as they contain a lot of other crap. Networking can't really be unit tested anyway, but the Connection object in these two classes can be mocked. The World library is entirely unit testable without any consideration of networking components, which is what I wanted to acheive in my design anyway.

It's a big system, and I've not covered much of it here, but hope this can give you some hints. Ask me if you'd like any more specific information on it.

If I could give any more advice, it would be to avoid any static or singletons - they'll come back to bite you. Favour increasing complexity if you need to as an alternative, but document where you need to. The system I have might seem complex to a maintainer, but is well documented. To a user it's particularly simple to use. Main is essentially

var world = new World();
var connectionFactory = new ClientConnectionFactory(world);
var server = new Server(settings.LocalIP, settings.LocalPort, connectionFactory);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文