項目地址: https://github.com/hwding/themis-von-wifi
首先設想一下這個情境: 端午前夕, 大學宿捨中A\B\C\D四個人, A和B明天就要和同學去旅游了, 因此B在瘋狂地用迅雷囤積電影, 然而無所事事的屌絲C正在某酷上看游戲視頻. 宿捨網速的極限在2.8MB/s, B的迅雷會員離線+加速通道全開, 導致C的視頻緩沖速度只有12KB/s( <-- 沒錯這個人就是我啦, 抓狂中...), 而A和D僅僅再用手機刷些網頁, 但是速度極慢以至於到了嚴重影響體驗的程度.
除了B其余人不爽中...

通過分析我們可以發現, B與C對網速具有同樣的高要求, 但是速度分配的結果極不公平, 並且B與C的網速和幾乎占滿極限網速從而導致A和D的網頁浏覽之類的低速操作也難以及時完成.因此本小白希望能夠在出現高需求競爭的時候公平地分配網速, 同時也保證其他低需求接入點的基礎速度.
以上就是本小白的困境與設想, 現在我們來看一看如何簡單地實現.
項目地址: https://github.com/hwding/themis-von-wifi, 編碼水平有限, 有更好的想法與算法上的建議歡迎私信!
預覽

如果想控制接入點的限速, 我們必須首先研究後台控制頁面的登入方式.
本小白宿捨使用TP-LINK TL-WR842N, 登陸界面是這樣的

現在使用Firefox的開發人員工具看一看訪問後台需要哪些文件我們重新刷新一下頁面

js和css頁面引入眼簾. 我們再來嘗試登錄一下, 看一看登錄憑據的格式

登錄時填寫的密碼被前端的js腳本加密了, 我們必須找到其中的加密算法
(省略一萬字...)加密算法在classs.js文件中, 我們將此文件從路由端下載下來後, 拖出其中的 this.securityEncode 和 this.orgAuthPwd 兩個函數, 把明文密碼傳進去然後拿到加密後的密文
1 ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
2 ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
3 scriptEngine.put("a", router_background_password);
4 String functionAlpha = extractFunctionFromJSFile("this.securityEncode");
5 String functionBeta = extractFunctionFromJSFile("this.orgAuthPwd");
6 try {
7 scriptEngine.eval(functionAlpha);
8 scriptEngine.eval(functionBeta);
9 scriptEngine.eval("result=this.orgAuthPwd(\""+ router_background_password +"\")");
10 } catch (ScriptException e) {
11 e.printStackTrace();
12 System.exit(0);
13 }
14 PASSWORD_ENCRYPTED = scriptEngine.get("result").toString();
登錄成功後路由器會返回一個token, 叫做stok, 只要拿著這串token作為憑證發送請求的時候接在URL後面就OK了

時間有限...其後關於收集接入WiFi的設備的信息以及如何告訴管理後台去限速這些技術細節暫且不表, 具體實現請點擊文首GitHub地址或者私信我都是可以的...
我們來看看如何實現文首設想的功能
大體邏輯是這樣的:
首先觀測一段時間, 收集WiFi的速度極限(maxSpeed)是多少(此處假設為2048KB/s)
然後通過收集設備信息得知已接入的設備數量(hostCount)(此處假設為7, 即3電腦+4手機, 宿捨只有四個人, 哈哈羨慕吧...)
此後每3秒(可在配置文件中修改)收集一次設備信息
每12秒(可在配置文件中修改)重新分析並調整各設備限速
為了保證不活躍的設備擁有最低保證的速度, 我們將取WiFi的速度極限的90%作為可調整的速度
首次判定時, 由於情報不足並不能直接做出判斷, 因次我們先將WiFi的速度極限平均除以已接入的設備數量, 即給每一台設備分配(maxSpeed * 0.9 / hostCount)的速度
此處等待12秒進入下一次分析判定
(12秒之後...)
發現其中一台(正在暴力下電影的)電腦貌似對現在的限速不滿意(即(實際速度 / 限速) > 80% ), 在這裡我將它稱為饑餓狀態
又發現其他的設備正處於不活躍的狀態, 即處於飽食狀態.
所以我們在保證不低於最低限速(可在配置文件中修改)的情況下拿走未被利用的速度給正處於饑餓狀態的host
是不是感覺這個機制並沒有什麼卵用?
(過了一小時...)
C突然想上Bilibili看個游戲解說, 無奈B正占著幾乎90%的速度下著電影
於是C只能開始在低限速下先以最高速度緩沖視頻
12秒之後, 程序發現C的限速利用率幾乎到了100%, 所以它立即將C的host標記為饑餓狀態
緊接著, 判定方法發現同時存在多個(>1)處於饑餓狀態的host, 因此他立即抽走處於飽食狀態的host那部分未被利用的速度, 壓縮獨占網速的那台host的速度, 並將可分配速度平均發給每一個饑餓的host
如此反復觀測與判定, 程序便對每個host的速度需求就更加了解, 在出現網速競爭的時候分配也更加公平
這B就能做出小小的犧牲下著電影, C也能流暢的看視頻, 同時A和D刷網頁也不會收到影響啦

