nodejs循序渐进-高性能游戏服务器框架pomelo之创建一个游戏聊天服务器

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

       上个章节我们简单介绍了下pomelo的安装和目录结构,有读者可能觉得有点吃不消,为什么不再深入讲一讲目录结构和里边的库,这里我就不费口舌了,大家可以去官网参考文档说明,本文只告诉大家如何利用这个框架来开发自己的东西。

随着文章的后续不断推进,我相信大家会越来越熟悉pomelo,对猪场框架的使用也会越来越得心用手。

为什么是聊天服务器?

我们目标是搭建游戏服务器,为什么从聊天开始呢?

聊天可认为是简化的实时游戏,它与游戏服务器有着很多共通之处,如实时性、频道、广播等。由于游戏在场景管理、客户端动画等方面有一定的复杂性,并不适合作为 pomelo 的入门应用。聊天应用通常是 Node.js 入门接触的第一个应用,因此更适合做入门教程。

一个聊天系统我们设计思路是客户端连接gate网关服务器,由gate网关服务器根据玩家的uid的crc32的校验码与connector服务器的个数取余,从而得到一个connector服务器,把这个connector服务器分配给请求用户,那么客户端就可以通过此connector服务器建立连接,而和connector服务器保持连接的是chat逻辑服务器,所有的逻辑处理交给connector发起remote的RPC调用。

 

新建gate和chat服务器

在app/servers目录下新建gate和chat服务器。

gate服务器:

在一般情况下用户量一台机器就可以支撑,但用户量多了就得横向扩充服务器(在gate服务器之前通过nginx反向代理做端口转发,相关文章可以参考我之前的 nginx+apache专栏),gate服务器的作用就相当于前端负载均衡服务器;

客户端向gate服务器发出请求,gate服务器会给客户端分配一个connector服务器;

分配策略是根据客户端的某一个key做hash得到connector的id,这样就可以实现各个connector服务器的负载均衡。

connector服务器:

接受客户端请求,并将其路由到chat服务器,以及维护客户端的链接;

同时,接收客户端对后端服务器的请求,按照用户配置的路由策略,将请求路由给具体的后端服务器。当后端服务器处理完请求或者需要给客户端推送消息的时候,connector服务器同样会扮演一个中间角色,完成对客户端的消息发送;

connector服务器会同时拥有clientPort和port,其中clientPort用来监听客户端的连接,port端口用来给后端提供服务;

chat服务器:

handler和remote决定了服务器的行为;

handler接收用户发送过来的send请求,remote由connector RPC发起远程调用时调用;

在remote里由于涉及到用户的加入和退出,所以会有对channel的操作。

配置master.json

在config下打开master.json

{
  "development": {
    "id": "master-server-1", "host": "127.0.0.1", "port": 3005
  },
  "production": {
    "id": "master-server-1", "host": "127.0.0.1", "port": 3005
  }
}

image.gif

配置servers.json

 打开config目录下servers.json文件,配置好各种 type 的服务器,配置如下

