jarlyyn 发表于 2024-11-16 00:34:35

杰哥胡搞搞之关于类似nginx的mud反向代理的一些探讨

这两天因为某些原因,捡起了我之前gmcp显示插件,看看能不能弄成mud版的nginx。记录一下。
nginx是一个在网络领域使用的十分广泛的反向代理软件,作用是在实际的运行程序(相当于mud服务)和访问者(相当于客户端)之间进行负责均衡,转发,缓存等功能的重要软件。

可以说是一般网络应用横向扩展的基础。

对于一般的HTTP请求来说,分为4个部分


[*]请求头
[*]请求正文
[*]响应头
[*]响应正文


这个可以在浏览器F12中查看。

具体来说,请求头和响应头是在正文开始前的一些原信息,在不影响正文的情况下可以在不同的中间件之间传播和维护,实现各种复杂功能。

正文就是服务器和用户之间交互的数据,在mud里相当于各种指令和文字显示。

jarlyyn 发表于 2024-11-16 00:50:36

实现这样一个功能的意义。
总体来说,参考nginx,就可以知道这种服务的意义。

mud再为一个上古遗留物,可以说发展被单核卡的死死的。

有这么一个服务,可以把原本纹丝不动的100%限制挖出点缝隙来。可能要用本来200%甚至300%的cpu才能实现150%的性能。但总归超过100%了,不是吗?

具体能够实现的可能包括:

1.前后端分离。分担mud主服务器的渲染压力。mud服务器可以直接输出类似hpbrief或者gmcp的信息,由中间件渲染成用户能看到的界面。
2.数据缓存。典型的就是现在gmcp是实时更新数据的,那么只要在中间件把gmcp信息缓存了,那么用户hp或者score时,能直接从中间件缓存的数据中取再渲染,完全不经过mud主服务器
3.请求分流。最典型的就是top榜常客的news和list指令了。news指令上榜对我来说完全优点不可思议。直接中间件直接从cms或者数据里读九好了,压根不需要经过主服务器。用户输入指令 news 117,服务器接受后决定运行显示news 117,直接发一个news 117的gmcp(阅),中间件接受到服务器返回的指令,从数据库中取得相应新闻,渲染后发送给用户,对服务器来说性能消耗不过相当于一个responser R:。当铺的list同理,只是查询完全没不要从主服务器走一波。
4.文字渲染格式话。和GBK不同,UTF8的显示调整对齐是非常慢的,某些时候也能带来性能瓶颈。有中间件可以把文本带上标记直接抛给中间件,由中间件处理
5.直通副本服务器。目前北侠的架构副本服务器还是要经过主服务器的,top cmd初期很多副本指令上榜就是这个原因。
6.实时监控。中间件可以在不耗费主服务器性能的情况下记录所有输入,传入各种日志服务器,甚至理论上可以管理实时连接,不经过主服务器实现snoop,可以做各种实时指令大屏显示监控。

最关键是,这种技术其实技术要求不高,但凡能做点基础客户端的肯定能做这个。解析ansi,处理指令,通过vm进行编程,本质这就是个跑在服务器端,以telnet接口为显示界面的mini客户端罢了。

jarlyyn 发表于 2024-11-16 00:54:47

接下来是实现的肉戏。我们要做什么。
作为一个反向代理,第一部也是最重要的一步是认证,替换环境信息。

以nginx为例,nginx最基本的一个功能是
1.认证,最简单的就是在请求头里通过一个预定义指在内部使用的足够长的token认真。复杂点可以加入各种摘要算法。
2.替换ip。nginx一般在请求头里带一个X_Forward_FOR的请求头。一旦通过了认证,服务器就相信这个是用户的真实ip。

也可以说是这个架构的核心基础,实现mud连接的请求头。

一旦实现了请求头,各种功能都能实现,比如session,jwt这种对多副本服务器很好用的东西。

jarlyyn 发表于 2024-11-16 01:21:53

调整Mud登陆流程。

为了这个我差不多把fluffos和我手头的mudlib读了几遍,终于把这个理顺了。

首先,fluffos这东西本质和客户端一样,有个主脚本入口。定义在config文件中,

比如我手头的mudlib是

master file : /adm/kernel/master
这个具体由fluffos的src/comm.cc负责