分析器部分代碼
1 static JSONObject retrieveSuggestion(HashMap<String, Long> speedSummation, int maxSpeed, HashMap<String, Integer> currentSpeedLimit) {
2 System.out.println("analyzing...");
3 Set<String> keySet = speedSummation.keySet();
4 JSONObject jsonObject_suggestSpeedLimit = new JSONObject();
5 hostCurrentSpeedLimitMap = currentSpeedLimit;
6 if (!isStartUp) {
7 int freeSpeed = maxSpeed;
8 for (String each : keySet) {
9 int averageSpeedOfThisHost = (int) (speedSummation.get(each) / ((int) judge_interval / watch_interval));
10 hostAverageSpeedMap.put(each, averageSpeedOfThisHost);
11 freeSpeed -= averageSpeedOfThisHost;
12 }
13
14 //COLLECT NEEDS FOR MORE SPEED
15 int hungryCount = 0;
16 for (String each : keySet) {
17 hostNeedMoreSpeedMap.put(each, null);
18 int thisCurrentSpeedLimit = currentSpeedLimit.get(each);
19 if (thisCurrentSpeedLimit == 0)
20 thisCurrentSpeedLimit = maxSpeed;
21 double thisRate = (double) hostAverageSpeedMap.get(each) / (double) thisCurrentSpeedLimit;
22 if (thisRate > 0.8) {
23 hostNeedMoreSpeedMap.replace(each, true);
24 hungryCount++;
25 } else if (thisRate < 0.2)
26 hostNeedMoreSpeedMap.replace(each, false);
27 }
28 //GIVE OUT FREE SPEED
29 if (hungryCount > 1) {
30 int hungryHostSpeedLimitSummation;
31 for (String each : keySet) {
32 if (hostNeedMoreSpeedMap.get(each) == null)
33 continue;
34 if (hostNeedMoreSpeedMap.get(each)) {
35 hungryHostSpeedLimitSummation = hostCurrentSpeedLimitMap.get(each);
36 hungryHostSpeedLimitSummation += (freeSpeed * 0.9) / hungryCount;
37 int thisTargetSpeedLimit = hungryHostSpeedLimitSummation;
38 if (thisTargetSpeedLimit < minSpeedLimit)
39 thisTargetSpeedLimit = minSpeedLimit;
40 jsonObject_suggestSpeedLimit.put(each, thisTargetSpeedLimit);
41 }
42 else if (!hostNeedMoreSpeedMap.get(each)) {
43 int thisTargetSpeedLimit = (int) (freeSpeed * 0.1);
44 if (thisTargetSpeedLimit < minSpeedLimit)
45 thisTargetSpeedLimit = minSpeedLimit;
46 jsonObject_suggestSpeedLimit.put(each, thisTargetSpeedLimit);
47 }
48 }
49 } else if (hungryCount == 1) {
50 int moreSpeedPerHungryHost = (int) (freeSpeed * 0.9);
51 for (String each : keySet) {
52 if (hostNeedMoreSpeedMap.get(each) == null)
53 continue;
54 if (hostNeedMoreSpeedMap.get(each)) {
55 int thisTargetSpeedLimit = moreSpeedPerHungryHost;
56 if (thisTargetSpeedLimit < minSpeedLimit)
57 thisTargetSpeedLimit = minSpeedLimit;
58 jsonObject_suggestSpeedLimit.put(each, thisTargetSpeedLimit);
59 }
60 else if (!hostNeedMoreSpeedMap.get(each)) {
61 int thisTargetSpeedLimit = (int) (freeSpeed * 0.1);
62 if (thisTargetSpeedLimit < minSpeedLimit)
63 thisTargetSpeedLimit = minSpeedLimit;
64 jsonObject_suggestSpeedLimit.put(each, thisTargetSpeedLimit);
65 }
66 }
67 }
68 }
69 else {
70 for (String each : keySet) {
71 int thisTargetSpeedLimit = (int) ((maxSpeed * 0.9) / keySet.size());
72 if (thisTargetSpeedLimit < minSpeedLimit)
73 thisTargetSpeedLimit = minSpeedLimit;
74 jsonObject_suggestSpeedLimit.put(each, thisTargetSpeedLimit);
75 }
76 isStartUp = false;
77 }
78 cleanUp();
79 return jsonObject_suggestSpeedLimit;
80 }
看門狗線程部分代碼
1 package thw.jellygo.com;
2
3 import org.json.JSONArray;
4 import org.json.JSONObject;
5 import java.io.UnsupportedEncodingException;
6 import java.net.URLDecoder;
7 import java.util.HashMap;
8 import java.util.Set;
9
10 class WatchDogThread extends Thread {
11 private static HashMap<String, JSONObject> hostsInfo = new HashMap<>();
12 private static HashMap<String, Long> speedSummation = new HashMap<>();
13 private static HashMap<String, Integer> currentSpeedLimit = new HashMap<>();
14 private static ConfigLoader configLoader;
15 private static int maxSpeed;
16
17 public void run() {
18 configLoader = ConfigLoader.getInstance();
19 long watch_interval = configLoader.getWatch_interval();
20 long judge_interval = configLoader.getJudge_interval();
21 int i=0;
22 while (true) {
23 storeHostInformation(RouterBackendManager.listOnlineHosts());
24 try {
25 i++;
26 if (i == (int) judge_interval / watch_interval) {
27 judge();
28 i=0;
29 }
30 sleep(watch_interval);
31 } catch (InterruptedException e) {
32 e.printStackTrace();
33 System.exit(0);
34 }
35 }
36 }
37
38 private void storeHostInformation(JSONObject jsonObject) {
39 configLoader = ConfigLoader.getInstance();
40 JSONObject jsonObject_hostInfo;
41 if (jsonObject.has("hosts_info")) {
42 jsonObject_hostInfo = jsonObject.getJSONObject("hosts_info");
43 JSONArray jsonArray_onlineHost = jsonObject_hostInfo.getJSONArray("online_host");
44 adjustMaxSpeed(jsonArray_onlineHost);
45 for (Object each : jsonArray_onlineHost) {
46 JSONObject jsonObject_aHost = new JSONObject(each.toString());
47 jsonObject_aHost = jsonObject_aHost.getJSONObject(jsonObject_aHost.keys().next());
48 currentSpeedLimit.put(jsonObject_aHost.getString("mac"), jsonObject_aHost.getInt("down_limit"));
49 if (!hostsInfo.containsKey(jsonObject_aHost.getString("mac"))) {
50 hostsInfo.put(jsonObject_aHost.getString("mac"), jsonObject_aHost);
51 speedSummation.put(jsonObject_aHost.getString("mac"), jsonObject_aHost.getLong("down_speed") / 1024);
52 } else {
53 hostsInfo.replace(jsonObject_aHost.getString("mac"), jsonObject_aHost);
54 speedSummation.replace(jsonObject_aHost.getString("mac"), speedSummation.get(jsonObject_aHost.getString("mac")) + jsonObject_aHost.getLong("down_speed") / 1024);
55 }
56 }
57 }
58 // System.out.println(speedSummary.toString());
59 }
60
61 private static void judge() {
62 HostAnalyzer.getInstance();
63 JSONObject jsonObject_suggestSpeedLimit = HostAnalyzer.retrieveSuggestion(speedSummation, maxSpeed, currentSpeedLimit);
64 Set<String> keySet = jsonObject_suggestSpeedLimit.keySet();
65 for (String each : keySet)
66 act(each, jsonObject_suggestSpeedLimit.getInt(each));
67 speedSummation.clear();
68 currentSpeedLimit.clear();
69 hostsInfo.clear();
70 }
71
72 private static void act(String key, int suggestSpeedLimit) {
73 String hostname = null;
74 try {
75 hostname = URLDecoder.decode(hostsInfo.get(key).getString("hostname"), "UTF-8");
76 } catch (UnsupportedEncodingException e) {
77 e.printStackTrace();
78 System.exit(0);
79 }
80 JSONObject jsonObject = new JSONObject();
81 JSONObject jsonObject_setBlockFlag = new JSONObject();
82 jsonObject_setBlockFlag.put("mac", key);
83 jsonObject_setBlockFlag.put("down_limit", suggestSpeedLimit);
84 System.out.println(hostname+" -> "+suggestSpeedLimit+" KB/s");
85 jsonObject_setBlockFlag.put("is_blocked", "0");
86 jsonObject_setBlockFlag.put("up_limit", "0");
87 jsonObject_setBlockFlag.put("name", hostname);
88 JSONObject jsonObject_hostsInfo = new JSONObject();
89 jsonObject_hostsInfo.put("set_block_flag", jsonObject_setBlockFlag);
90 jsonObject.put("hosts_info", jsonObject_hostsInfo);
91 jsonObject.put("method", "do");
92 RouterBackendManager.setLimitOnHost(jsonObject);
93 }
94
95 private void adjustMaxSpeed(JSONArray jsonArray_onlineHost) {
96 int thisMaxSpeed = 0;
97 for (Object each : jsonArray_onlineHost) {
98 JSONObject jsonObject_aHost = new JSONObject(each.toString());
99 jsonObject_aHost = jsonObject_aHost.getJSONObject(jsonObject_aHost.keys().next());
100 thisMaxSpeed+=jsonObject_aHost.getLong("down_speed") / 1024;
101 }
102 if (thisMaxSpeed > maxSpeed)
103 maxSpeed = thisMaxSpeed;
104 }
105 }