CBrother从2.3.1版本开始提供了一个Http后台框架,方便开发者进行http后台接口开发。但使用前需要开发者先了解Http模块的用法。
HttpEasy简介
当今的web开发,前后端分离已经成为主流趋势,因为服务器除了要支持浏览器外,经常还要支持手机端,各家APP小程序端(如微信支付宝等),电脑PC端等,因此后台开发者使用的各类MVC框架中,也只使用到了数据模型M和用户控制器C, 视图V一直都在淡化。
故,HttpEasy直接割舍了视图V的存在,简化框架层次,只实现了用户控制和数据模型,并在数据模型上支持海量数据多线程分割管理以及跨线程调用,使开发者并不需要很深厚的编程功底,就可以开发出支持高并发的后台接口。
HttpEasy术语
HttpEasy中用户控制层,即接收前端请求的处理,称为:Action。 如http://x.x.x.x/login对应一个Action,http://x.x.x.x/logout对应另一个Action
HttpEasy中数据模型层,即处理数据逻辑并读写数据库,称为:Data。Data可以有多个,每个Data运行在自己的线程内,要根据自己业务需求来设计需要几个Data
HttpEasy分层
前端层 |
网页,手机,小程序等等需要调用服务器接口的终端 |
Action层 |
服务器对外提供各个接口,要负责检测cookie的合法性后分发到各个Data去处理。Action层运行在HttpServer线程池内,会同时有很多并发。 |
Data层 |
负责数据逻辑的处理,在启动的时候将负责的数据缓存进内存,定时回写改变过的数据到数据库。每一个Data自己有自己的一个专属线程。 |
DB层 |
数据库 |
HttpEasy接口
HttpEasy是对HttpServer的一层封装,源码在CBrother目录下lib/httpeasy.cb。成员变量_httpServer为HttpServer对象,可以使用该变量修改http服务的参数
函数 |
描述 |
用法 |
listenPort(port,CRT_PATH,KEY_PATH) |
添加监听端口.port:端口,CRT_PATH,KEY_PATH:证书路径,不写证书路径启动http服务,写了证书路径启动https服务.可以调用多次同时监听多个端口 |
httpEasy.listenPort(80) |
addAction(actobj,name) |
给已经添加的端口绑定http响应接口,actobj:响应对象,name:接口名字,可省略,省略后默认为actobj类名 |
httpEasy.addAction(actobj) |
addData(dataobj,name) |
添加Data,dataobj:Data对象,name:data名字,可省略,省略后默认为dataobj类名 |
httpEasy.addData(dataobj) |
setRoot(path) |
设置服务器跟目录,path: 根目录绝对路径 |
httpEasy.setroot(path) |
setThreadCount(cnt) |
设置响应线程数量,默认10个 |
httpEasy.setThreadCount(50) |
setNormalAction(actName) |
设置默认响应接口 |
httpEasy.setNormalAction("hello.cb") |
set404Action(actName) |
设置错误响应接口,不设置CBrother有默认页面 |
httpEasy.set404Action("404.cb") |
setOutTime(t) |
设置请求超时时间,默认10秒,单位为秒 |
httpEasy.setOutTime(3) |
setMaxReqDataLen(len) |
设置客户机发送的最大请求数据长度,默认500K,参数单位为(字节) |
httpEasy.setMaxReqDataLen(1024 * 1024) |
openLog() |
打开日志,在webroot平级建立log目录默认关闭,建议打开 |
httpEasy.openLog() |
closeFileService() |
关闭文件下载服务录 |
httpEasy.closeFileService() |
getHttpServer(port) |
获取端口对应的HttpServer对象 |
var httpServer = httpEasy.getHttpServer(80) |
run() |
启动服务 |
httpEasy.run() |
syncCallData(dataName,dataFunc,parmArray) |
同步调用Data接口,会阻塞等待函数返回值,超过3秒则超时返回null |
httpEasy.syncCallData("testData","testFunc",[1,2]) |
asyncCallData(dataName,dataFunc,parmArray) |
异步调用Data接口,不阻塞直接返回,用于不需要接收函数返回值时 |
httpEasy.asyncCallData("testData","testFunc",[1,2]) |
import lib/httpeasy
var HTTPEasy = new HttpEasy();
function main(parm)
{
HTTPEasy.listenPort(8000); //http server port 8000
HTTPEasy.addAction(new HelloAction()); //访问http://x.x.x.x/HelloAction会触发HelloAction的DoAction方法
HTTPEasy.addData(new HelloData()); //注册HelloData
HTTPEasy.run();
}
class HelloAction
{
function DoAction(request,respon)
{
var res = HTTPEasy.syncCallData("HelloData","hello"); //同步调用HelloData的hello方法,接收返回值
respon.write("now time is:" + res);
respon.flush();
}
}
class HelloData
{
function onInit(t)
{
print "HelloData init";
}
function onEnd()
{
print "HelloData end";
}
function hello()
{
var myTime = new Time();
return myTime.strftime("%Y/%m/%d %H:%M:%S");
}
}
调用/HelloAction时,会返回当前时间
其中Action的用法与HttpServer需要注册的Action类完全相同,可以直接去Http模块查看具体用法,这里只讲解一下HttpEasy新增的Data类
数据响应类Data
Data响应类可以有如下接口
function onInit(t),Data线程初始化,入参是一个Thread对象,为自己所运行的线程。一般建议在此时加载数据库数据到内存里。
function onEnd(),Data线程结束,一般建议此时要回写变化过的数据。
定时器的添加
在onInit方法中框架会传递所在线程对象,可以使用该对象来添加定时器。具体可以查看多线程中Thread类的用法。
一些特别重要的数据可以在修改后立即回写数据库,其他的一些数据可以直接在内存中修改后返回前端,然后在定时器中回写这些数据,降低数据库压力,并且提高响应效率。
class HelloData
{
function onInit(t)
{
print "HelloData init";
t.addTimer(1000,testHeart); //添加一个定时器,每1秒执行一次testHeart方法
t.addTimer(5000,testHeart1,2); //添加一个定时器,每5秒执行一次testHeart1方法,执行两次后自动删除改定时器
}
function onEnd()
{
print "HelloData end";
}
function hello()
{
var myTime = new Time();
return myTime.strftime("%Y/%m/%d %H:%M:%S");
}
function testHeart()
{
print "testHeart";
}
function testHeart1()
{
print "testHeart111";
}
}
Data的使用原则
1. 每一个Data都运行在自己的线程内,所以每个Data只能操作自己的成员变量。
2. 把互相关联强的数据放到同一个Data内处理,化并行为串行,简化逻辑。
3. 一个Data也可以同步跨线程调用另一个Data的方法,但是如果调用频率过高,就把两个Data数据合并在一个Data里,会提高执行效率。
4. 尽量避免在一个Data里同步调用另一个Data的方法,但是可以异步调用
5. Data的划分可以是数据关系纵向划分,比如用户数据一个Data,商品数据一个Data等。
6. 当数据海量以后还可以按照ID横向划分,比如500万以内的用户一个Data,500万以上的另一个Data。
7. 当你对于多个Data的线程关系一直无法理解的时候,你所遇到的需求用一个Data绝对可以搞定,不要担心压力问题。
用HttpEasy来实现一个简单的商城后台需求
为了能深入理解HttpEasy的用法,我们来实现一个简单的商城后台,需要预留如下接口。
接口 |
描述 |
参数和返回 |
/RechargeAction |
充值接口,留给第三方后台调用,比如用户用支付宝或者微信充值到我们平台,对方后台会调用这个接口。 |
post数据 : {"channel":"wechat","money":1000,"userid":"11111"} |
/LoginAction |
登陆接口,前端调用。 |
post数据 : {"account":"aaa","pwd":"bbb"} |
/ItemListAction |
查看商品列表,前端调用。 |
post数据 : {"type":1} //type为0表示全部类型商品 |
/OrderListAction |
查看订单列表,前端调用。 |
不提交数据 |
/BuyOrderAction |
下订单,前端调用。 |
post数据 : {"itemid":1,"count":1} |
/PayAction |
支付订单,前端调用。 |
post数据 : {"orderid":"202012162020201"} |
下面我们来设计一下数据库表结构,数据库我们使用mysql
用户表如下,ID做主键,账号加索引
并手动初始化两条用户数据
商品表如下,ID做主键
并手动初始化三条商品数据
订单表如下,ID做主键,没有人下订单,所以开始是空的
用单个Data来实现这个需求
我们先不去考虑数据的划分,直接放到同一个Data里去实现这个功能,这样比较简单,程序入口如下,注册6个Action和1个Data
import lib/httpeasy
import lib/log
var HTTPEasy = new HttpEasy();
function main(parm)
{
InitLog(GetRoot(),"httpeasy"); //初始化日志路径到工作根目录
HTTPEasy.listenPort(8000); //http server port 8000
HTTPEasy.addAction(new RechargeAction());
HTTPEasy.addAction(new LoginAction());
HTTPEasy.addAction(new ItemListAction());
HTTPEasy.addAction(new OrderListAction());
HTTPEasy.addAction(new BuyOrderAction());
HTTPEasy.addAction(new PayAction());
HTTPEasy.addData(new ServerData());
WriteLog("server start! port:" + 8000);
HTTPEasy.run();
}
Data在启动的时候把用户和商品信息加载进内存,内存中的数据主要用Map容器来管理
class User //定义描述用户数据在内存中的类型
{
var id;
var account;
var pwd;
var userName;
var sex;
var money;
}
class Item //定义描述商品数据在内存中类型
{
var itemID;
var itemName;
var price;
var count;
var type;
}
class ServerData
{
var _mysql = new MySQL("127.0.0.1",3306,"root","123456","httpeasy");
var _userAccountMap = new Map(); //通过用户名查找到用户信息
var _userIDMap = new Map(); //通过用户ID查找到用户信息
var _itemMap = new Map(); //通过商品ID查找到商品信息
function onInit(t)
{
if(!_mysql.connect())
{
WriteLog("mysql connect err!");
return;
}
initUserTable(); //加载用户数据
initItemTable(); //加载商品数据
}
function onEnd()
{
}
function initUserTable()
{
var sql = "select * from usertable";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var user = new User();
user.id = _mysql.getInt("id");
user.account = _mysql.getString("account");
user.pwd = _mysql.getString("pwd");
user.userName = _mysql.getString("userName");
user.sex = _mysql.getString("sex");
user.money = _mysql.getInt("money");
_userAccountMap.add(user.account,user);
_userIDMap.add(user.id,user);
}
}
}
function initItemTable()
{
var sql = "select * from itemtable";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var item = new Item();
item.itemID = _mysql.getInt("itemID");
item.itemName = _mysql.getString("itemName");
item.price = _mysql.getInt("price");
item.count = _mysql.getInt("count");
item.type = _mysql.getInt("type");
_itemMap.add(item.itemID,item);
}
}
}
}
第一个接口编写登陆调用的LoginAction,最主要一句代码是通过HTTPEasy.syncCallData方法调用ServerData的login方法
const COOKIE_PWD = "TEST_HTTPEASY"; //cookie的密码
class LoginAction
{
function DoAction(request,respon)
{
var postData = request.getData(); //获取post数据
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var account = json.getString("account");
var pwd = json.getString("pwd");
if (account == null || pwd == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的login方法
var resJson = HTTPEasy.syncCallData("ServerData","login",[account,pwd]);
if (resJson != null)
{
var uid = resJson.get("userid");
var cookie = new Cookie();
cookie.setName("userid");
cookie.setValue(uid,COOKIE_PWD);
respon.addCookie(cookie); //添加userid密文到cookie,不设置时间的话关闭浏览器自动失效
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加login方法
class ServerData
{
......
function login(account,pwd)
{
var user = _userAccountMap.get(account);
if (user == null)
{
return null;
}
var tempPwd = openssl_sha1(pwd + "test"); //密码为 sha1(密码明文 + 字符串test)
if (tempPwd != user.pwd)
{
return null; //密码错误
}
var resJson = new Json();
resJson.add("userid",user.id);
resJson.add("username",user.userName);
resJson.add("sex",user.sex);
resJson.add("money",user.money);
return resJson;
}
......
}
用户登录成功后,会先请求一遍商品列表,下面再实现一下ItemListAction
class ItemListAction
{
function DoAction(request,respon)
{
//这个接口没有验证用户登录状态,因为即便用户不登录也应该有权限看到我们的商品列表
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var type = json.get("type");
if (type == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的itemList方法
var resJson = HTTPEasy.syncCallData("ServerData","itemList",[type]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加itemList方法
class ServerData
{
......
function itemList(type)
{
var resJson = new Json();
foreach (k,v : _itemMap)
{
if (type == 0 || v.type == type)
{
var itemObj = resJson.pushObject();
itemObj.add("itemid",v.itemID);
itemObj.add("itemname",v.itemName);
itemObj.add("type",v.type);
itemObj.add("price",v.price);
itemObj.add("count",v.count);
}
}
return resJson;
}
......
}
如果用户看上了某件商品,会来下单购买这件商品,我们来实现下单接口BuyOrderAction,用户只有登陆了才可以购买物品,所以这个接口要检测登陆状态
function GetCookieUserID(request) //这个方法来查找客户机的登录cookie信息
{
var cookCnt = request.getCookieCount();
for (var i = 0; i < cookCnt ; i++)
{
var cookie = request.getCookie(i);
if (cookie.getName() == "userid")
{
return cookie.getValue(COOKIE_PWD);
}
}
return null;
}
class BuyOrderAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request); //检测登陆状态
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var itemid = json.get("itemid");
var count = json.get("count");
if (itemid == null || count == null || count < 1)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的buyOrder方法
var resJson = HTTPEasy.syncCallData("ServerData","buyOrder",[userid,itemid,count]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加对订单的支持
class Order //定义描述订单数据在内存中类型
{
var orderID;
var userID;
var itemID;
var count;
var state;
var price;
var lasttime;
}
class UserOrder //定义描述用户自己订单列表在内存中类型
{
var oldOrderList = new Array(); //已支付或关闭
var newOrderList = new Array(); //下单未支付
}
class ServerData
{
......
var _orderMap = new Map(); //通过用户ID查找到订单列表
var _orderIndex = 1;
function buyOrder(userid,itemid,count)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
var item = _itemMap.get(itemid);
if (item == null)
{
return null; //商品不存在
}
if (item.count < count)
{
return null; //库存不足
}
var price = item.price * count;
var userOrder = _orderMap.get(userid);
if (userOrder == null)
{
userOrder = new UserOrder();
_orderMap.add(userid,userOrder);
}
item.count -= count; //占用库存
var time = new Time();
var timestr = time.strftime("%Y%m%d%H%M%S");
var orderidx = _orderIndex++;
var order = new Order();
order.orderID = timestr + orderidx;
order.userID = userid;
order.state = 0;
order.itemID = itemid;
order.count = count;
order.price = price;
order.lasttime = time();
userOrder.newOrderList.add(order);
WriteLog(user.userName + " buy " + item.itemName + "X" + count + " price:" + price + " orderid:" + order.orderID);
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",price);
json.add("itemid",itemid);
json.add("count",count);
json.add("state",0);
return json;
}
......
}
用户下了订单之后确认无误就要真正的支付了,下面实现一下支付的PayAction
class PayAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request);
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var orderid = json.get("orderid");
if (orderid == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的pay方法
var resJson = HTTPEasy.syncCallData("ServerData","pay",[userid,orderid]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加pay方法
class ServerData
{
......
function pay(userid,orderid)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
var userOrder = _orderMap.get(userid);
if(userOrder == null)
{
return null; //用户没有任何订单信息
}
var order = null;
var orderIdx = -1;
for (var i = 0; i < userOrder.newOrderList.size() ; i++)
{
if (userOrder.newOrderList[i].orderID == orderid)
{
order = userOrder.newOrderList[i];
orderIdx = i;
break;
}
}
if(order == null)
{
return null; //没有找到订单
}
if(user.money < order.price)
{
return null; //用户钱不够
}
order.state = 1;
order.lasttime = time();
userOrder.newOrderList.remove(orderIdx); //从未付费列表删除
userOrder.oldOrderList.add(order); //加入支付列表
//钱很重要先扣钱
user.money -= order.price;
var sql = "update usertable set money=" + user.money + " where id='" + userid + "'";
_mysql.upDate(sql);
//插入数据库
var sql = "insert into orderTable (orderID,userID,itemID,count,state,price,lasttime) values('"
+ orderid + "'," + userid + "," + order.itemID + "," + order.count + "," + order.state + "," + order.price + "," + order.lasttime + ")";
_mysql.upDate(sql);
WriteLog(user.userName + " pay price:" + order.price + " orderid:" + order.orderID + " userMoney:" + user.money);
var resJson = new Json();
resJson.add("orderid",orderid);
return resJson;
}
......
}
从下单到支付的流程就通了,但是用户数据初始化时候money字段都是0,当前端调用支付接口的时候总是因为钱不够支付失败,先来实现一下充值接口RechargeAction
class RechargeAction
{
function DoAction(request,respon)
{
//第三方平台调用接口,应该要有对方IP的白名单,这里是测试只允许本机调用
var targetip = request.getRemoteIP();
if(targetip != "127.0.0.1")
{
respon.write("err");
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var channel = json.get("channel");
var money = json.get("money");
var userid = json.get("userid");
if (channel == null || money == null || userid == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的recharge方法
var res = HTTPEasy.syncCallData("ServerData","recharge",[userid,money,channel]);
if (res != null)
{
respon.write(res);
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加recharge方法
class ServerData
{
......
function recharge(userid,money,channel)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
if(money < 0)
{
return null;
}
user.money += money;
//钱实时写入
var sql = "update usertable set money=" + user.money + " where id='" + userid + "'";
_mysql.upDate(sql);
WriteLog(user.userName + " recharge money:" + money + " channel:" + channel + " userMoney:" + user.money);
return "ok";
}
......
}
用户充值后就可以正常支付了,支付成功后用户就有了历史订单,前端要展示这些历史订单,我们再来实现返回订单列表的接口OrderListAction
class OrderListAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request);
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
//同步调用ServerData的buyOrder方法
var resJson = HTTPEasy.syncCallData("ServerData","orderList",[userid]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ServerData增加orderList方法
class ServerData
{
......
function orderList(userid)
{
var userOrder = _orderMap.get(userid);
if (userOrder == null)
{
return null; //用户订单不存在
}
var resJson = new Json();
for (var i = userOrder.newOrderList.size() - 1 ; i >= 0 ; i--)
{
var order = userOrder.newOrderList[i];
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",order.price);
json.add("itemid",order.itemID);
json.add("count",order.count);
json.add("state",order.state);
resJson.push(json);
}
for (var i = userOrder.oldOrderList.size() - 1 ; i >= 0 ; i--)
{
var order = userOrder.oldOrderList[i];
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",order.price);
json.add("itemid",order.itemID);
json.add("count",order.count);
json.add("state",order.state);
resJson.push(json);
}
return resJson;
}
......
}
接口实现完了,但我们发现当服务器重启后订单数据没有加载进内存,所以要在ServerData启动时候加载订单数据,在ServerData退出时候把未完成的订单关闭并回写数据库
class ServerData
{
......
function onInit(t)
{
......
initOrderTable(); //加载订单数据
}
function onEnd()
{
foreach (k,v : _orderMap)
{
if(v.newOrderList.size() <= 0)
{
continue;
}
for (var i = 0; i < v.newOrderList.size(); i++)
{
//关闭未完成的订单
var order = v.newOrderList[i];
closeOrder(order);
}
}
}
function initOrderTable()
{
var sql = "select * from ordertable ORDER BY lasttime";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var order = new Order();
order.orderID = _mysql.getString("orderID");
order.userID = _mysql.getString("userID");
order.itemID = _mysql.getInt("itemID");
order.count = _mysql.getInt("count");
order.state = _mysql.getInt("state");
order.price = _mysql.getInt("price");
order.lasttime = _mysql.getLong("lasttime");
var userOrder = _orderMap.get(order.userID);
if (userOrder == null)
{
userOrder = new UserOrder();
_orderMap.add(order.userID,userOrder);
}
userOrder.oldOrderList.add(order);
}
}
}
function closeOrder(order)
{
//关闭未完成的订单
order.state = 2;
order.lasttime = time();
//插入数据库
var sql = "insert into orderTable (orderID,userID,itemID,count,state,price,lasttime) values('"
+ order.orderID + "'," + order.userID + "," + order.itemID + "," + order.count + "," + order.state + "," + order.price + "," + order.lasttime + ")";
_mysql.upDate(sql);
var item = _itemMap.get(itemid);
if (item != null)
{
//把商品的库存数量还回去
item.count += count;
}
WriteLog("close order price:" + order.price + " orderid:" + order.orderID);
}
......
}
为了防止用户下订单占用商品库存后一直不支付,我们需要定时监测未支付订单,超过10分钟仍未支付的就要自动关闭,将物品库存数量还原回去让其他用户正常购买。这里我们需要给ServerData增加定时器
class ServerData
{
......
function onInit(t)
{
......
t.addTimer(1000 * 60,orderTimer); //增加检测订单的定时器,每60秒执行一次orderTimer方法
}
function orderTimer()
{
foreach (k,v : _orderMap)
{
if(v.newOrderList.size() <= 0)
{
continue;
}
var nowTime = time();
for (var i = 0; i < v.newOrderList.size(); i++)
{
var order = v.newOrderList[i];
if(nowTime - order.lasttime > 60 * 10)
{
closeOrder(order); //用户未支付订单大于10分钟,删除
v.newOrderList.remove(i);
i--;
}
}
}
}
......
}
最后发现还忽略了一点,商品库存变化后没有回写数据库,这个数据没有必要实时回写,我们用定时器来做,首先要给Item类添加一个是否变化的属性isChange,在用户下单占用库存时和关闭订单还原库存时将这个变量赋值isChange=true
class Item
{
......
var isChange = false; //商品信息是否发生了变化
}
class ServerData
{
......
function onInit(t)
{
......
t.addTimer(1000 * 60 * 5,itemTimer); //检测商品库存的定时器,每5分钟执行一次itemTimer方法
}
function itemTimer()
{
foreach (k,v : _itemMap)
{
if(!v.isChange)
{
continue;
}
var sql = "update itemtable set count=" + v.count + " where itemID=" + v.itemID;
_mysql.upDate(sql);
v.isChange = false;
}
}
......
}
这个简单的系统基本上就完成了,看完后可以发现,Action是直接和前端通讯的,负责收发客户机提交的数据,只做一些简单的状态判断和参数判断,真正的逻辑是在Data中进行的。理解了这一点,你就基本掌握了HttpEasy的用法
上面的代码在CBrother路径下的sample/httpeasy/SingleData/SingleDataHttpEasy.cb,还有一个简单的web前端测试例子在sample/httpeasy/SingleData/webroot/testhttpeasy.html,启动SingleDataHttpEasy.cb后浏览器访问http://127.0.0.1:8000/testhttpeasy.html 可以调试一下代码
如果你没有海量数据的需求暂时可以先不用看后面的多Data实现,就按照一个Data的方式来做,这样既简单又不容易出错
用多个Data来实现这个需求
当数据量在几十万条以内,一般来说一个Data完全可以应付,可是当达到了上百万数据,一个Data就可能会有性能瓶颈问题,所以我们要拆分数据,用多个Data来负载均衡。
这个例子里我们把Data划分成三个。UserData管理用户信息,ItemData管理商品信息,OrderData管理订单信息。程序入口如下
import lib/httpeasy
import lib/log
var HTTPEasy = new HttpEasy();
function main(parm)
{
InitLog(GetRoot(),"httpeasy"); //初始化日志路径到工作根目录
HTTPEasy.listenPort(8000); //http server port 8000
HTTPEasy.addAction(new RechargeAction());
HTTPEasy.addAction(new LoginAction());
HTTPEasy.addAction(new ItemListAction());
HTTPEasy.addAction(new OrderListAction());
HTTPEasy.addAction(new BuyOrderAction());
HTTPEasy.addAction(new PayAction());
HTTPEasy.addData(new UserData());
HTTPEasy.addData(new ItemData());
HTTPEasy.addData(new OrderData());
WriteLog("server start! port:" + 8000);
HTTPEasy.run();
}
UserData启动时候加载用户数据
class User
{
var id;
var account;
var pwd;
var money;
}
class UserData
{
var _mysql = new MySQL("127.0.0.1",3306,"root","123456","test");
var _userAccountMap = new Map(); //通过用户名查找到用户信息
var _userIDMap = new Map(); //通过用户ID查找到用户信息
function onInit(t)
{
if(!_mysql.connect())
{
WriteLog("mysql connect err!");
return;
}
initUserTable(); //加载用户数据
}
function onEnd()
{
}
function initUserTable()
{
var sql = "select * from usertable";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var user = new User();
user.id = _mysql.getInt("id");
user.account = _mysql.getString("account");
user.pwd = _mysql.getString("pwd");
user.money = _mysql.getInt("money");
_userAccountMap.add(user.account,user);
_userIDMap.add(user.id,user);
}
}
}
}
ItemData启动时候加载商品数据
class Item
{
var itemID;
var itemName;
var price;
var count;
var type;
var isChange = false;
}
class ItemData
{
var _mysql = new MySQL("127.0.0.1",3306,"root","123456","test");
var _itemMap = new Map(); //通过商品ID查找到商品信息
function onInit(t)
{
if(!_mysql.connect())
{
WriteLog("mysql connect err!");
return;
}
initItemTable(); //加载商品数据
}
function onEnd()
{
}
function initItemTable()
{
var sql = "select * from itemtable";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var item = new Item();
item.itemID = _mysql.getInt("itemID");
item.itemName = _mysql.getString("itemName");
item.price = _mysql.getInt("price");
item.count = _mysql.getInt("count");
item.type = _mysql.getInt("type");
_itemMap.add(item.itemID,item);
}
}
}
}
OrderData启动时候加载订单数据
class Order
{
var orderID;
var userID;
var itemID;
var count;
var state;
var price;
var lasttime;
}
class UserOrder
{
var oldOrderList = new Array(); //已支付或关闭
var newOrderList = new Array(); //下单未支付
}
class OrderData
{
var _mysql = new MySQL("192.168.1.25",3306,"root","root","test");
var _orderMap = new Map();
var _orderIndex = 1;
function onInit(t)
{
WriteLog("OrderData onInit");
if(!_mysql.connect())
{
WriteLog("mysql connect err!");
return;
}
initOrderTable(); //加载订单数据
}
function onEnd()
{
WriteLog("OrderData onEnd");
}
function initOrderTable()
{
var sql = "select * from ordertable ORDER BY lasttime";
if (_mysql.query(sql))
{
while (_mysql.next())
{
var order = new Order();
order.orderID = _mysql.getString("orderID");
order.userID = _mysql.getString("userID");
order.itemID = _mysql.getInt("itemID");
order.count = _mysql.getInt("count");
order.state = _mysql.getInt("state");
order.price = _mysql.getInt("price");
order.lasttime = _mysql.getLong("lasttime");
var userOrder = _orderMap.get(order.userID);
if (userOrder == null)
{
userOrder = new UserOrder();
_orderMap.add(order.userID,userOrder);
}
userOrder.oldOrderList.add(order);
}
}
}
}
我们还是先来写登陆接口,基本上和单个Data代码一样,只是调用的Data名从ServerData换成了UserData
class LoginAction
{
function DoAction(request,respon)
{
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var account = json.getString("account");
var pwd = json.getString("pwd");
if (account == null || pwd == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用UserData的login方法
var resJson = HTTPEasy.syncCallData("UserData","login",[account,pwd]);
if (resJson != null)
{
var uid = resJson.get("userid");
var cookie = new Cookie();
cookie.setName("userid");
cookie.setValue(uid,COOKIE_PWD);
respon.addCookie(cookie); //添加userid密文到cookie,不设置时间的话关闭浏览器自动失效
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
给UserData添加login方法
class UserData
{
......
function login(account,pwd)
{
var user = _userAccountMap.get(account);
if (user == null)
{
return null;
}
var tempPwd = openssl_sha1(pwd + "test",); //密码为 sha1(密码明文 + 字符串test)
if (tempPwd != user.pwd)
{
return null; //密码错误
}
var resJson = new Json();
resJson.add("userid",user.id);
resJson.add("username",user.userName);
resJson.add("sex",user.sex);
resJson.add("money",user.money);
return resJson;
}
......
}
商品列表接口ItemListAction也没有太大变化,只是换了调用的Data名
class ItemListAction
{
function DoAction(request,respon)
{
//这个接口没有验证用户登录状态,因为即便用户不登录也应该有权限看到我们的商品列表
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var type = json.get("type");
if (type == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的login方法
var resJson = HTTPEasy.syncCallData("ItemData","itemList",[type]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ItemData中增加itemList接口
class ItemData
{
......
function itemList(type)
{
var resJson = new Json();
foreach (k,v : _itemMap)
{
if (type == 0 || v.type == type)
{
var itemObj = resJson.pushObject();
itemObj.add("itemid",v.itemID);
itemObj.add("itemname",v.itemName);
itemObj.add("type",v.type);
itemObj.add("price",v.price);
itemObj.add("count",v.count);
}
}
return resJson;
}
......
}
下订单的BuyOrderAction接口跟单个Data不同了,因为要同时访问ItemData扣除商品库存并获取价格,再到OrderData里面保存订单,所以要顺序调用这两个Data的buyOrder方法
class BuyOrderAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request);
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var itemid = json.get("itemid");
var count = json.get("count");
if (itemid == null || count == null || count < 1)
{
respon.write("err");
respon.flush();
return;
}
//先调用ItemData的buyOrder方法占用库存并获取商品信息
var itemInfo = HTTPEasy.syncCallData("ItemData","buyOrder",[itemid,count]);
if (itemInfo == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用OrderData的buyOrder方法
var resJson = HTTPEasy.syncCallData("OrderData","buyOrder",[userid,itemid,count,itemInfo]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
ItemData中增加buyOrder方法
class ItemData
{
......
function buyOrder(itemid,count)
{
var item = _itemMap.get(itemid);
if (item == null)
{
return null; //商品不存在
}
if (item.count < count)
{
return null; //库存不足
}
item.count -= count; //占用库存
item.isChange = true;
return {"price":item.price,"itemName":item.itemName};
}
......
}
OrderData中增加buyOrder方法
class OrderData
{
......
function buyOrder(userid,itemid,count,itemInfo)
{
var price = itemInfo["price"] * count;
var userOrder = _orderMap.get(userid);
if (userOrder == null)
{
userOrder = new UserOrder();
_orderMap.add(userid,userOrder);
}
var time = new Time();
var timestr = time.strftime("%Y%m%d%H%M%S");
var orderidx = _orderIndex++;
var order = new Order();
order.orderID = timestr + orderidx;
order.userID = userid;
order.state = 0;
order.itemID = itemid;
order.count = count;
order.price = price;
order.lasttime = time();
userOrder.newOrderList.add(order);
WriteLog(userid + " buy " + itemInfo["itemName"] + "X" + count + " price:" + price + " orderid:" + order.orderID);
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",price);
json.add("itemid",itemid);
json.add("count",count);
json.add("state",0);
return json;
}
......
}
下面实现支付的PayAction,这个Action执行的任务顺序为:
1.去OrderData获取订单价格
2.去UserData扣除金额
3.去OrderData修改订单状态
4.如果最后一步错误了还要把扣除的钱还给UserData,从这一步可以看出异步操作虽然提升了性能,但也使代码的复杂度增高
class PayAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request);
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var orderid = json.get("orderid");
if (orderid == null)
{
respon.write("err");
respon.flush();
return;
}
//1.去OrderData获取订单价格
var orderPrice = HTTPEasy.syncCallData("OrderData","getOrderPrice",[userid,orderid]);
if (orderPrice == null)
{
respon.write("err");
respon.flush();
return;
}
//2.去UserData扣钱
var orderPrice = HTTPEasy.syncCallData("UserData","pay",[userid,orderPrice,orderid]);
if (orderPrice == null)
{
respon.write("err");
respon.flush();
return;
}
//3.到OrderData里修改订单状态
var res = HTTPEasy.syncCallData("OrderData","pay",[userid,orderid]);
if (res != null)
{
if (res)
{
var resJson = new Json();
resJson.add("orderid",orderid);
respon.write(resJson.toJsonString());
}
else
{
//4.出错了,把钱还回去,异步调用就好了,不需要等结果
HTTPEasy.asyncCallData("UserData","payErr",[userid,orderPrice,orderid]);
respon.write("err");
}
}
else
{
respon.write("err");
}
respon.flush();
}
}
UserData增加pay方法和payErr方法
class UserData
{
......
function pay(userid,price,orderid)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
if(user.money < price)
{
return null; //用户钱不够
}
//钱很重要先扣钱
user.money -= price;
var sql = "update usertable set money=" + user.money +" where id='" + userid + "'";
_mysql.upDate(sql);
WriteLog(user.userName + " pay price:" + price + " orderid:" + orderid + " userMoney:" + user.money);
return true;
}
function payErr(userid,price,orderid)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
//钱很加回来
user.money += price;
var sql = "update usertable set money=" + user.money + " where id='" + userid + "'";
_mysql.upDate(sql);
WriteLog(user.userName + " payErr price:" + price + " orderid:" + orderid + " userMoney:" + user.money);
return true;
}
......
}
OrderData增加getOrderPrice方法和pay方法
class OrderData
{
......
function getOrderPrice(userid,orderid)
{
var userOrder = _orderMap.get(userid);
if(userOrder == null)
{
return null; //用户没有任何订单信息
}
var order = null;
var orderIdx = -1;
for (var i = 0; i < userOrder.newOrderList.size() ; i++)
{
if (userOrder.newOrderList[i].orderID == orderid)
{
order = userOrder.newOrderList[i];
orderIdx = i;
break;
}
}
if(order == null)
{
return null; //没有找到订单
}
return order.price;
}
function pay(userid,orderid)
{
var userOrder = _orderMap.get(userid);
if(userOrder == null)
{
return null; //用户没有任何订单信息
}
var order = null;
var orderIdx = -1;
for (var i = 0; i < userOrder.newOrderList.size() ; i++)
{
if (userOrder.newOrderList[i].orderID == orderid)
{
order = userOrder.newOrderList[i];
orderIdx = i;
break;
}
}
if(order == null)
{
//到这一步没有订单,应该是订单被定时器关闭了,需要把钱还回去,这里概率非常低但是也要处理
return false;
}
order.state = 1;;
order.lasttime = time();
userOrder.newOrderList.remove(orderIdx); //从未付费列表删除
userOrder.oldOrderList.add(order); //加入支付列表
//插入数据库
var sql = "insert into orderTable (orderID,userID,itemID,count,state,price,lasttime) values('"
+ orderid + "'," + userid + "," + order.itemID + "," + order.count + "," + order.state + "," + order.price + "," + order.lasttime + ")";
_mysql.upDate(sql);
return true;
}
......
}
再实现一下充值接口RechargeAction,这个接口只访问UserData,没有大的变化
class RechargeAction
{
function DoAction(request,respon)
{
//第三方平台调用接口,应该要有对方IP的白名单,这里是测试只允许本机调用
var targetip = request.getRemoteIP();
if(targetip != "127.0.0.1")
{
respon.write("err");
respon.flush();
return;
}
var postData = request.getData();
if (postData == null)
{
respon.write("err");
respon.flush();
return;
}
var json = new Json(postData);
var channel = json.get("channel");
var money = json.get("money");
var userid = json.get("userid");
if (channel == null || money == null || userid == null)
{
respon.write("err");
respon.flush();
return;
}
//同步调用ServerData的pay方法
var res = HTTPEasy.syncCallData("UserData","recharge",[userid,money,channel]);
if (res != null)
{
respon.write(res);
}
else
{
respon.write("err");
}
respon.flush();
}
}
UserData增加recharge方法
class UserData
{
......
function recharge(userid,money,channel)
{
var user = _userIDMap.get(userid);
if (user == null)
{
return null; //用户不存在
}
if(money < 0)
{
return null;
}
user.money += money;
//钱实时写入
var sql = "update usertable set money=" + user.money + " where id='" + userid + "'";
_mysql.upDate(sql);
WriteLog(user.userName + " recharge money:" + money + " channel:" + channel + " userMoney:" + user.money);
return "ok";
}
......
}
最后剩下订单列表接口OrderListAction也没有太大变化
class OrderListAction
{
function DoAction(request,respon)
{
var userid = GetCookieUserID(request);
if(userid == null)
{
respon.write("err"); //没有登录
respon.flush();
return;
}
//同步调用ServerData的buyOrder方法
var resJson = HTTPEasy.syncCallData("OrderData","orderList",[userid]);
if (resJson != null)
{
respon.write(resJson.toJsonString());
}
else
{
respon.write("err");
}
respon.flush();
}
}
OrderData增加orderList方法
class OrderData
{
......
function orderList(userid)
{
var userOrder = _orderMap.get(userid);
if (userOrder == null)
{
return null; //用户订单不存在
}
var resJson = new Json();
for (var i = userOrder.newOrderList.size() - 1 ; i >= 0 ; i--)
{
var order = userOrder.newOrderList[i];
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",order.price);
json.add("itemid",order.itemID);
json.add("count",order.count);
json.add("state",order.state);
resJson.push(json);
}
for (var i = userOrder.oldOrderList.size() - 1 ; i >= 0 ; i--)
{
var order = userOrder.oldOrderList[i];
var json = new Json();
json.add("orderid",order.orderID);
json.add("price",order.price);
json.add("itemid",order.itemID);
json.add("count",order.count);
json.add("state",order.state);
resJson.push(json);
}
return resJson;
}
......
}
我们还需要在OrderData退出时候将未完成的订单关闭,在下订单后超过10分钟未支付也将订单关闭,关闭订单时要把商品库存还回去。这里要注意OrderData中调用ItemData的方法,用的是异步,而不是同步。
class OrderData
{
......
function onInit(t)
{
......
t.addTimer(1000 * 60,orderTimer); //检测订单的定时器,每60秒执行一次
}
function onEnd()
{
WriteLog("OrderData onEnd");
foreach (k,v : _orderMap)
{
if(v.newOrderList.size() <= 0)
{
continue;
}
for (var i = 0 ; i < v.newOrderList.size(); i++)
{
var order = v.newOrderList[i];
closeOrder(order);
}
}
}
function closeOrder(order)
{
//关闭未完成的订单
order.state = 2;
order.lasttime = time();
//插入数据库
var sql = "insert into orderTable (orderID,userID,itemID,count,state,price,lasttime) values('"
+ order.orderID + "'," + order.userID + "," + order.itemID + "," + order.count + "," + order.state + "," + order.price + "," + order.lasttime + ")";
_mysql.upDate(sql);
//异步通知ItemData,不阻塞,不接收返回值,在Data内部要尽量避免同步调用
HTTPEasy.asyncCallData("ItemData","closeOrder",[order.itemID,order.count]);
WriteLog("close order price:" + order.price + " orderid:" + order.orderID);
}
function orderTimer()
{
foreach (k,v : _orderMap)
{
if(v.newOrderList.size() <= 0)
{
continue;
}
var nowTime = time();
for (var i = 0 ; i < v.newOrderList.size(); i++)
{
var order = v.newOrderList[i];
if(nowTime - order.lasttime > 60 * 10)
{
closeOrder(order);
v.newOrderList.remove(i);
i--;
}
}
}
}
......
}
ItemData增加closeOrder方法,并增加定时回写库存的定时器
class ItemData
{
......
function onInit(t)
{
......
t.addTimer(1000 * 60 * 5,itemTimer); //检测商品库存的定时器,每5分钟执行一次
}
function onEnd()
{
WriteLog("ItemData onEnd");
itemTimer(); //退出的时候检测一边是否要回写
}
function closeOrder(itemid,count)
{
var item = _itemMap.get(itemid);
if (item != null)
{
//把商品的数量还回去
item.count += count;
item.isChange = true;
}
}
function itemTimer()
{
foreach (k,v : _itemMap)
{
if(!v.isChange)
{
continue;
}
var sql = "update itemtable set count=" + v.count + " where itemID=" + v.itemID;
_mysql.upDate(sql);
v.isChange = false;
}
}
......
}
到这里这个系统用多个Data的方式也实现完了,理解后你会发现,Data还可以划分的更细,比如ID小于500万用UserData1大于则用UserData2,或者男性用UserData1女性用UserData2,水果用ItemData1零食用ItemData2, 其原理就是将数据的最小单元按照某一个标准再次拆分,具体如何拆分还要根据自己的业务需求来定,其实这样的拆分思想和数据库分表以及服务器分布式拆分思想是相同的。
代码在CBrother目录下sample/httpeasy/MulData目录下,这个工程我把文件拆成了多个,这是为了做一个示范,当代码量大的时候可以按照这样的目录来拆分代码。
如若转载,请注明出处:https://www.daxuejiayuan.com/5516.html