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是對HttpServer的一層封裝,源碼在CBrother目錄下lib/httpeasy.cb。成員變量_httpServer為HttpServer對象,可以使用該變量修改http服務的參數
函數 |
描述 |
用法 |
listenPort(port,CRT_PATH,KEY_PATH) |
添加監聽端口.port:端口,CRT_PATH,KEY_PATH:證書路徑,不寫證書路徑啟動http服務,寫了證書路徑啟動https服務.可以調用多次同時監聽多個端口 |
httpEasy.listenPort(80)httpEasy.listenPort(443,"xx.crt","xx.key") |
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) |
setRoot(path) |
設置服務器跟目錄,path: 根目錄絕對路徑 |
httpEasy.setRoot(path) |
setThreadCount(cnt) |
設置響應線程數量,默認10個設置大了并發量大但是消耗資源多 |
httpEasy.setThreadCount(50) |
setNormalAction(actName) |
設置默認響應接口訪問http://x.x.x.x/後面不帶路徑時默認執行到的界面 |
httpEasy.setNormalAction("hello.cb")httpServer.setNormalAction("hello.html") |
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秒則超時返回nulldataName:要調用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.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類
數據響應類DataData響應類可以有如下接口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";
}
}
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"}成功返回 : "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" |
用戶表如下,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目錄下,這個工程我把文件拆成了多個,這是為了做一個示範,當代碼量大的時候可以按照這樣的目錄來拆分代碼。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!