nodejs循序渐进-高性能游戏服务器框架pomelo之启动流程和组件

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: nodejs循序渐进-高性能游戏服务器框架pomelo之启动流程和组件

游戏启动过程

启动入口

在使用pomelo进行游戏开发时,工程目录下的app.js是整个游戏服务器的启动运行入口。app.js中创建项目,进行默认配置并启动服务器的代码如下:

var pomelo = require('pomelo');
var app = pomelo.createApp();
app.set('name', 'nameofproject');
app.start();

image.gif

从上面的代码可以看出,用户首先需要在项目中引入pomelo,然后创建application的实例app,接着完成一些基本的应用配置,最后应用就可以启动了。当app.js运行起来后,pomelo会根据游戏的配置启动不同的相关组件及服务器。

服务器与组件

服务器启动流程的主要工作就是逐一启动app.load注册的组件。组件是连接pomelo框架和当前服务器所依赖的服务之间的桥梁。不同的服务器可以选择加载不同的组件。Pomelo提供了一些系统默认的组件,主要包括:handler, filter, master, monitor, proxy, remote, server, sync, connection。开发者也可以根据需要,开发自己的组件,并加载到服务器进程中。

组件同时是具有生命周期的,其生命周期可以包括start, after start, stop等。在组件中可以实现这些方法,应用服务器会在不同的运行阶段执行组件不同生命周期的方法。

启动流程详述

应用创建及启动

所有服务器的启动都是从运行app.js开始。每一个服务器的启动都首先创建一个全局唯一的application对象,该对象中挂载了所在服务器的所有信息,包括服务器物理信息、服务器逻辑信息、以及pomelo的组件信息等。同时,该对象还提供应用管理和配置等基本方法。 在app.js中调用app.start()方法后,application对象首先会通过loadDefaultComponents方法加载默认的组件。

组件加载

