SDL中文论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz

对战服务器

查看数: 4617 | 评论数: 5 | 收藏 0
关灯 | 提示:支持键盘翻页<-左 右->
    组图打开中,请稍候......
发布时间: 2020-9-4 08:43

正文摘要:

概述 修改完善 1、个人暂时不会在服务器上花太多时间。考虑到已有版本在主功能已没多大问题,即使客户端增改功能,服务器对绝大多数包都是透明的,不必改。 2、服务器程序会一直提供个开源版本。提供的版本会尽 ...

回复

ancientcc 发表于 2020-9-4 08:45:05
为支持半即时制服务器代码需做的改动

bool game::is_legal_command(...)
  1. bool game::is_legal_command(const simple_wml::node& command, bool is_player) {
  2.         // Only single commands allowed.
  3.         if (!command.one_child()) return false;
  4.         // Chatting is never an illegal command.
  5.         if (command.child("speak")) return true;
  6.         ......
  7.         if (is_player
  8.         && (command.child("label")
  9.                 || command.child("clear_labels")
  10.                 || command.child("rename")
  11.                 || command.child("countdown_update")
  12.                 || command.child("global_variable")
  13.                 ))
  14.         {
  15.                 return true;
  16.         }
  17.         return false;
  18. }
复制代码

改为
  1. bool game::is_legal_command(const simple_wml::node& command, bool is_player) {
  2.         // Only single commands allowed.
  3.         // if (!command.one_child()) return false;                <==修改1:注释掉这一句
  4.         // Chatting is never an illegal command.
  5.         if (command.child("speak")) return true;
  6.         ......
  7.         if (is_player
  8.         && (command.child("label")
  9.                 || command.child("clear_labels")
  10.                 || command.child("rename")
  11.                 || command.child("countdown_update")
  12.                 || command.child("global_variable")
  13.                 || command.child("prefix_unit")         <==修改2:增加prefix_unit
  14.                 ))
  15.         {
  16.                 return true;
  17.         }
  18.         return false;
  19. }
复制代码


bool game::process_turn(...)
  1. bool game::process_turn(simple_wml::document& data, const player_map::const_iterator user) {
  2.         ......
  3.         for (command = commands.begin(); command != commands.end(); ++command) {
  4.                 if (!is_current_player(user->first)
  5.                 && !is_legal_command(**command, player)) {
  6.                         ......
  7.                 } else if ((**command).child("speak")) {
  8.                         ......
  9.                 } else if (is_current_player(user->first) && (**command).child("end_turn")) {
  10.                         turn_ended = end_turn();
  11.                 }
  12.                 ++index;
  13.         }
  14.         ......
  15. }
复制代码

改为
  1. bool game::process_turn(simple_wml::document& data, const player_map::const_iterator user) {
  2.         ......
  3.         for (command = commands.begin(); command != commands.end(); ++command) {
  4.                 if (!is_current_player(user->first)
  5.                 && !is_legal_command(**command, player)) {
  6.                         ......
  7.                 } else if ((**command).child("speak")) {
  8.                         ......
  9.                 } else if ((**command).child("prefix_unit")) {                <==修改1:删除end_turn入口,改为此个prefix_unit
  10.                         simple_wml::node& prefix_unit = *(**command).child("prefix_unit");
  11.                         if (prefix_unit["new_turn"].to_int()) {
  12.                                 turn_ended = true;
  13.                                 end_turn_ = current_turn() * nsides_; // +1 turn
  14.                                 if (description_) {
  15.                                         description_->set_attr_dup("turn", describe_turns(current_turn(), level_["turns"]).c_str());
  16.                                 }
  17.                         }
  18.                         end_turn_ = (nsides_? end_turn_ / nsides_ : 0) * nsides_ + prefix_unit["side"].to_int() - 1;

  19.                 }
  20.                 ++index;
  21.         }
  22.         ......
  23. }
复制代码


bool game::end_turn()
  1. bool game::end_turn() {
  2.         ......
  3.         // Skip over empty sides.
  4.         for (int i = 0; i < nsides_ && nsides_ <= gamemap::MAX_PLAYERS && side_controllers_[current_side()] == "null"; ++i) {
  5.                 ++end_turn_;
  6.                 if (current_side() == 0) {
  7.                         turn_ended = true;
  8.                 }
  9.         }
  10.         ......
  11. }
复制代码

