(十二)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}
}

 

 配置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}]}
}

 

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

  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"
}]

解决服务器分配问题

 从上面的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];
};

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);
};

  准备好这些文件后,在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);

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

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 configuresapp.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 configuresapp.filter(pomelo.timeout());
});// start app
app.start();process.on('uncaughtException', function(err) {console.error(' Caught exception: ' + err.stack);
});

实现 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 connectorsvar connectors = this.app.getServersByType('connector');if(!connectors || connectors.length === 0) {next(null, {code: 500});return;}// select connectorvar res = dispatcher.dispatch(uid, connectors);next(null, {code: 200,host: res.host,port: res.clientPort});
};

其中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 + '*' + ridvar sessionService = self.app.get('sessionService');//duplicate log inif( !! 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 channelself.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);
};

  这里完成的主要就是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 channelif( !! channel) {channel.leave(uid, sid);}var username = uid.split('*')[0];var param = {route: 'onLeave',user: username};channel.pushMessage(param);cb();
};

 可以看到上面代码中的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 usersif(msg.target == '*') {channel.pushMessage(param);}//the target is specific userelse {var tuid = msg.target + '*' + rid;var tsid = channel.getMember(tuid)['sid'];channelService.pushMessageByUids(param, [{uid: tuid,sid: tsid}]);}next(null, {route: msg.route});
};

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

 

10.运行

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

cd game-server目录
pomelo start

进入webserver 执行node app

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

登录用户信息:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444536.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(十三)nodejs循序渐进-高性能游戏服务器框架pomelo之扩展聊天服务器为机器人自动聊天

聊天服务器扩展 大家在上一篇文章里相信已经学会了pomelo框架的基本用法了&#xff0c;那么我们在上一篇文章的代码基础上继续扩展&#xff0c;丰富系统&#xff0c;另外也熟悉下他的更多的用法&#xff0c;这一节我将扩展它&#xff1a;增加一个机器人自动聊天的功能。 目的…

leetcode1290. 二进制链表转整数 刷新认知,最简单算法题

给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。 请你返回该链表所表示数字的 十进制值 。 示例 1&#xff1a; 输入&#xff1a;head [1,0,1] 输出&#xff1a;5 解释&#xff1a;二进制数 (101) 转化为十进…

Redis:02---安装Redis(Linux+Windows+Docker)

Linux安装&#xff1a;一、安装方式1&#xff08;下载源码编译安装&#xff09;第一步&#xff1a;从下面的网址中下载Redis最新稳定版本的源代码sudo wget http://download.redis.io/redis-stable.tar.gz第二步&#xff1a;下载完之后解压&#xff0c;建立一个软链接指向于red…

C++: 06---构造函数析构函数

拷贝构造函数: 用一个已经存在的对象来生成一个相同类型的新对象。(浅拷贝)默认的拷贝构造函数: 如果自定义了拷贝构造函数,编译器就不在生成默认的拷贝构造函数。 如果没有自定义拷贝构造函数,但在代码中用到了拷贝构造函数,编译器会生成默认…

C++:11---友元函数、友元类

一、友元(friend) 概念:通过友元,打破了类的封装性,可以访问类内的所有成员分类:友元函数、友元类二、友元函数 概念:友元函数是一个普通函数,不属于类,但需要在类内表明友元关系 友元函数可访问类内所有成员,但类不可以访问友元函数…

C++:12---运算符重载

一、概念 对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型重载的运算符是具有特殊名字的函数,该函数也有返回值、参数列表、函数体二、运算符重载的3种实现方式 成员函数:私有、公有、保护都可以友元函数:同上全局函数:只能访问公有的三、运算符重载的…

Redis:03---Redis的启动与配置参数大全

一、Redis的可执行文件当我们安装完Redis之后&#xff0c;src和/usr/local/bin目录下提供了下面这些可执行程序&#xff0c;我们称之为Redis Shell&#xff1a;redis-serverRedis服务器redis-cliRedis命令行客户端redis-benchmarkRedis性能测试工具redis-check-aofRedis AOF持久…

leetcode80. 删除排序数组中的重复项 II

给定一个排序数组&#xff0c;你需要在原地删除重复出现的元素&#xff0c;使得每个元素最多出现两次&#xff0c;返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。 示例 1: 给定 nums [1,1,1,2…

Redis:04---键的基本命令(上)