在加载组件时,系统会根据application对象中服务器的信息,针对不同的服务器加载不同的组件,从而使得不同服务器进程对外提供不同个服务。对于master服务器,主要加载的组件是master组件。Master组件主要负责根据根据servers.json文件中的配置信息和启动参数去启动其他服务器。对于其它服务器默认加载proxy、channel、sync、backendSession和server组件,特定的服务器还需要加载特定的组件,例如前端服务器会加载统计客户端连接数量的connection组件。具体组件的说明如下:

    • master: master组件主要负责启动master服务器。
    • monitor: monitor组件主要负责启动各个服务器的monitor服务,该服务负责收集服务器的信息并定期向master进行消息推送,保持master与各个服务器的心跳连接。
    • proxy: proxy组件主要负责生成服务器rpc客户端,由于系统中存在多个服务器进程,不同服务器进程之间相互通信需要通过rpc调用(master服务器除外)。
    • remote: remote组件主要负责加载后端服务器的服务并生成服务器rpc服务端。
    • server:server组件主要负责启动所有服务器的用户请求处理服务。
    • connector: connector组件主要负责启动前端服务器的session服务和接收用户请求。
    • sync: sync组件主要负责启动数据同步模块并对外提供数据同步功能。
    • connection: connection组件主要负责启动用户连接信息的统计服务。
    • channel: channel组件主要负责启动channelService服务,该服务主要提供channel相关的功能包括创建channel,通过channel进行消息推送等。
    • session: session组件主要负责启动sessionService服务,该服务主要用来对前端服务器的用户session进行统一管理。
    • backendSession: backendSession组件主要负责启动backendSession服务,该服务主要负责维护服务器本地session并与前端服务器进行交互。
    • dictionary: dictionary组件主要负责生成handler的字典。
    • protobuf: protobuf组件主要负责解析服务端和客户端的proto buffer的定义,从而对客户端和服务端的通信内容进行压缩。

    组件概述

    我们知道,pomelo的应用程序执行的全部过程,就是对其相应组件的生命周期的管理,实际的所有逻辑功能均由pomelo组件提供。pomelo内建提供了十多个组件,这些组件适用于不同的服务器,提供不同的功能。有些组件提供的功能比较复杂,有些则比较简单。下面我们将以提供的功能为主线来阐述pomelo提供的内建组件。

    master组件

    master组件仅仅由master服务器加载,它主要的功能包括启动所有的应用服务器、管理和监控所有的应用服务器和接受管理客户端的请求与响应。

    master服务器总是率先启动,master组件在其start调用的最后才会调用Starter.start,Starter.start才会启动所有的应用服务器,因此Master组件总是最先start。在Master组件的start调用中,会完成以下几步:

      • 加载注册Module到MasterConsoleService,Module的导出方式有两种,可以导出工厂函数,也可以导出对象,如果导出工厂函数的话,其签名应该是 FacFunc(opts, ConsoleServicee),其中opts是用户调用app.registerAdmin的时候传入的,ConsoleService则是具体的加载注册Module的MasterConsoleService。
      • 在加载注册完所有的Module后,会开启MasterAgent对端口的监听,此时,master就已经可以接收来自monitor和client的request和notify了。
      • 开启监听后,MasterConsoleService会enable所有的module,这步操作主要是看看有没有module配置了周期性地拉去monitor信息,也就是module的配置中有type选项和interval选项,且type的值为'pull',interval指定了周期,则认为其配置了周期性监控操作,此时会完成周期性事件的调度,使得master可以周期性地获取监控信息。
      • 最后如果有Module定义了start回调,将会在这里调用,一般在start回调里会做一些初始化信息。 经历了这些步骤后,master完成启动。

      在master组件的start方法里,会根据用户提供的服务器配置信息,启动用户配置的所有的具体应用服务器。

      当master组件start结束后,他将开启一个socket监听端口,接受应用服务器和监控客户端的连接和注册,收集应用服务器上报的监控信息,给应用服务器推送一些消息,并对管理客户端发出的管理请求给予响应。管理客户端如pomelo-cli可能发出的请求包括查看某个服务器进程状态,增加一个服务器,停掉一个服务器等。以增加一个服务器为例,当管理客户端发出增加服务器请求时,会提供相应的服务器参数,如服务器类型,主机ip,开启的端口等。此时,master组件接受后,会启动相应的服务器,并将新增加的服务器信息广播通知给其他已经启动的服务器。

      master组件无配置项。

      monitor组件

      monitor组件由所有的包括master服务器在内的服务器都会加载,它的主要功能就是与master建立连接进行通信,从而对整个应用服务器群进行管理和监控。master服务器本身也会加载monitor服务器,因为master服务器也会收集其本身自己的监控信息。

      由于应用服务器是在master组件启动后期才创建,因此monitor总是后于master启动。monitor的启动过程与master类似,唯一不同的就是,monitor会发起到master的连接,而不是监听接口。monitor中同样也会使用与master完全相同的方式,加载注册Module,如果有Module配置了周期性地推送监控数据到master的话,即其配置type的值为'push',这里也会调度对应的事件,使得能够周期地推送数据。最后如果有Module定义了start的话,则会回调start。Monitor的启动过程与master基本一致。

      monitor会通过master接受一些命令,比如关闭整个服务器等。对于一些周期性监控的信息,pomelo提供了两种收集方式,即pull方式和push方式。pull方式要求master周期地去与monitor通信,拉取相应的监控信息;push方式,则是由monitor周期地主动地向master报告其监控信息。

      monitor组件无配置项。

      filter

      pomelo内建了常见的一些filter,用户可以通过如下的方式启用:

      app.filter(pomelo.filters.<filterName>(<args>));

      image.gif

      下面介绍一下这几个fitler:

      serial

      这个filter是用来对用户请求做串行化的,可以使得用户的请求只有在第一个请求被处理完后,才会处理第二个请求。serial中使用了一个taskManager,当用户请求到来时,在beforeFilter中,将用户的请求放到taskManager中,taskManager中维护着一个task队列。在对应的afterFilter中,如果taskManager还有未处理的请求,将会处理其请求,即在一个请求的afterFilter里启动在taskManager中还没处理的下一个请求,这样就实现了请求的序列化。

      timeout

      这个filter是用来对服务端处理超时进行警告的,在beforeFilter中会启动一个定时器,在afterFilter中清除。如果在其定时器时间内,afterFilter被调用,定时器将会被清除,因此不会出现超时警告。如果定时器超时时,afterFilter还没有执行到,则会引发超时警告,并记录日志。默认的处理超时是3秒,可以在加载timeout的时候作为参数传入。

      time

      这个filter使用来记录服务器处理时间的,在beforeFilter中会记录一下当前的时间戳,在afterFilter中再次获取当前的时间戳,然后两个时间戳相减,得到整个处理时间,然后记录日志。

      toobusy

      这个filter中,一旦检测到node.js中事件循环的请求等待队列过长,超过一个阀值时,就会触发toobusy。一旦触发了toobusy,那么toobusy的filter中将终止此请求处理链,并在next调用中,传递错误参数。

      connector组件

      connector组件是一个重量级的组件,它会依赖于session组件,server组件,pushScheduler组件和connection组件。connector组件仅仅被前端服务器加载,它主要用来管理客户端的连接。connector组件会加载底层的connector,创建端口监听,绑定事件响应。当有客户端连接请求时,connector组件会请求session组件,获得当前连接的session,如果session组件中没有相应的session的话,session组件会为这个新连接创建新的session,并维护相应的连接;然后connector组件还会向connection组件上报连接信息,供统计使用;最后,将拿到的session以及客户端的请求,一起抛给server组件,由server组件进行请求处理。当server组件处理完请求后,又会通过connector组件将响应返回给客户端。在返回响应给客户端的时候,connector组件做了一个缓存选择,这个缓存实现依赖于pushScheduler组件,也就是说connector组件并不是直接将响应发给客户端,而是将响应给pushScheduler组件。pushScheduler组件根据相应调度策略,可能不缓存直接通过session组件维护的连接,将响应发出去,也可能进行缓存,并按时flush。这是可以配置的。

      connector组件支持如下配置项:

        • connector: 底层使用的通信connector,不配置的话,会默认使用sioconnector;
        • useProtobuf: 目前仅仅支持connector配置使用hybridconnector的情况,配置其为true,将开启消息的protobuf功能;
        • useDict: 目前仅仅支持connector配置使用hybridconnector的情况,配置其为true时,将会开启基于字典的路由消息压缩;
        • useCrypto: 目前仅仅支持connector配置为hybridconnector的情况,配置其为true时,将会启用通信时的数字签名;
        • encode/decode: 消息的编码解码方式,如果不配置的话,将会默认使用connector配置中,底层connector提供的相应的编码解码函数。
        • transports:这个配置选项是用于sioconnector的,因为socket.io的通信方式可能会有多种,如websocket,xhr-polling等等。通过这个配置选项可以选择需要的方式。

        配置connector组件,通过调用如下方式进行:

        app.set('connectorConfig', opts);

        image.gif

        session组件

        session组件跟connector相关,也是仅仅被前端服务器加载,为sessionService提供一个组件包装, 加载session组件后,会在app的上下文中增加sessionService,可以通过app.get('sessionService')获取。它主要用来维护客户端的连接信息,以及生成session并维护session。如果与经典TCP进行类比的话,那么session中维护的连接就可以粗略地认为就是TCP服务器端accept返回的socket句柄。一个连接与一个session对应,同时session组件还维护具体登录用户与session的绑定信息。一个用户可以有多个客户端登录,对应于多个session。当需要给客户端推送消息或者给客户端返回响应的话,必须通过session组件拿到具体的客户端连接来进行。

        session组件支持如下配置项:

          • singleSession: 如果这个配置项配置为true的话,那么将将不允许一个用户同时绑定到多个session,在绑定用户一次后,后面的绑定将会失败。

          配置session组件,通过调用如下方式进行:

          app.set('sessionConfig', opts);

          image.gif

          connection组件

          connection组件是一个功能相对简单的组件,也是仅仅被前端服务器加载,为connectionService提供一个组件包装,他主要进行连接信息的统计,connector组件接收到客户端连接请求以及有客户端离线时,以及用户登录下线等等情况,都会向其汇报。

          connection组件无配置项。

          server组件

          server组件也是一个功能比较复杂的组件,它被除master外的服务器加载。server组件会加载并维护自身的Filter信息和Handler信息。server组件会从connector组件的回调里获得到相应的客户端请求或者通知,然后会使用自己的before filters对其消息进行过滤,再次调用自己的相应Handler进行请求的逻辑处理,然后将响应通过回调的方式发给connector处理。最后调用after filters进行一些清理处理。

          当然,如果客户请求的服务本来就是前端服务器提供的话,会是上面的那种处理流程。如果客户请求的服务是后端服务器提供的服务的话,则将不是上面的那种处理流程,此时会出现sys rpc调用。前面那种前端服务器自己处理的情况具体调用为doHandle,而发起rpc调用的情况则为doForward。这两种处理流程的不同点是,对于自身的请求,调用自己的filter-handler链进行处理,对于不是前端服务器自己提供的服务,则是发起一个sys rpc,然后将rpc调用的结果作为响应,发给connector进行处理。关于这个rpc调用则是pomelo内建的msgRemote实现的。

          对于后端服务器来说,其客户请求不是直接来源于真实的客户端,而是来源于前端服务器对其发起的sys rpc调用,这个rpc调用的实现就是pomelo内建的msgRemote,在msgRemote的实现里,会将来自前端服务器的sys rpc调用请求派发给后端服务器的server组件,然后后端服务器会启用filter-handler链对其进行处理,最后通过rpc调用的返回将具体的响应返回给前端服务器。

          在前端服务器将客户端请求向后端服务器分派时,由于同类型的后端服务器往往有很多,因此需要一个路由策略router,一般情况下用户通过Application.route调用为后端服务器配置router。

          server组件无配置项。

          pushScheduler组件

          pushScheduler组件也是一个功能较为简单的组件,它仅仅被前端服务器加载,与connector组件的关系密切。当connector组件收到server组件的对客户端请求的响应后,connector并不直接将此响应返回给客户端,而是将这个给客户端发送响应的操作调度给scheduler组件。pushScheduler组件完成最后通过session组件拿到具体的客户端连接并将请求的响应发送给客户端的任务。因此,通过pushScheduler组件可以对发给用户的响应进行缓冲,从而提高通信效率。pomelo实现了两种调度策略,一种是不进行任何缓冲,直接将响应发送给客户端,一种是进行缓冲,并定时地将已缓冲的响应发送给对应的客户端。

          pushScheduler配置项:

            • scheduler: scheduler组件的具体调度策略配置,默认的是直接将响应发给客户端,同时pomelo还提供了有缓冲并且定时刷新的调度策略。用户也可以自定义自己的调度策略。

            配置pushScheduler组件,通过调用如下:

            app.set('pushSchedulerConfig', opts);

            image.gif

            如果要启用使用缓冲的scheduler的话,可以在app.js中增加:

            app.set('pushSchedulerConfig', {scheduler: pomelo.pushSchedulers.buffer, flushInterval: 20});

            image.gif

            flushInterval是刷新周期,默认为20毫秒。

            proxy组件

            proxy组件是一个重量级的组件,它被除master外的所有服务器加载。proxy组件会扫描具体应用服务器的目录,抽取其中的remote部分,由于javascript语言的动态性,可以很轻易地获得到remote中的关于远程调用的元信息,生成stub,并将这些调用都挂到app.rpc下面,当用户发起rpc调用时,proxy组件会查看其扫描到的stub信息,以此决定此远程调用是否合法。同时,proxy又会创建一个RpcClient,当发起远程调用时,负责与远端的remote进行通信,并得到远程调用的结果供调用者使用。当进行远程调用时,由于同类型的远程服务器可能有多个,所以这里同样需要配置相应的router。

            proxy的配置项:

              • cacheMsg, 配置cacheMsg为true的话,将开启rpc调用时的对消息的缓冲,而不是直接一旦有rpc请求就发出。
              • interval, 与配置参数cacheMsg配合使用,设置flush缓存的周期
              • mailBoxFactory, rpc底层实现需要的,用户可以定义自己的mailBoxFactory,我们将在rpc原理里面详述。

              另外,可以开启rpc的调用日志,通过如下的调用:

              app.enable('rpcDebugLog');

              image.gif

              配置proxy使用:

              app.set('proxyConfig', opts);

              image.gif

              remote组件

              remote组件是与proxy组件对等的组件,它用来提供rpc调用服务。rpc组件完成对当前服务器的remote的加载,并开启监听端口,等待rpc客户端的连接及相应的rpc调用。当接收到具体的调用请求时,会根据调用请求中描述的调用请求信息,调用相应的remote中的相应方法。然后再将具体的处理结果返回给rpc客户端。rpc服务端还支持对调用请求的filter,也就是说跟server组件处理客户端请求一样,rpc服务端处理具体请求时也会使用filter-remote链进行处理。

              remote组件配置项:

                • cacheMsg, 与proxy组件的含义相同
                • interval, 与proxy组件的含义相同
                • acceptorFactory, rpc底层实现需要的,可以认为跟proxy配置中的mailBoxFactory是对等的,我们将在rpc原理里面详述。

                跟proxy组件一样,用户可以开启rpcDebugLog来得到所有的rpc调用过程的日志。 配置remote组件使用:

                app.set('remoteConfig', opts);

                image.gif

                dictionary组件

                dictionary组件是一个可选组件,不会被默认加载,只有当connector组件的配置中开启了useDict的时候,此组件才会被加载。此组件会遍历所有handler的route字符串,还会从config/dictionary.json中读取客户端的route字符串,然后对这些字符串进行编码,给予每一个路由赋予一个唯一的小整数,实现route信息压缩,当客户端与前端服务器通信时需要路由信息时,将不会再使用很长的那个字符串,而仅仅使用一个小整数。

                dictionary的配置项:

                  • dict, 客户端路由字符串文件的位置,默认使用的是config/dictionary.json 配置dictionary组件使用:
                    app.set('dictionaryConfig', opts);

                  protobuf组件

                  protobuf组件也是一个可选组件,不会被默认加载,只有当connector组件的配置中开启了useProtobuf的时候,此组件才会被加载。此组件会加载对应的proto文件,并完成消息的基于protobuf的编解码。默认的proto文件的配置信息在config/serverProtos.json和config/clientProtos.json中。具体会在详细介绍pomelo-protobuf中详细介绍。

                  protobuf组件无配置项。

                  channel组件

                  channel组件维护channel信息,可以被除了master之外的服务器加载。channel组件可以看作是channelService的组件包装,加载该组件后,会在app上下文中加入channelService,可以通过app.get('channelService')获取。可以认为一个channel就是一个用户的集合,每一个用户大致对应于前端服务器中的一个session,用户可以通过channel组件向一个channel里面的所有用户推送消息。当然,由于后端服务器并不与客户端直接相连,故后端服务器会发起一个sys rpc来表示向客户端推送消息,接受这个远程调用的是pomelo已经实现的ChannelRemote。

                  channel组件的配置项:

                    • broadcastFilter, broadcast的过滤函数。会在执行channel的broadcast的时候,在前端服务器上,在消息发送给每个session之前,进行一个过滤。其函数签名为
                      broadcastFilter(session, msg, filterParam)

                    其中filterParam参数由在channelService的broadcast调用时传入,如下:

                    channelService.broadcast(type, route, {filterParam: param}, cb);

                    image.gif

                    可以通过如下方式对Channel组件进行配置:

                    app.set('channelConfig', opts)

                    image.gif

                    backendSession组件

                    BackendSession组件可以看作是BackendSessionService的组件包装,加载该组件后,会在app的上下文中加入backendSessionService,可以通过app.get('backendSessionService')调用获取。可以被除了master之外的服务器加载。它主要为后端服务器提供BackendSession信息,并通过远程过程调用完成一些比如对原始session绑定uid等操作。

                    backendSession组件无配置项。

                    相关实践学习
                    日志服务之使用Nginx模式采集日志
                    本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
                    相关文章
                    |
                    1月前
                    |
                    弹性计算 固态存储
                    玩转阿里云游戏服务器:阿里云幻兽帕鲁Palworld游戏专属服务器搭建保姆级流程
                    对于热爱《幻兽帕鲁》的玩家们来说,与好友一起联机冒险无疑是游戏的一大乐趣。但如何快速搭建一个专属服务器,让你和朋友轻松“抓帕鲁”呢?本文将为您提供阿里云极简部署幻兽帕鲁专属服务器的指引,让您仅需轻点三次鼠标,3秒轻松开服!
                    |
                    2月前
                    |
                    弹性计算 监控 安全
                    阿里云ECS云服务器推荐配置,选择流程
                    阿里云ECS云服务器推荐配置,选择流程,阿里云服务器配置选择方法包括云服务器类型、CPU内存、操作系统、公网带宽、系统盘存储、网络带宽选择、安全配置、监控等,阿里云百科分享阿里云服务器配置选择方法,选择适合自己的云服务器配置
                    |
                    1月前
                    |
                    弹性计算 Ubuntu Linux
                    新手怎么搭建《幻兽帕鲁Palworld》服务器流程步骤
                    对于《幻兽帕鲁》的忠实粉丝来说,与好友一同在这个开放世界中冒险无疑是最大的乐趣。而搭建一个专属服务器,则能为你们提供更加稳定和自由的游戏体验。那么,如何轻松搭建《幻兽帕鲁》服务器呢?接下来,就让我们一起探索吧!
                    |
                    2月前
                    |
                    弹性计算 数据安全/隐私保护
                    阿里云上怎样搭建幻兽帕鲁Palworld游戏服务器,流程介绍
                    在数字游戏的浪潮中,与好友联机共游已成为一种新风尚。最近备受瞩目的幻兽帕鲁,你是否已经跃跃欲试,想和好友一同探索这片神秘的世界?今天,就为大家带来一篇实用的教程,教你如何轻松搭建属于自己的幻兽帕鲁游戏服务器,与好友畅享云端乐趣。
                    |
                    2月前
                    |
                    弹性计算 搜索推荐
                    「玩转幻兽帕鲁/Palworld」阿里云自建Palworld/幻兽帕鲁服务器全流程攻略
                    随着《幻兽帕鲁》这款开放世界生存游戏的热度不断上升,越来越多的玩家开始追求更加自由和个性化的游戏体验。搭建自己的专属服务器,无疑是实现这一目标的最佳选择。今天,就让我们一起来了解如何轻松搭建《幻兽帕鲁》服务器,与好友共同开启精彩刺激的联机游戏吧!
                    48 6
                    |
                    20天前
                    |
                    开发框架 JavaScript 中间件
                    node+express搭建服务器环境
                    node+express搭建服务器环境
                    node+express搭建服务器环境
                    |
                    1月前
                    |
                    弹性计算 Ubuntu 搜索推荐
                    幻兽帕鲁Palworld搭建联机服务器教程,新手搭建流程步骤
                    随着《幻兽帕鲁》这款开放世界生存游戏的持续火热,越来越多的玩家希望搭建属于自己的游戏服务器,与好友一同探险。那么,怎样才能轻松搭建《幻兽帕鲁》服务器呢?接下来,就跟随我们的脚步,简单几步助你实现梦想!
                    224 0
                    |
                    10天前
                    |
                    弹性计算 Java Linux
                    阿里云服务器搭建部署宝塔详细流程
                    该内容是一个阿里云服务器和域名的配置指南。首先,需注册阿里云账号并进行企业实名认证。接着,选购服务器如2核2G1兆的Linux系统,并购买域名。完成域名备案后,进行域名解析和ICP备案。然后,通过远程连接登录服务器,重置密码,安装宝塔面板。在安全组中开启宝塔面板随机生成的端口。最后,登录宝塔面板安装LNMP环境,配置数据库如MySQL和Redis,部署JDK、Tomcat,上传前端和后端项目以实现上线。
                    |
                    11天前
                    |
                    弹性计算 JavaScript Java
                    阿里云服务器搭建部署宝塔详细流程
                    以下是内容的摘要: 本文主要介绍了在阿里云上创建和配置服务器环境的步骤,包括注册阿里云账号、实名认证、购买和设置服务器、域名的获取与备案、以及使用宝塔面板安装和配置环境。首先,用户需要注册阿里云账号并进行实名认证,选择合适的服务器配置。接着,购买服务器后,要准备并备案域名,以便通过友好的网址访问网站。在服务器上安装宝塔面板,可以方便地管理和配置LAMP/LNMP/Tomcat/Node.js等应用环境。完成这些步骤后,用户还需要在宝塔面板中安装MySQL、Redis等数据库,部署Java或Vue项目,并配置相关端口。最后,将前端项目打包上传至服务器,并设置站点,即可实现网站的上线。
                    |
                    11天前
                    |
                    人工智能 安全 云计算
                    阿里云服务器购买之后发票如何申请?申请发票流程及常见问题参考
                    申请发票是很多用户尤其是企业级用户在购买完阿里云服务器之后非常关注的问题,对于初次购买阿里云服务器的用户来说,往往并不清楚如何找阿里云申请发票,本文以图文形式为大家介绍阿里云服务器购买完成之后申请发票的详细流程以及常见问题。
                    阿里云服务器购买之后发票如何申请?申请发票流程及常见问题参考

                    热门文章

                    最新文章