用户请求连入后,会先在调用comm.cc里的new_conn_handler
    auto *user = new_user(port, fd, addr, addrlen);
    new_user_event_listener(base, user);

    if (user->connection_type == PORT_TYPE_TELNET) {
      user->telnet = net_telnet_init(user);
      send_initial_telnet_negotiations(user);
    }

    event_base_once(
      base, -1, EV_TIMEOUT,
      [](evutil_socket_t /*fd*/, short /*what*/, void *arg) {
          auto *user = reinterpret_cast<interactive_t *>(arg);
          on_user_logon(user);
      },
      (void *)user, nullptr);


new_user是一个interactive_t对象,本质代表了一个连接
这代码就是初始化新连接,初始化telnet的一些协商,然后调用on_user_login方法,开始尝试登陆

on_user_logon的代码为

void on_user_logon(interactive_t *user) {
set_command_giver(master_ob);
master_ob->flags |= O_ONCE_INTERACTIVE;
master_ob->interactive = user;

/*
   * The user object has one extra reference. It is asserted that the
   * master_ob is loaded.Save a pointer to the master ob incase it
   * changes during APPLY_CONNECT.We want to free the reference on
   * the right copy of the object.
   */
object_t *master, *ob;
svalue_t *ret;

master = master_ob;
add_ref(master_ob, "new_user");
push_number(user->local_port);
set_eval(max_eval_cost);
ret = safe_apply_master_ob(APPLY_CONNECT, 1);
/* master_ob->interactive can be zero if the master object self
   destructed in the above (don't ask) */
set_command_giver(nullptr);
这个一眼看上去就和lua差不多……

里面其实有个重要的地方,就是master_ob->interactive = user;,连接所有权的转交。之前没接触过mudos,fluffos的文档又不全,这东西看代码卡了我很久……

代码很明显,压入一个数字参数 local port,调用 APPLY_CONNECT方法初始化。

这一段在我的mudlib里是这样的

object connect(int port)
{
      object login_ob;
      mixed err;

      err = catch(login_ob = new(LOGIN_OB));
        set_encoding("GBK");

      //if(port == GBK_PORT)
      //{
      //      login_ob->set_temp("GBK",1);
      //      set_encoding("GBK");
      //}
   
      if (err)
      {
                write("现在有人正在修改使用者连线部份的程式,请待会再来。\n");
                write(err);
                destruct(this_object());
      }
      return login_ob;
}
master.c作为入口负责接受新请求,然后初始化一个登陆对象抛会去。

登陆对象定义在include/globals.h里
#define LOGIN_OB      "/clone/user/login"
指向了clone/user/login文件。


这是一个登陆对象,指的是登陆中的用户。很明显,初始化后,用户登陆前,这是我们需要接入的地方。


回到fluffos源代码,初始化后
ob = ret->u.ob;
ob->interactive = master_ob->interactive;
ob->interactive->ob = ob;
ob->flags |= O_ONCE_INTERACTIVE;
/*
   * assume the existance of write_prompt and process_input in user.c
   * until proven wrong (after trying to call them).
   */
ob->interactive->iflags |= (HAS_WRITE_PROMPT | HAS_PROCESS_INPUT);

free_object(&master, "new_user");

master_ob->flags &= ~O_ONCE_INTERACTIVE;
master_ob->interactive = nullptr;
add_ref(ob, "new_user");

// start reverse DNS probing.
query_name_by_addr(ob);

set_command_giver(ob);

set_prompt("> ");

// Call logon() on the object.
set_eval(max_eval_cost);
ret = safe_apply(APPLY_LOGON, ob, 0, ORIGIN_DRIVER);
if (ret == nullptr) {
    debug_message("new_conn_handler: logon() on object %s has failed, the user is disconnected.\n",
                  ob->obname);
    remove_interactive(ob, false);
主对象把输入接口转交给登陆对象,一次交接,然后开始调用登陆对象logon函数

void logon()
{
        remove_call_out("time_out");
        call_out("time_out", LOGIN_TIMEOUT);

        if (interactive(this_object()))
        {
                //log_file("static/logon", sprintf("%s %s\n", ctime(time()), query_ip_number(this_object())));
                set_temp("ip_number", query_ip_number(this_object()));
        }

        LOGIN_D->logon(this_object());
}
很明显,设置ip地址,然后调用登陆服务(根据登陆信息生成玩家对象,转移输入接口所有权,这里坑了我一天,我用登陆对象发gmcp怎么都发不出去,拉最新的fluffos编译再调整兼容性都不行)

我们要改的必然又是这里。目标找到,开始改。

jarlyyn 发表于 2024-11-16 01:35:42

我们既然目标是nginx,那就从学nginx开始。

nginx是先和主机交互http请求头,知道明确的终止标志,再开始处理正文。

刚刚这段代码,明显调用LogonD是开始处理正文。

所以,我们开始定义 请求头 和 请求头结束的标志。

有个input_to的efun(内建api)很简单好用,就是把等待用户输出传给指定函数。

所以,我这里定义的是三个指令


[*]auth xxxxx 认证身份
[*]ipadress xxxxxx 试图指定ip地址
[*]finish 请求头结束


首先,我把logon函数改名,叫raw_logon

其次,写一个新的logon

void logon(){
                input_to( "waiting", this_object(),1,this_object());
}指定了请求头处理函数"waiting"

void waiting(string message,object ob){
        if (strlen(message)>4 && message=="Auth "){
                if (proxy_auth(ob,message)!=1){
                                destruct(this_object());
                                return;
                }
        }
        else if(strlen(message)>9 && message="IPAddress "){
                        proxy_set_ipaddress(ob,message);
        }else if (message=="Finish"){
                if (proxy_authed(ob)!=1){
                                destruct(this_object());
                                return;
                }
                evaluate(bind((:raw_logon:),ob));
                return;
        }
        input_to( "waiting",1,ob);
很简单。

里面用来很多proxy开头的函数,那是我自定义的simul_efun,就是脚本框架接口,由mudlib(相当于机器人)提供,用LPC写的。对应的efun,由driver(相当于客户端)提供,由C写的,

string ProxyToken="12345";

string query_ip_proxy_token(){
    return ProxyToken;
}


int proxy_set_ipaddress(object ob,string val){
    if (undefinedp(ob)){
      ob=this_object();
    }
    if (objectp(ob->query_temp("link_ob"))){
      ob=ob->query_temp("link_ob");
    }
    if (ob->query_temp("proxy_verified")){
            ob->set_temp("ipaddress",val);
            return 1;
    }
    return 0;
}
int proxy_auth(object ob,string token){
    if (undefinedp(ob)){
      ob=this_object();
    }
    if (objectp(ob->query_temp("link_ob"))){
      ob=ob->query_temp("link_ob");
    }
    if (ProxyToken==token){
      ob->set_temp("proxy_verified",1);
      return 1;
    }
    return 0;
}
int proxy_authed(object ob){
      if (undefinedp(ob)){
      ob=this_object();
    }
    if (objectp(ob->query_temp("link_ob"))){
      ob=ob->query_temp("link_ob");
    }
    if (ob->query_temp("proxy_verified")){
      return 1;
    }
    return 0;

}
void proxy_set_token(string token){
    ProxyToken=token;
}
void proxy_command(object ob, string data){
    ob->gmcp_command("Proxy.Command "+data+"\n");
}
string query_ip_number(object ob){
    if (undefinedp(ob)){
      ob=this_object();
    }
    if (objectp(ob->query_temp("link_ob"))){
      ob=ob->query_temp("link_ob");
    }
    if (ProxyToken==""){
      return efun::query_ip_number(ob);
    }
    return ob->query_temp("ipaddress");
}
proxy.c的内容就是这些。

里面有一个query_ip_number的覆盖。把实际ip储存在连接对象(这是个大坑,这不是连接对象,是码上失效的登陆对象)里。每次查ip就直接查这个值。

很好,nginx的请求头部分做好了。

我的中间件会给每个请求开一个javascript的vm。

请求初始化后的回调会直接发这些信息

OnConnectReady = function () {
    Connect.WriteSendBufferEscaped(Text(`Auth ${Token}\nIPAddress ${RemoteAddr}\nFinish\n`))
}


然后写了个proxy测试的指令

inherit F_CLEAN_UP;

void create() { seteuid(getuid()); }

int main(object me, string arg)
{
                  write("ip:"+query_ip_number(me)+"\n");
                  write("ip2: "+me->query_temp("ipaddress")+"\n");
                  write("id: "+me->query("id")+"\n");
                  write("gmcp "+ has_gmcp(this_player())+"\n");
                          proxy_command(me,"proxytest");
                  return 1;

}

int help(object me){
    return 1;
}


很好,第一部,伪造(认证)ip地址完成了。

jarlyyn 发表于 2024-11-16 01:48:05

完成了请求头部分的工作,就该完成响应部分了。

响应很简单,由于已经完成了认证,所以直接利用gmcp或者类似的SubNegotiation实现就好。
fluffos默认只有一个send_gmcp,(很坑,害得我所有相关的代码和mudlib看了好几遍),但可以参考send_gmcp写一个efun,应该也不难。

用send_gmcp最大的坑就是,只有当前有交互所有权的对象才能发。当交互所有权转出去后,可能处于一个不报错,也不发的状态。

我在可能交互的交互所有权的clone/login和clone/user里都加了相应发送函数

void gmcp_command(string data){
                send_gmcp(data);
}这样在不同的情况都能用proxy_command接口统一发送gmcp了。

然后中间件相应gmcp内容,我有个配置表
#脚本引擎.可为空或js
Engine = "js"
# DaemonScript="scripts/gmcpdaemon.js"
#连接脚本主程序
ConnectScript = "gmcp.js"
#连接启动函数
OnConnectStart = "OnConnectStart"
OnConnectReady = "OnConnectReady"
#连接用户输入处理函数
OnConnectUserInput = "OnConnectUserInput"
#系统指令命令处理函数表

#IAC Will 处理函数
"251" = "OnConnectServerCommandWill"
#用户指令命令处理函数表

#IAC DONT 处理函数
"254" = "OnConnectUserCommandDont"
#IAC WONT 处理函数
"252" = "OnConnectUserCommandWont"
#系统子协议 SubNegotiation 处理函数表

#GMCP 处理函数
"201" = "OnGMCP"
然后就能处理gmcp数据了。

OnGMCP = (output, opt, data) => {
    let text = Decode(data)
    if (text.startsWith("Proxy.Command sort_message ")) {
      Connect.WriteDisplayBufferEscaped(Text(`${sort_message(text.slice(27))}`))
      return true
    }
}

好了,接下来就是怎么根据gmcp指令来渲染界面了,这就是我这个外挂程序的本职工作。

剩下的就是扩展了。

能通过请求头指定ip,之然根据请求头指定用户id,这样可以实现通过js控制的,登陆主服务器,获取session,断开主服务器,连接副服务器,通过session免重复登陆的工作了。

剩下的可以通过fluffos的开启socker端口服务功能,将主服务器与副本服务器/中间件进行直接的数据互通,更好的从主服务器上卸工作,降低主服务器cpu占有率,甚至开发更多更好玩的功能。

jarlyyn 发表于 2024-11-16 01:49:54

写完了。

fluffos的文档内容实在有点单薄,大部分内容是我爬代码和一点一点测试出来的。

毕竟对mudlib/mudos这块不熟悉,也花了我两三天功夫了,记录下,备忘,当然能帮助到有同样想法的人自然最好。

ppmm 发表于 2024-11-16 06:30:39

虽然看不懂,看着好像这个有助于解决现在北侠的cpu负载过高问题?

man 发表于 2024-11-16 06:56:52

老一辈的程序员超过35岁就会被优化,因为跟不上自己的技术了啊,新技术至少mudos里心跳和网络接发不是一个cpu吧,然而老的人都不会了

jarlyyn 发表于 2024-11-16 08:34:34

ppmm 发表于 2024-11-16 06:30 AM
虽然看不懂,看着好像这个有助于解决现在北侠的cpu负载过高问题?

只能说是一个思路吧。

毕竟我对nginx模式比较熟悉,第一反应就是套下这个模式,也算路径依赖了。

搁网络游戏里这应该就属于网关服务器的范畴了,参考。

其实我很早就对网关服务器就很感兴趣,自己的go框架也早早实现了连接网关的模块,然后做客户端的时候就拿这个网关降级了下使,意外的好用。

这个只是一个原型验证,验证driver和mudlib是否能实现这样的功能。这个可行的话,思路就能打开很多了。

我熟悉go js,有成熟的代码可以复用,所以马上能糊一个网关框架。

你对python熟,其实利用现成的库,写一个简单网关也不难。

只有有成熟的网络支持,有脚本支持,弄一个网关都容易。

甚至其实fluffos本身跑一个极简的mudlib做网关可能也不错。

现在的北侠的主服务器就是同时负担了网关服务器和游戏服务器的双重责任,那么把网关部分提出来,应该能很简单的就给服务器起到了减负作用。

而且有很多基础telnet/ansi的工具,解析mud文件也异常顺手。

别的不说,网关服务器实现news和当铺的list部分,不需要主服务器查找/渲染,怎么看都简单的不行,搞不好ln几个文件过来就行了。
页: [1] 2
查看完整版本: 杰哥胡搞搞之关于类似nginx的mud反向代理的一些探讨