一、KEYS&#xff1a;全量遍历键KEYS pattern功能&#xff1a;用来获取此数据库中所有的键名注意事项&#xff1a;KEYS命令需要遍历Redis中的所有键&#xff0c;当键的数量较多时会影响性能&#xff0c;不建议在生产环境下使用支持glob风格通配符格式&#xff0c;见下表&#x…

(十三) 深入浅出TCPIP之setsockopt参数详解

在socket编程中我们会经常用到setsockopt这个函数&#xff0c;那么本节我们将对这个函数的参数和使用做说明&#xff1a; 首先看下函数原型&#xff1a; int setsockopt( int socket, int level, int option_name,const void *option_value, size_t &#xff0c;ption_len); 第…

Redis:05---键的基本命令(下) 生存周期

一、设置键生存/过期时间生存时间&#xff08;Time To Live&#xff0c;TTL&#xff09;&#xff1a;在经过指定的秒数或者毫秒数之后&#xff0c;服务器就会自动删除生存时间为0的键过期时间&#xff08;expire time&#xff09;&#xff1a;是一个UNIX时间戳&#xff0c;当键…

C++:13---多态和虚函数表

多态的意思为“以一个public基类的指针/引用,寻址一个派生类对象”。 “多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。这是如何实现的呢?请看下面的程序,该程序演示了多态类对象存储空间的大小。 #in…

leetcode96. 不同的二叉搜索树 动归vs数学?

给定一个整数 n&#xff0c;求以 1 ... n 为节点组成的二叉搜索树有多少种&#xff1f; 示例: 输入: 3 输出: 5 解释: 给定 n 3, 一共有 5 种不同结构的二叉搜索树: 1 3 3 2 1 \ / / / \ \ 3 2 1 1 3 …

Redis:06---数据库管理

一、服务器中的数据库Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中&#xff0c;db数组的每个项都是一个redis.h/redisDb结构&#xff0c;每个redisDb结构代表一个数据库&#xff1a;struct redisServer {// ...redisDb *db; // 一个数组&#…

在同一局域网下连接共享文件夹失败,提示:你不能访问共享文件夹,因为你组织的安全策略阻止未经身份验证的来宾访问

1.尝试打开guest访问。 &#xff08;1&#xff09;使用键盘 win R 键&#xff0c;打开运行窗口&#xff0c;并输入 gpedit.msc 打开本地组策略编辑器窗口 &#xff08;2&#xff09;选择计算机配置------->管理模板-------->网络-------->Lanman工作站。 &#…

(十五)深入浅出TCPIP之Hello CDN

什么是CDNCDN 其实是 Content Delivery Network 的缩写&#xff0c;即“内容分发网络”。CDN是将媒体资源&#xff0c;动静态图片(Flash) &#xff0c;HTML, CSS, JS等等内容缓存到距离你更近的互联网数据中心&#xff0c;从而让用户进行共享资源&#xff0c;实现缩减站点间的响…

Redis:07---Redis数据结构

一、五大数据结构Redis可以存储键与5种不同数据结构类型之间的映射&#xff0c;这5种数据结构类型分别为&#xff1a;STRING&#xff1a;字符串LIST&#xff1a;列表SET&#xff1a;集合HASH&#xff1a;散列ZSET&#xff1a;有序集合TYPE命令用来获得键的数据类型&#xff0c;…

C++:14---虚继承,虚函数,多态

一、多级混合继承 下面先介绍菱形继承 //菱形继承 class A { public: int data; }; class B:public A { public: int data; }; class C:public A { public: int data; }; class D:public B,public C { public: int data; };int main() { D c; D.data=1; D.B::data=2;//访问B中的…

如何使得客户端和服务器端完美配合做IOS应用内付费

配置Developer.apple.com 登录到Developer.apple.com,然后进行以下步骤: 为应用建立建立一个不带通配符的App ID用该App ID生成和安装相应的Provisioning Profile文件。配置iTunes Connect 登录到iTunes Connet,然后进行以下步骤: 用该App ID创建一个新的应用。在该应用中…

IOS内购流程从0-1手把手教会

苹果掌握着可能是全球最重要的APP分发渠道,然而30%的抽成近年来也被人批评,现在苹果似乎也看到反对意见了,从2021年1月1日开始,部分小型企业的分成费用降低到15%。 据报道,苹果将于2021年1月1日启动App Store小企业项目,会降低他们的抽成费用。针对年收入不足100万美元的…