项目地址: 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 (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 }