2

我还在继续写一个简单的游戏服务器。由于这里的一条建议,我实现了 mvar 支持,希望它会在不包含至少 2 个玩家的情况下阻塞线程。但它不会等到我把任何数据放在那里。它总是返回睡眠 Lwt.t。首先,在这里我们接受连接并提供玩家进入START开始寻找合作伙伴:

let waiting_players = 
    Lwt_mvar.create_empty();;

let rec make_ready player = 
    player >>= fun cli ->
        send_to_client player "Type in START";
        let answer = read_from_client player in 
            answer >>= fun str ->
            match str with
                |"START" -> 
                    let ready_client =  cli in  
                    send_to_client player "Waiting for opponent";
                    Lwt_mvar.put waiting_players ready_client;
                | _ -> 
                    send_to_client player "Unknown command. try again";
                    make_ready player

let handle_income () =
    let in_conection = Lwt_unix.accept sock in 
    in_conection >>= fun (cli, addr) ->
    let player = Lwt.return cli in
    send_to_client player "Welcome to the server. To start game type in START and press Enter";
    make_ready player;;

    val make_ready : Lwt_unix.file_descr Lwt.t -> unit Lwt.t = <fun>
    val handle_income : unit -> unit Lwt.t = <fun>

似乎没问题,但是当我调用 Lwt_mvar.take waiting_players 它总是返回一些值,即使之前没有放任何东西并且线程没有被阻塞。这种奇怪的(对我来说)行为在例子中更好看:

# let bucket = Lwt_mvar.create_empty ();;
val bucket : '_a Lwt_mvar.t = <abstr>

# let apple = Lwt_mvar.take bucket;;
val apple : '_a Lwt.t = <abstr>

# Lwt.state apple;;
- : '_a Lwt.state = Sleep

如果“阻塞”意味着返回正是这样的睡眠对象,请告诉。以及如何制作一个循环,以最好的方式只返回“准备好的”对象?使用 Lwt.is_sleeping 是个好主意吗?非常感谢。

4

2 回答 2

4

您的方法几乎没有问题,代码中有一些错误。因此,我将首先强调后者,然后提出并证明另一种方法。

问题

第一期

看起来你send_to_client返回一个 type 的值unit Lwt.t。如果您只是通过用 终止表达式来忽略它;,那么这意味着“不要等到消息发送并继续前进”。通常这不是你想要的。unit Lwt.t因此,您需要通过绑定到其返回值来等待线程完成。

第 2 期