改为
  1. bool game::end_turn() {
  2.         ......
  3.         // Skip over empty sides.
  4.         for (int i = 0; i < nsides_ && nsides_ <= gamemap::MAX_PLAYERS && side_controllers_[current_side()] == "null"; ++i) {
  5.                 // ++end_turn_;                        <==修改1:注释掉这一句
  6.                 if (current_side() == 0) {
  7.                         turn_ended = true;
  8.                 }
  9.         }
  10.         ......
  11. }
复制代码


修改原因
bool game::is_legal_command(...)
该函数用于判断客户端发来的是否是合法命令。合法指的是在当前玩家下、其它玩家可产生的命令。对于这些命令,直观可知道是他人发给当前玩家的聊天记录,也就是当中的“speak”。修改目的是要把新增的prefix_unit视为合法命令,只要下一轮到行动单位的玩家和之前不属同一个,那么此个命令就不会是当前玩家产生的。

注释掉“if (!command.one_child()) return false”,“prefix_unit”有可能是多个child。在command认为,random块也是一个和prefix_unit同级的的块。prefix_unit极可能会产生多个random。

修改2则是把prefix_unit增加到合法命令集。

bool game::process_turn(...)
回合制时,结束每个势力时都会发送“end_turn”,由于势力肯定按固定顺序操作,服务器就可根据收到的“end_turn”以及当前关卡的势力数判断出回合是否结束了(turn_ended)、接下是轮到哪势力(end_turn_)。

改为半即时制后,“prefix_unit”中的side字段指示接下轮到哪势力。“new_turn”指示接下轮到行动的单位是否是新回合的第一单位,要注意的是回合1时new_turn字段值是0

end_turn_仍旧需沿袭韦诺设置,值不是回合数,而是考虑了当前势力、由它可计算出回合数的值。
  1. end_turn_ = (turn - 1) * nsides_ + side;

  2. turn:以1开始的回合数。
  3. nsides_:此次关卡的势力数。
  4. side:当前轮到的势力。以0开始,最大值是nsides_ - 1。
复制代码


bool game::end_turn()
在韦诺的回合制下,空势力是不会发“end_turn”的,因而计算end_turn_要继续跳。

改为半即时制后,当前轮到势力已被单位乱序,判断下一是空势力反而会使得“end_turn_”出现误错误。
ancientcc 发表于 2020-9-4 08:44:44

对战服务器中的一个发送包可能不能即时发送BUG

该BUG不会造成程序非法退出,但由于数据不能即时发送,会造成玩家不得不耗时间在等。这个BUG只是延时,倒是不会漏发。

举个例子。要支持存档上继续对战,此种情况要开始游戏时,创建者短时间内要把BINARY_HEROS、BINARY_HEROS_START、BINARY_REPLAY、BINARY_SIDE这几个二进制数据包发送去客户端(这些包都没有应答),但由于BUG影响,等待玩家不能即时收到这些包,致使只能在大厅不断等待游戏开始。

如何解决BUG
方法一:修改代码
找到收发数据线程函数:static int process_queue(void* shard_num)
  1. if(min_threads && waiting_threads[shard] >= min_threads) {
  2.         DBG_NW << "worker thread exiting... not enough jobs\n";
  3.         to_clear[shard].push_back(threading::get_current_thread_id());
  4.         return 0;
  5. }
复制代码
改为
  1. if(outgoing_bufs[shard].empty() && min_threads && waiting_threads[shard] >= min_threads) {
  2.         DBG_NW << "worker thread exiting... not enough jobs\n";
  3.         to_clear[shard].push_back(threading::get_current_thread_id());
  4.         return 0;
  5. }
复制代码
增加退出线程的判断条件,只在没有待发送数据时(outgoing_bufs[shard].empty())才允许退出线程。

方法二:启动服务器(wesnothd)时,让至少线程数等于至多线程数,而且都不为0
像以下这个命令行。
  1. -t 5 -T 5:至少五个收发线程、至多5个收发线程。这使得不会动态创建、删除收发线程。
