SDL中文论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 4618|回复: 5
打印 上一主题 下一主题

[network] 对战服务器

[复制链接]

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
跳转到指定楼层
楼主
发表于 2020-9-4 08:43:08 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
概述

不支持直连对战。直连指的是A的kingdom.exe和B的kingdom.exe直接通信。要对战必须通过kingdomd.exe,对于这个kingdomd.exe可以和当中一个kingodm.exe同一台PC上,甚至A的kingdom.exe、B的kingdom.exe和kingdomd.exe三个都位在同一台PC。

功能
1、转发数据包。Server #0介于Player A和Player B之间,B要向A发包时,全路径为B==>#0==>A,反之则为A==>#0==>B。

2、管理会话。在这里会话一般指一次游戏过程。Player A建立游戏,新会话开始,Player B加入开始游戏,在游戏过程中Server #0要不断监视会话状态,像Player B中途退出了,它就要发包给Player A告知Player B已经退出,以便Plyaer A根据玩家选择是终止游戏,还是以AI/本地玩家代替Plyaer B。当此次游戏只剩一个玩家或没玩家时,会话将被Server #0拆掉。

以上是对战服务器两大主要功能。也是当前版本实现了的。至于其它功能,像用户管理,做出类似普通/VIP用户系统(游戏时所有数据都要经过对战服务器,包括用户登陆包),这些就多了。


数据包中数据
数据包中数据决定了网络流量,也基本上决定了服务器需要的CPU消耗。

非特殊情况,游戏会确保将来也不发送多媒体数据。多媒体数据包括声音和视频。

数据包中内容可能是字符串和二进制。当前版本实现的都是字符串,但将来为更缩减流量及减少CPU消耗,可能会使用二进制(当前版本已是很省了,不过没人不想更省的)。字符串好处是使数据包简单、明了,易修改数据格式。

具体数据包格式参考:http://www.freeors.com/bbs/forum.php?mod=viewthread&tid=20868

服务器程序
要运行服务器需要以下这五个exe/dll。
kingdomd.exe:可执行程序
intl.dll:支持多语言gettext动态链接库
SDL.dll:SDL主功能动态链接库
SDL_net.dll:SDL网络动态链接库[/code]注:
1、当前只编译出windows下版本的kingdomd.exe,希望有人能编译出Linux下的。
2、服务器运行时会在kingdome.exe同一目录下生成stderr.txt和stdout.txt两个文本文件。
3、要运行服务器直接双击kingdomd.exe就行。要结束则按ALT+CTRL+DEL,从进程列表中删除kingdomd.exe。


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

2、服务器程序会一直提供个开源版本。提供的版本会尽量和韦诺之战服务器最新版看齐,代码尽量不做修改。

3、欢迎有兴趣网友自写服务器程序。对于要理解已有数据包格式,或因为功能需要增加何种数据包,可以协作开发。至于写出的服务器程序源码要不要开放,程序是不是免费,个人不作干涉,视编写人自已决定。
回复

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
沙发
 楼主| 发表于 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楼的“至少、至多收发线程数”。
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
板凳
 楼主| 发表于 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”,也不要置空。
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
地板
 楼主| 发表于 2020-9-4 08:44:21 | 只看该作者

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

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

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
5#
 楼主| 发表于 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。
回复 支持 反对

使用道具 举报

149

主题

331

帖子

2445

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2445
6#
 楼主| 发表于 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_”出现误错误。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2025-5-2 05:05 , Processed in 0.060972 second(s), 22 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

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