通常在 Lwt 编程中,函数接受立即类型的值(即,不包含在 中的Lwt.t值)并返回延迟线程(即,类型的值'some Lwt.t)。当然,这通常是没有人阻止你做一些不同的事情。但尽量坚持“立即输入,延迟输出”的模式。

第 3 期

使用工具。用于ocp-indent缩进您的代码,这将有助于提高可读性。此外,看起来您不使用编译器并且正在顶级播放。通常这是一个坏主意,尤其是对于系统编程。用于ocamlbuild编译和运行您的代码:

ocamlbuild game.native --

游戏

与 Python 或其他弱类型系统语言的编程相比,OCaml 编程具有不同的理念。在 OCaml 中,应该从设计类型和签名开始,然后填写实现。当然,这是理想化,在现实生活中会有一个迭代提炼的过程,但大体的做法还是一样的。从类型开始。

所以,首先,让我们定义一个player类型。这是微不足道的,但有改进的余地。

open Lwt

type player = {
  fd : Lwt_unix.file_descr
}

接下来,让我们使用类型系统来帮助我们,了解我们的游戏初始化问题。你需要让两名玩家准备好并愿意玩你的游戏。这意味着,您有三个连续的状态:

  • Nobody准备好了
  • One player准备好了
  • Both (player1, player2)准备好了

实际上,因为一旦你到达第三个状态,你就可以开始游戏了,你不需要那个状态,所以我们最终只有两个选择:

type stage =
  | Nobody
  | One of player

我们可以player option在这里使用类型,因为它与我们的选择同构。但是让我们更明确地使用我们自己的stage类型。它将使我们的模型更加受约束和拟合。

下一步将是定义客户端和服务器之间的交互协议。我们将使用名称request来表示从服务器到客户端的消息,以及response反向移动的消息。

type request =
  | Init
  | Wait
  | Unknown_command
  | Bye 

type response =
  | Start
  | Quit

这个协议是抽象的,因为它不包含任何具体的表示——基于它,您可以构建不同的表示,例如,gui 界面或支持不同语言的文本聊天。

但是让我们模拟一个使用文本命令的最简单的具体实现:

let response_of_string msg =
  match String.trim (String.uppercase msg) with
  | "START" -> Some Start
  | "QUIT" -> Some Quit
  | _ -> None

而在相反的方向(注意:最好在客户端呈现此消息,并发送类型requestresponse在线的值,这将使您的流量配置文件保持较低,更重要的是,将允许透明地附加不同的客户端)。

let string_of_request = function
  | Init -> "Welcome to a game server.
    Please, type 
    - `start' to start game;
    - `quit' to finish session"
  | Wait -> "Please wait for another player to join the game"
  | Unknown_command -> "Don't understand this"
  | Bye -> "Thank you, see you later!"

下一步是为Io. 该模块负责客户端和服务器之间的交互。请注意我们如何通过抽象隐藏所有细节,例如使用套接字或字符串。

module Io : sig
  val send : player -> request -> unit Lwt.t
  val recv : player -> response option Lwt.t 
end = struct
  let send dst msg = return_unit
  let recv dst = return None
end 

现在,我们可以定义我们的Game模块了。起初它将有两个不同的自动机:

  • init初始化两个玩家之间的游戏;
  • play玩一次游戏,我们得到两个受害者。

让我们在 OCaml 中明确地说:

module Game : sig

  (** [play a b] play a game between player [a] and player [b] *) 
  val play : player -> player -> unit Lwt.t

  (** [init next_player] waits until two players are ready to play.
      TODO: Describe a grammar that is recognized by this automaton. *)
  val init : (unit -> player Lwt.t) -> (player * player) Lwt.t
end = struct
  let play a b = return_unit

  let init next_player =
    let rec process stage player = 
      Io.send player Init >>= fun () -> 
      Io.recv player >>= function
      | None ->
        Io.send player Unknown_command >>= fun () ->
        process stage player
      | Some Quit ->
        Io.send player Bye >>= fun () -> 
        next_player () >>= process stage
      | Some Start -> match stage with
        | One a -> return (a,player)
        | Nobody -> 
          Io.send player Wait >>= fun () ->
          next_player () >>= process (One player) in
    next_player () >>= process Nobody
end

现在我们可以写出 main 函数,它将所有内容粘合在一起:

let main server_sock = 
  let next_player () =
    Lwt_unix.accept server_sock >>=
    fun (fd,_) -> return {fd} in
  Game.init next_player >>= fun (a,b) -> 
  Game.play a b

当您继续使用这种方法时,您稍后可能会注意到,游戏的不同有限状态机定义了不同的语言(即协议)。因此,您最终可能会为每个 FSM 使用特定的协议,而不是使用一个协议,例如 、init_protocolplay_protocol。但是您可能还会注意到,这些协议有一些交叉点。要处理此问题,您可以使用子类型和多态变体。

于 2015-08-28T17:06:23.000 回答
0

找到了办法。

let rec form_pairs () = 
let player1 = Lwt_mvar.take waiting_players in 
    player1 >>= fun descriptor1 ->
let player2 = Lwt_mvar.take waiting_players in 
    player2 >>= fun descriptor2->
Lwt_io.printl "Pairs formed";
Lwt.return (descriptor1, descriptor2);
form_pairs ();;
于 2015-08-23T17:03:17.663 回答