{
    "development":{
        "connector":[
             {"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true},
             {"id":"connector-server-2", "host":"127.0.0.1", "port":4051, "clientPort": 3051, "frontend": true},
             {"id":"connector-server-3", "host":"127.0.0.1", "port":4052, "clientPort": 3052, "frontend": true}
         ],
        "chat":[
             {"id":"chat-server-1", "host":"127.0.0.1", "port":6050},
             {"id":"chat-server-2", "host":"127.0.0.1", "port":6051},
             {"id":"chat-server-3", "host":"127.0.0.1", "port":6052}
        ],
        "gate":[
           {"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}
        ]
    },
    "production":{
           "connector":[
             {"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true},
             {"id":"connector-server-2", "host":"127.0.0.1", "port":4051, "clientPort": 3051, "frontend": true},
             {"id":"connector-server-3", "host":"127.0.0.1", "port":4052, "clientPort": 3052, "frontend": true}
         ],
        "chat":[
             {"id":"chat-server-1", "host":"127.0.0.1", "port":6050},
             {"id":"chat-server-2", "host":"127.0.0.1", "port":6051},
             {"id":"chat-server-3", "host":"127.0.0.1", "port":6052}
        ],
        "gate":[
           {"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}
        ]
  }
}

image.gif

 解释一下配置中的各字段:

 id:  字符串类型的应用服务器ID

 host:应用服务器的IP或者域名

 port:RPC请求监听的端口

 clientPort: 前端服务器的客户端请求的监听端口

 frontend:bool类型,是否是前端服务器,默认: false

 可选参数:

 max-connections:前端服务器最大客户连接数

 args:node/v8配置,如配置为"args": "--debug=5858 "这样就可以启用项目调试(没用过,临时问了一下谷歌,看别人是这么解释的^_^!)

配置adminServer.json

打开config目录下adminServer.json文件,配置好各种 type 的服务器,adminServer.json的使用是让指定type的服务器通过token去向master注册。

master是框架组件,在poemlo.start()时首先被启动,然后由它负责启动其他的组件,包括系统组件和servers.json 中的用户配置组件。

servers.json 中的组件被启动,要向master注册报告自己已经启动了。

在报告的时候需要通过consoleService的authserver的token进行验证。

这个authserver的token就在adminServer.json中

若是没有对对应的type配置对应的token,那么这个服务器就无法注册到master。

这个功能在 node_modules/pomelo/node_modules/pomelo-admin中实现。

因此,你server.json中有几种服务器,那么就得在adminserver.json中配置对应的type与token.

进入config/adminServer.json

[{
    "type": "connector",
    "token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}, {
    "type": "chat",
    "token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},{
    "type": "gate",
    "token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}]

image.gif

解决服务器分配问题

从上面的servers.json配置的修改可以看出与最开始创建出来的项目一个服务器相比,connector和chat我都配置了三个服务器

这就要解决客户端请求服务器分配问题

解决思路:用户访问gate服务器,使用用户的uid的crc32的校验码与connector服务器的个数取余,从而得到一个connector服务器,把这个connector服务器分配给请求用户

在app目录下新建util目录,目录下新建“dispatcher.js”和 “routeUtil.js”文件,处理此服务器分配逻辑

dispatcher.js

var crc = require('crc');
module.exports.dispatch = function(uid, connectors) {
  var index = Math.abs(crc.crc32(uid)) % connectors.length;
  return connectors[index];
};

image.gif

routeUtil.js

var dispatcher = require('./dispatcher');
module.exports.chat = function(session, msg, app, cb) {
  var chatServers = app.getServersByType('chat');
  if(!chatServers || chatServers.length === 0) {
    cb(new Error('can not find chat servers.'));
    return;
  }
  var res = dispatcher.dispatch(session.get('rid'), chatServers);
  cb(null, res.id);
};

image.gif

 准备好这些文件后,在game-server服务器入口文件app.js中添加配配置:

注意:

  app.configure('production|development', 'connector', function(){  

  修改为

  app.configure('production|development',  function(){

  这个如果不修改,在启动调用时会遇到 engine.io 中报错  TypeError: Cannot read property  'indexOf' of undefined  at Server.verify .

这里我们使用的connector是sioconnector(支持socket.io)

这里我有必要说明下route的API:

API 说明
route(serverType, routeFunc)

 Application.route(); serverType:服务类型;routeFunc:路由功能函数,如:routeFunc(session, msg, app, cb)

未指定的服务类型设置路由功能。如:

app.route('area', routeFunc);

var routeFunc = function(session, msg, app, cb) {

  // all request to area would be route to the first area server

  var areas = app.getServersByType('area');

  cb(null, areas[0].id);

}

另外注意的是transports这个参数:这个配置选项是用于sioconnector的,因为socket.io的通信方式可能会有多种,如websocket,xhr-polling等等。通过这个配置选项可以选择需要的方式。

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

app.set('connectorConfig', opts);

image.gif

知道了以上基础知识,那么我们直接上代码“:

var pomelo = require('pomelo');
var routeUtil = require('./app/util/routeUtil');
/**
 * Init app for client.
 */
var app = pomelo.createApp();
app.set('name', 'demoserver');
// app configure
app.configure('production|development', function() {
  // route configures
  app.route('chat', routeUtil.chat);
  app.set('connectorConfig', {
    connector: pomelo.connectors.sioconnector,
    // 'websocket', 'polling-xhr', 'polling-jsonp', 'polling'
    transports: ['websocket', 'polling'],
    heartbeats: true,
    closeTimeout: 60 * 1000,
    heartbeatTimeout: 60 * 1000,
    heartbeatInterval: 25 * 1000
  });
  // filter configures
  app.filter(pomelo.timeout());
});
// start app
app.start();
process.on('uncaughtException', function(err) {
  console.error(' Caught exception: ' + err.stack);
});

image.gif

实现 gate.gateHandler

 作用:用户连接gate服务器,返回分配的connector

 在gate目录下handler下新建gateHandler.js,代码如下

var dispatcher = require('../../../util/dispatcher');
module.exports = function(app) {
  return new Handler(app);
};
var Handler = function(app) {
  this.app = app;
};
var handler = Handler.prototype;
/**
 * Gate handler that dispatch user to connectors.
 *
 * @param {Object} msg message from client
 * @param {Object} session
 * @param {Function} next next stemp callback
 *
 */
handler.queryEntry = function(msg, session, next) {
  var uid = msg.uid;
  if(!uid) {
    next(null, {
      code: 500
    });
    return;
  }
  // get all connectors
  var connectors = this.app.getServersByType('connector');
  if(!connectors || connectors.length === 0) {
    next(null, {
      code: 500
    });
    return;
  }
  // select connector
  var res = dispatcher.dispatch(uid, connectors);
  next(null, {
    code: 200,
    host: res.host,
    port: res.clientPort
  });
};

image.gif

其中handler.queryEntry是到时候客户端请求访问的路由接口,如果读者对此处有疑惑的可以阅读我上一篇文章里的架构部分,有专门对route路由规则介绍的。

实现connector中entryHandler.js

 主要完成接受客户端的请求,维护与客户端的连接,路由客户端的请求到chat服务器;

在原有的entryHandler.js基础上做修改,加入enter进入聊天室的接口.

这里我要说明下:

在chat服务器里创建了channel, 这个channel只有在chat服务器里能用.

别的服务器里, 获取不到chat服务器的channel, 不过你可以通过RPC方式调用chat服务器的方式来获取其属性。

module.exports = function(app) {
  return new Handler(app);
};
var Handler = function(app) {
  this.app = app;
}; 
/**
 * New client entry chat server.
 *
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next stemp callback
 * @return {Void}
 */
Handler.prototype.enter = function(msg, session, next) {
  var self = this;
  var rid = msg.rid;
  var uid = msg.username + '*' + rid
  var sessionService = self.app.get('sessionService');
  //duplicate log in
  if( !! sessionService.getByUid(uid)) {
    next(null, {
      code: 500,
      error: true
    });
    return;
  }
  session.bind(uid);
  session.set('rid', rid);
  session.push('rid', function(err) {
    if(err) {
      console.error('set rid for session service failed! error is : %j', err.stack);
    }
  });
  session.on('closed', onUserLeave.bind(null, self.app));
  //put user into channel
  self.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, true, function(users){
    next(null, {
      users:users
    });
  });
};
/**
 * User log out handler
 *
 * @param {Object} app current application
 * @param {Object} session current session object
 *
 */
var onUserLeave = function(app, session) {
  if(!session || !session.uid) {
    return;
  }
  app.rpc.chat.chatRemote.kick(session, session.uid, app.get('serverId'), session.get('rid'), null);
};
/**
 * New client entry.
 *
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next step callback
 * @return {Void}
 */
Handler.prototype.entry = function(msg, session, next) {
  next(null, {code: 200, msg: 'game server is ok.'});
};
/**
 * Publish route for mqtt connector.
 *
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next step callback
 * @return {Void}
 */
Handler.prototype.publish = function(msg, session, next) {
  var result = {
    topic: 'publish',
    payload: JSON.stringify({code: 200, msg: 'publish message is ok.'})
  };
  next(null, result);
};
/**
 * Subscribe route for mqtt connector.
 *
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next step callback
 * @return {Void}
 */
Handler.prototype.subscribe = function(msg, session, next) {
  var result = {
    topic: 'subscribe',
    payload: JSON.stringify({code: 200, msg: 'subscribe message is ok.'})
  };
  next(null, result);
};

image.gif

 这里完成的主要就是RPC远程调用chat服务器chatRemote中的实现(通过app.rpc.chat.chatRemote来拉起RPC调用,我们将在下面实现chat服务器的remote接口)。

这里我有必要说下为什么要用rpc方式,而不是用socket.io 方式或者websocket方式:

比如我们游戏的角色信息是作为一个redis缓存对方存放的,有一个不好的地方就是,如果别的地方调用该玩家的信息并进行修改,就可能会出现两处数据修改,结果却只有一处能够修改成功。

例如:A接口,B接口。都会获取charInfo并对charInfo 进行修改。

先调用A接口, 在A接口处理逻辑的过程中,调用了B接口。

这时候A,B获取到的charInfo是一样的,但是,修改的属性值可能不一样,在redis设置缓存的时候,只能是哪个最后设置的,charInfo 修改的属性值才会生效。比如A修改exp属性,B修改gold属性。

A接口先设置缓存,B后设置。这时候,只有gold属性值才会被真正的修改。因为在B获取到的charInfo里没有修改charInfo.exp. 说白了,就是不是修改的同一个对象。

所以这次我们将角色信息专门定义了一个char对象,提供一个get和一个set接口。这样,不论是那个地方对charInfo 的修改,都是针对同一个对象的修改。

char对象定义放在data服务器,其他服务器例如chat服务器,要获取charInfo ,就需要rpc调用data服务器的get方法获取charInfo. 如果在chat服务器里有对charInfo进行修改,则一定要rpc调用data服务器的set方法,重新设置charInfo. 如果data是单个的服务器就没有必要。不过一般至少有三个data服务器。

玩家在登陆的时候,分配一个data服务器给当前玩家。该玩家的信息就保存在这个data服务器的session里。其他data服务器不会有。这也是为什么其他非data服务器对charInfo 修改一定要远程rpc set一下. 因为在其他非data服务器下对charInfo 的修改,都修改的不是一个不同的对象了。一个服务器一个session,一个charInfo保存在一个服务器的session, 这样就好理解点了。

实现chat服务器chatRemote.js

chat服务器会接受connector的远程调用,完成channel维护中的用户的加入以及离开。

这里大家需要再次了解这两个模块的API ChannelServiceChannel

ChannelService

    创建和维护本地服务的信道。

API 说明
createChannel(name) ChannelService.prototype.createChannel() 根据信道名称创建信道,如果该信道已存在则返回已存在的信道
getChannel(name,create) ChannelService.prototype.getChannel() name:信道名称,create:如果为true,并且信道不存在时,则创建新的信道。根据信道名称获取信道
destroyChannel(name) ChannelService.prototype.destroyChannel() 根据信道名称,删除信道
pushMessageByUids(route, msg, uids, cb) ChannelService.prototype.pushMessageByUids() route:消息路由;msg:发送到客户端的消息;uids:接收消息的客户端列表,格式 [{uid: userId, sid: frontendServerId}];cb:回调函数 cb(err)。根据uids将消息推送给客户端,如果uids中的sid未指定,则忽略相应的客户端
broadcast(stype,route, msg, opts, cb) ChannelService.prototype.broadcast() stype:前端服务的类型;route:路由;msg:消息;opts:广播参数;cb:回调函数。广播消息到所有连接的客户端。

Channel

API 说明
add(uid,sid) Channel.prototype.add() uid:用户编号;sid:用户连接到的前端服务id。添加指定用户到信道。
leave(uid,sid) Channel.prototype.leave() uid:用户编号;sid:用户连接到的前端服务id。从信道中移除用户。
getMembers() Channel.prototype.getMembers() 获得信道中的成员
getMember(uid) Channel.prototype.getMember() 根据uid获取成员信息
destroy() Channel.prototype.destroy() 销毁信道
pushMessage(route,msg,cb) Channel.prototype.pushMessage()  route:消息路由,msg:要推送的消息,cb:回调函数。将消息推送给信道的所有成员。
module.exports = function(app) {
  return new ChatRemote(app);
};
var ChatRemote = function(app) {
  this.app = app;
  this.channelService = app.get('channelService');
};
/**
 * Add user into chat channel.
 *
 * @param {String} uid unique id for user
 * @param {String} sid server id
 * @param {String} name channel name
 * @param {boolean} flag channel parameter
 *
 */
ChatRemote.prototype.add = function(uid, sid, name, flag, cb) {
  var channel = this.channelService.getChannel(name, flag);
  var username = uid.split('*')[0];
  var param = {
    route: 'onAdd',
    user: username
  };
  channel.pushMessage(param);
  if( !! channel) {
    channel.add(uid, sid);
  }
  cb(this.get(name, flag));
};
/**
 * Get user from chat channel.
 *
 * @param {Object} opts parameters for request
 * @param {String} name channel name
 * @param {boolean} flag channel parameter
 * @return {Array} users uids in channel
 *
 */
ChatRemote.prototype.get = function(name, flag) {
  var users = [];
  var channel = this.channelService.getChannel(name, flag);
  if( !! channel) {
    users = channel.getMembers();
  }
  for(var i = 0; i < users.length; i++) {
    users[i] = users[i].split('*')[0];
  }
  return users;
};
/**
 * Kick user out chat channel.
 *
 * @param {String} uid unique id for user
 * @param {String} sid server id
 * @param {String} name channel name
 *
 */
ChatRemote.prototype.kick = function(uid, sid, name, cb) {
  var channel = this.channelService.getChannel(name, false);
  // leave channel
  if( !! channel) {
    channel.leave(uid, sid);
  }
  var username = uid.split('*')[0];
  var param = {
    route: 'onLeave',
    user: username
  };
  channel.pushMessage(param);
  cb();
};

image.gif

可以看到上面代码中的add和kick分别对应着加入和离开channel

实现chat服务器chatHandler.js

chat服务器执行聊天逻辑,维护channel信息,一个房间就是一个channel,一个channel里有多个用户,当有用户发起聊天的时候,就会将其内容广播到整个channel。

var chatRemote = require('../remote/chatRemote');
module.exports = function(app) {
  return new Handler(app);
};
var Handler = function(app) {
  this.app = app;
};
var handler = Handler.prototype;
/**
 * Send messages to users
 *
 * @param {Object} msg message from client
 * @param {Object} session
 * @param  {Function} next next stemp callback
 *
 */
handler.send = function(msg, session, next) {
  var rid = session.get('rid');
  var username = session.uid.split('*')[0];
  var channelService = this.app.get('channelService');
  var param = {
    route: 'onChat',
    msg: msg.content,
    from: username,
    target: msg.target
  };
  channel = channelService.getChannel(rid, false);
  //the target is all users
  if(msg.target == '*') {
    channel.pushMessage(param);
  }
  //the target is specific user
  else {
    var tuid = msg.target + '*' + rid;
    var tsid = channel.getMember(tuid)['sid'];
    channelService.pushMessageByUids(param, [{
      uid: tuid,
      sid: tsid
    }]);
  }
  next(null, {
    route: msg.route
  });
};

image.gif

这里面是发送消息(给房间内所有人和指定用户)

10.运行

 到此这个聊天服务器实现就完成, 打开命令行工具,执行没有错误信息,基本就成功了!

cd game-server目录
pomelo start

image.gif

image.gif编辑

进入webserver 执行node app

image.gif编辑

浏览器输入http://localhost:3001/

image.gif编辑

登录用户信息:

image.gif编辑

登录其他用户,互发消息:

image.gif编辑

源码资源可通过 此链接 下载(目前仅限关注的粉丝下载)

相关文章
|
1月前
|
弹性计算 固态存储
玩转阿里云游戏服务器:阿里云幻兽帕鲁Palworld游戏专属服务器搭建保姆级流程
对于热爱《幻兽帕鲁》的玩家们来说,与好友一起联机冒险无疑是游戏的一大乐趣。但如何快速搭建一个专属服务器,让你和朋友轻松“抓帕鲁”呢?本文将为您提供阿里云极简部署幻兽帕鲁专属服务器的指引,让您仅需轻点三次鼠标,3秒轻松开服!
|
24天前
|
存储 缓存 NoSQL
Redis 服务器指南:高性能内存数据库的完整使用指南
Redis 服务器指南:高性能内存数据库的完整使用指南
|
1月前
|
监控 JavaScript 安全
监控内网电脑软件设计与实现:基于Node.js的服务器端架构分析
在当今信息技术高度发达的时代,监控内网电脑的需求日益增长。企业需要确保网络安全,个人用户也需要监控家庭网络以保护隐私和安全。本文将介绍一种基于Node.js的服务器端架构,用于设计和实现监控内网电脑软件。
105 0
|
1月前
|
弹性计算 运维 安全
畅享幻兽帕鲁:快速搭建稳定服务器,与小伙伴畅游游戏世界!
畅享幻兽帕鲁:快速搭建稳定服务器,与小伙伴畅游游戏世界!
515 0
|
1月前
|
弹性计算 Ubuntu Windows
阿里云自建《幻兽帕鲁Palworld》多人游戏专属服务器,搭建方法分享
对于《幻兽帕鲁》的忠实粉丝来说,与好友一同在游戏中探险、生存无疑增加了更多的乐趣。而为了实现这一愿望,搭建一个专属的多人游戏服务器就显得尤为重要。今天,我将为大家带来一篇极简教程,教您如何在三次点击内,轻松搭建《幻兽帕鲁》的专属服务器。
|
1月前
|
弹性计算 Ubuntu Windows
阿里云幻兽帕鲁Palworld搭建游戏联机服务器教程,3秒开服
对于《幻兽帕鲁》这款开放世界生存制作游戏的狂热粉丝来说,能够拥有自己的游戏服务器无疑是一个巨大的诱惑。这款游戏由Pocketpair开发,于2024年1月18日发行了抢先体验版本,其广阔的游戏世界和多样的玩法——收集神奇的生物"帕鲁",派遣它们进行战斗、建造、农活和工业生产等——已经吸引了无数玩家。今天,我将为大家详细介绍,如何在阿里云服务器上以最简单的方式一键部署幻兽帕鲁联机服务器。
|
1月前
|
弹性计算 Linux Windows
阿里云游戏服务器部署攻略,阿里云《幻兽帕鲁》服务器怎么搭建?
《幻兽帕鲁》无疑是近期游戏圈内的热议焦点。这款由Pocketpair倾力打造的开放世界生存制作游戏,自2024年1月18日发布抢先体验版本以来,便吸引了无数玩家的目光。今天,我们就来详细讲解一下,如何利用阿里云服务器,轻松搭建《幻兽帕鲁》联机服务器,让你和小伙伴们在这款神奇的游戏中畅享无限乐趣!
|
1月前
|
弹性计算
2024年阿里云轻松创建《幻兽帕鲁Palworld》多人游戏专属服务器教程
对于热爱《幻兽帕鲁》的玩家们来说,与好友共同探险、挑战,无疑是游戏中的一大乐趣。但如何快速搭建一个稳定的专属服务器,却常常让玩家们感到头疼。别担心,阿里云为您提供了极简的解决方案,让您在三次点击之内,3秒内轻松开服!
29 0
|
1月前
|
弹性计算 Linux Windows
阿里云幻兽帕鲁Palworld多人游戏专属服务器搭建教学,小白专属
随着幻兽帕鲁/Palworld的风靡,越来越多的小伙伴渴望搭建属于自己的游戏服务器,与好友共同探索这个神秘而奇幻的世界。今天,就为大家带来一篇简单易懂的服务器搭建教学,让你轻松成为服务器主人!首先,我们要感谢阿里云提供的便捷服务。只需10秒,你就能在阿里云上自动开设属于自己的幻兽帕鲁服务器。现在,就跟着我一起操作吧!
|
1月前
|
Ubuntu JavaScript 关系型数据库
在阿里云Ubuntu 20.04服务器中搭建一个 Ghost 博客
在阿里云Ubuntu 20.04服务器上部署Ghost博客的步骤包括创建新用户、安装Nginx、MySQL和Node.js 18.x。首先,通过`adduser`命令创建非root用户,然后安装Nginx和MySQL。接着,设置Node.js环境,下载Nodesource GPG密钥并安装Node.js 18.x。之后,使用`npm`安装Ghost-CLI,创建Ghost安装目录并进行安装。配置过程中需提供博客URL、数据库连接信息等。最后,测试访问前台首页和后台管理页面。确保DNS设置正确,并根据提示完成Ghost博客的配置。
在阿里云Ubuntu 20.04服务器中搭建一个 Ghost 博客

热门文章

最新文章