HttpEasy 框架
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
服务的参数
函数 | 描述 | 用法 |
---|---|---|
addAction(actobj,name) | 添加http 响应接口,actobj :响应对象,name :接口名字,可省略,省略后默认为actobj 类名 | httpEasy.addAction(actobj)httpEasy.addAction(actobj,name) |
addData(dataobj,name) | 添加Data ,dataobj :Data 对象,name :data 名字,可省略,省略后默认为dataobj 类名 | httpEasy.addData(dataobj)httpEasy.addData(dataobj,name) |
run(port) | 启动服务,port :监听端口,默认8000 | httpEasy.run(80) |
syncCallData(dataName,dataFunc,parmArray) | 同步调用Data 接口,会阻塞等待函数返回值,超过3秒则超时返回null
dataName :要调用Data 的名字,与addData 时候名字相同
dataFunc :要调用的方法名
parmArray :函数参数,为一个Array 对象 | httpEasy.syncCallData("testData","testFunc",[1,2]) |
asyncCallData(dataName,dataFunc,parmArray) | 异步调用Data 接口,不阻塞直接返回,用于不需要接收函数返回值时
dataName :要调用Data 的名字,与addData 时候名字相同
dataFunc :要调用的方法名
parmArray :函数参数,为一个Array 对象 | httpEasy.asyncCallData("testData","testFunc",[1,2]) |
import lib/httpeasy
var HTTPEasy = new HttpEasy();
function main(parm)
{
HTTPEasy.addAction(new HelloAction()); //访问http://x.x.x.x/HelloAction会触发HelloAction的DoAction方法
HTTPEasy.addData(new HelloData()); //注册HelloData
HTTPEasy.run(8000);
}
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的使用原则
每一个
Data
都运行在自己的线程内,所以每个Data
只能操作自己的成员变量。把互相关联强的数据放到同一个
Data
内处理,化并行为串行,简化逻辑。一个
Data
也可以同步跨线程调用另一个Data
的方法,但是如果调用频率过高,就把两个Data
数据合并在一个Data
里,会提高执行效率。尽量避免在一个
Data
里同步调用另一个Data
的方法,但是可以异步调用Data
的划分可以是数据关系纵向划分,比如用户数据一个Data
,商品数据一个Data
等。当数据海量以后还可以按照
ID
横向划分,比如500万以内的用户一个Data
,500万以上的另一个Data
。当你对于多个
Data
的线程关系一直无法理解的时候,你所遇到的需求用一个Data
绝对可以搞定,不要担心压力问题。
用HttpEasy来实现一个简单的商城后台需求
为了能深入理解HttpEasy
的用法,我们来实现一个简单的商城后台,需要预留如下接口。
接口 | 描述 | 参数和返回 |
---|---|---|
/RechargeAction | 充值接口,留给第三方后台调用,比如用户用支付宝或者微信充值到我们平台,对方后台会调用这个接口。 (我们这个接口只是虚拟的,具体要接第三方支付的话还需要去研究第三方文档) | post 数据 : {"channel":"wechat","money":1000,"userid":"11111"}
成功返回 : "ok" 失败返回 : "err" |
/LoginAction | 登陆接口,前端调用。 | post 数据 : {"account":"aaa","pwd":"bbb"}
成功返回 : {userid:"111",username:"小红",sex:"女"} 并添加cookie
失败返回 : "err" |
/ItemListAction | 查看商品列表,前端调用。 | post 数据 : {"type":1} //type 为0表示全部类型商品
成功返回 : [{"itemid":1,"itemname":"苹果","type":1,"price":10000,"count":100},……]
失败返回 : "err" |
/OrderListAction | 查看订单列表,前端调用。 | 不提交数据
成功返回 : [{"orderid":"202012162020201","price":10000,"itemid":1,"count":1,"state":1},……]
失败返回 : "err" |
/BuyOrderAction | 下订单,前端调用。 | post 数据 : {"itemid":1,"count":1}
成功返回 : {"orderid":"202012162020201","price":10000,"itemid":1,"count":1,"state":0}
失败返回 : "err" |
/PayAction | 支付订单,前端调用。 | post 数据 : {"orderid":"202012162020201"}
成功返回 : {"orderid":"202012162020201"}
失败返回 : "err" |
下面我们来设计一下数据库表结构,数据库我们使用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.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(8000);
}
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.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(8000);
}
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
零食用UserData2
, 其原理就是将数据的最小单元按照某一个标准再次拆分,具体如何拆分还要根据自己的业务需求来定,其实这样的拆分思想和数据库分表以及服务器分布式拆分思想是相同的。
代码在CBrother
目录下sample/httpeasy/MulData
目录下,这个工程我把文件拆成了多个,这是为了做一个示范,当代码量大的时候可以按照这样的目录来拆分代码。