复制代码
BUG内在原因
BUG存在的一个客观原因是系统中并发存在多个收发线程(process_queue)。让看以下这个执行逻辑(假设至少线程数[min_threads]设的是2)。
  • 收发线程#1检测到有一个包要发送,于是把套接字状态设到SOCKET_LOCKED,并开始在网络上执行发送数据。
  • 又有数据要发送,收发线程#2被触发,但由于套接字状态是SOCKET_LOCKED,线程#2随即进入睡眠。
  • 又有数据要发送,收发线程#3被触发,但由于套接字状态是SOCKET_LOCKED,线程#3随即进入睡眠。
  • 收发线程#1发送数据结束,转去线程循环处开始执行是否要退出该线程判断,由于满足“if (min_threads && waiting_threads[shard] >= min_threads)”(线程#2和线程#3的等待使得waiting_threads等于2),导致线程#1退出!
  • 线程#1退出,线程#2、线程#3则死等收发信号量有信号,而在收发信号量有信号前,堆积着的待发送数据就这样没被即时发送出去了。当然,后面会发心跳包(ping)啥,迟早会让收发信号量有信号,迟早数据是会被发送。
  • 这个未被即时发送碰到概率以及延时时间和该服务器连接着的用户数有关,用户数越多,来回的数据包越多,收发信号量被有信号越频率,该BUG影响越小。

  • 解决BUG中的第一种方法是一旦还有待发送数据,不允许该线程退出,要等没数据可发送了再退出。
  • 解决BUG中的第二种方法是不让动态创建、退出线程。由于不会退出,线程在等待收发信号量前会检查是否有数据要发送,因而必然会先发送光数据。


虽然王国战争程序和对战服务器程序在收发网络数据使用的是同一个线程函数(process_queue),但王国战争程序不存在这BUG,只有对战服务器才存在。因为王国战争程序至少、至多线程数设的都是1。
ancientcc 发表于 2020-9-4 08:44:21

至少、至多收发线程数(process_queue)

  • 对战服务器的命令行参数“-t”用于指定至少线程数,“-T”用于指定至多线程数。当命令行未指定参数时,默认至少线程数是5,至多线程数是0(不限制)。王国战争也使用服务器一样的收发线程,它的至少、至多线程数固定都设为1。
  • 至少线程数指示程序一定会保证并发存在的线程数。至多线程数是程序最多能并发存在的线程数。
  • 当至少线程数不等于至多线程数时,程序将有动态创建、删除线程操作。
  • 程序确保至少线程数是通过统计当前等待线程数(正在等待收发信号的线程)(waiting_threads[shard]),它会确保只有等待线程数>=至少线程数时才开始考虑删除线程。
  • 当等待线程数等于0时,表示系统收发线程都在忙于工作,这时会考虑新建收发线程。
ancientcc 发表于 2020-9-4 08:44:01

配置文件、mysql模块

配置文件
配置文件来自命令行参数“-c”指定的参数,当没有“-c”时使用空配置。如何使用“-c”分种两种情况。
  • 在dos框启动kingdomd时,-c由命令行自主设定。
  • 游戏内置启动时(按需启动对战服务器),-c是指定在了“<My Documents>/My Games/kingdom/lan_server.cfg”,而这个文件来自<kingdom-res>/data/lan_server.cfg。


mysql模块
玩家账号在韦诺官方论坛有注册时,韦诺对战服器有有可能去自动连接这个论坛、从而强制要求输入密码才能进入对战。

在程序中,称和这个自动连接相关叫mysql模块。对战服务器要调用mysql需同时满足以下三个条件。
  • server::cfg_有[user_handler]块。server::cfg_就是服务器配置文件中内容。
  • uh_name_等于“forum”或置空。uh_name_来自[user_handler]块下的“user_handler”字段。
  • 预定义了宏HAVE_MYSQLPP。

希望不调用mysql,又不想改源代码,1)不要在配置文件中出现[user_handler]块;2)不得不出现[user_handler]时,当中的“user_handler”不要等于“forum”,也不要置空。
ancientcc 发表于 2020-9-4 08:43:35

FAQ

A:kingdomd需要多少个端口,TCP还是UDP,默认端口号多少?
Q:kingdomd只需要一个端口,类型TCP。默认端口号是15000,可用命令行kingdomd -p <port>修改端口号。

A:运行kingdomd第二个实例,会不会在系统中存在两个kingdomd?
Q:不会。kingdomd要绑定到指定端口,这个端口和第一个是一样的,致使绑定失败,kingdomd会报“Could not bind to port”失败而退出。

A:kingdomd会开多少个线程来处理客户端I/O请求?
Q:min_threads=5,max_threads=0。关于min_threads/max_threads见4楼的“至少、至多收发线程数”。

Archiver|手机版|小黑屋|丽谷软件|libsdl.cn

GMT+8, 2025-5-2 04:11 , Processed in 0.072595 second(s), 24 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表