From 0e4f0f17cd290855e5feb8dfe768c166852e4485 Mon Sep 17 00:00:00 2001 From: icylogic Date: Mon, 10 Aug 2015 19:01:05 +0800 Subject: [PATCH 1/6] Add availability statistics service and a simple strategy Strategy: Simply Choose By Statistics --- .../Service/AvailabilityStatistics.cs | 109 ++++++++++++ .../Controller/ShadowsocksController.cs | 17 ++ .../SimplyChooseByStatisticsStrategy.cs | 160 ++++++++++++++++++ .../Controller/Strategy/StrategyManager.cs | 1 + shadowsocks-csharp/Data/cn.txt | 1 + shadowsocks-csharp/Model/Configuration.cs | 1 + shadowsocks-csharp/View/MenuViewController.cs | 8 + shadowsocks-csharp/shadowsocks-csharp.csproj | 2 + 8 files changed, 299 insertions(+) create mode 100644 shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs create mode 100644 shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs new file mode 100644 index 00000000..52ae5c4b --- /dev/null +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.NetworkInformation; +using System.Threading; +using Shadowsocks.Model; +using System.Reflection; + +namespace Shadowsocks.Controller +{ + class AvailabilityStatistics + { + private static readonly string StatisticsFilesName = "shadowsocks.availability.csv"; + private static readonly string Delimiter = ","; + private static readonly int Timeout = 500; + private static readonly int Repeat = 4; //repeat times every evaluation + private static readonly int Interval = 60 * 60 * 1000; //evaluate proxies every 60 minutes + private Timer timer = null; + private State state = null; + private List servers; + + public static string AvailabilityStatisticsFile; + + //static constructor to initialize every public static fields before refereced + static AvailabilityStatistics() + { + string temppath = Path.GetTempPath(); + AvailabilityStatisticsFile = Path.Combine(temppath, StatisticsFilesName); + } + + public bool Set(bool enabled) + { + try + { + if (enabled) + { + if (timer?.Change(0, Interval) == null) + { + state = new State(); + timer = new Timer(Evaluate, state, 0, Interval); + } + } + else + { + timer?.Dispose(); + } + return true; + } + catch (Exception e) + { + Logging.LogUsefulException(e); + return false; + } + } + + private void Evaluate(object obj) + { + Ping ping = new Ping(); + State state = (State) obj; + foreach (var server in servers) + { + Logging.Debug("eveluating " + server.FriendlyName()); + foreach (var _ in Enumerable.Range(0, Repeat)) + { + //TODO: do simple analyze of data to provide friendly message, like package loss. + string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + //ICMP echo. we can also set options and special bytes + //seems no need to use SendPingAsync + PingReply reply = ping.Send(server.server, Timeout); + state.data = new List>(); + state.data.Add(new KeyValuePair("Timestamp", timestamp)); + state.data.Add(new KeyValuePair("Server", server.FriendlyName())); + state.data.Add(new KeyValuePair("Status", reply.Status.ToString())); + state.data.Add(new KeyValuePair("RoundtripTime", reply.RoundtripTime.ToString())); + //state.data.Add(new KeyValuePair("data", reply.Buffer.ToString())); // The data of reply + Append(state.data); + } + } + } + + private static void Append(List> data) + { + string dataLine = string.Join(Delimiter, data.Select(kv => kv.Value).ToArray()); + string[] lines; + if (!File.Exists(AvailabilityStatisticsFile)) + { + string headerLine = string.Join(Delimiter, data.Select(kv => kv.Key).ToArray()); + lines = new string[] { headerLine, dataLine }; + } + else + { + lines = new string[] { dataLine }; + } + File.AppendAllLines(AvailabilityStatisticsFile, lines); + } + + internal void UpdateConfiguration(Configuration _config) + { + Set(_config.availabilityStatistics); + servers = _config.configs; + } + + private class State + { + public List> data = new List>(); + } + } +} diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index c251400d..2c0442aa 100755 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -25,6 +25,7 @@ namespace Shadowsocks.Controller private StrategyManager _strategyManager; private PolipoRunner polipoRunner; private GFWListUpdater gfwListUpdater; + private AvailabilityStatistics _availabilityStatics; private bool stopped = false; private bool _systemProxyIsDirty = false; @@ -246,6 +247,16 @@ namespace Shadowsocks.Controller } } + public void ToggleAvailabilityStatistics(bool enabled) + { + if (_availabilityStatics != null) + { + _availabilityStatics.Set(enabled); + _config.availabilityStatistics = enabled; + SaveConfig(_config); + } + } + public void SavePACUrl(string pacUrl) { _config.pacUrl = pacUrl; @@ -295,6 +306,12 @@ namespace Shadowsocks.Controller _listener.Stop(); } + if (_availabilityStatics == null) + { + _availabilityStatics = new AvailabilityStatistics(); + _availabilityStatics.UpdateConfiguration(_config); + } + // don't put polipoRunner.Start() before pacServer.Stop() // or bind will fail when switching bind address from 0.0.0.0 to 127.0.0.1 // though UseShellExecute is set to true now diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs new file mode 100644 index 00000000..9ca63576 --- /dev/null +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using Shadowsocks.Model; +using System.IO; +using System.Net.NetworkInformation; +using System.Threading; + +namespace Shadowsocks.Controller.Strategy +{ + class SimplyChooseByStatisticsStrategy : IStrategy + { + private ShadowsocksController _controller; + private Server _currentServer; + private Timer timer; + private Dictionary statistics; + private static readonly int CachedInterval = 60 * 60 * 1000; //choose a new server every 60 minutes + + public SimplyChooseByStatisticsStrategy(ShadowsocksController controller) + { + _controller = controller; + _currentServer = null; //we can also choose a server randomly at first + timer = new Timer(ReloadStatisticsAndChooseAServer); + } + + private void ReloadStatisticsAndChooseAServer(object obj) + { + Logging.Debug("Reloading statistics and choose a new server...."); + List servers = _controller.GetCurrentConfiguration().configs; + LoadStatistics(); + ChooseNewServer(servers); + } + + /* + return a dict: + { + 'ServerFriendlyName1':StatisticsData, + 'ServerFriendlyName2':... + } + */ + private void LoadStatistics() + { + try + { + var path = AvailabilityStatistics.AvailabilityStatisticsFile; + Logging.Debug(string.Format("loading statistics from{0}", path)); + statistics = (from l in File.ReadAllLines(path) + .Skip(1) + let strings = l.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + let rawData = new + { + ServerName = strings[1], + IPStatus = strings[2], + RoundtripTime = int.Parse(strings[3]) + } + group rawData by rawData.ServerName into server + select new + { + ServerName = server.Key, + data = new StatisticsData + { + SuccessTimes = server.Count(data => IPStatus.Success.ToString().Equals(data.IPStatus)), + TimedOutTimes = server.Count(data => IPStatus.TimedOut.ToString().Equals(data.IPStatus)), + AverageResponse = Convert.ToInt32(server.Average(data => data.RoundtripTime)), + MinResponse = server.Min(data => data.RoundtripTime), + MaxResponse = server.Max(data => data.RoundtripTime) + } + }).ToDictionary(server => server.ServerName, server => server.data); + } + catch (Exception e) + { + Logging.LogUsefulException(e); + } + } + + //return the score by data + //server with highest score will be choosen + private static double GetScore(StatisticsData data) + { + return (double)data.SuccessTimes / (data.SuccessTimes + data.TimedOutTimes); //simply choose min package loss + } + + private class StatisticsData + { + public int SuccessTimes; + public int TimedOutTimes; + public int AverageResponse; + public int MinResponse; + public int MaxResponse; + } + + private void ChooseNewServer(List servers) + { + try + { + var bestResult = (from server in servers + let name = server.FriendlyName() + where statistics.ContainsKey(name) + select new + { + server, + score = GetScore(statistics[name]) + } + ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); + + Logging.Debug(string.Format("best server {0}: {1}", bestResult.server.FriendlyName(), bestResult.score)); + Console.WriteLine("Switch to server by statistics: {0}", bestResult.server.FriendlyName()); + _currentServer = bestResult.server; + } + catch (Exception e) + { + Logging.LogUsefulException(e); + } + } + + string IStrategy.ID + { + get { return "com.shadowsocks.strategy.scbs"; } + } + + string IStrategy.Name + { + get { return I18N.GetString("Simply Choose By Statics"); } + } + + Server IStrategy.GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) + { + return _currentServer; //current server cached for CachedInterval + } + + void IStrategy.ReloadServers() + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + timer?.Change(0, CachedInterval); + } + + void IStrategy.SetFailure(Server server) + { + Logging.Debug(String.Format("failure: {0}", server.FriendlyName())); + } + + void IStrategy.UpdateLastRead(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + void IStrategy.UpdateLastWrite(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + void IStrategy.UpdateLatency(Server server, TimeSpan latency) + { + //TODO: combine this part of data with ICMP statics + } + + } +} diff --git a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs index be8869da..dd1f705c 100644 --- a/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs +++ b/shadowsocks-csharp/Controller/Strategy/StrategyManager.cs @@ -13,6 +13,7 @@ namespace Shadowsocks.Controller.Strategy _strategies = new List(); _strategies.Add(new BalancingStrategy(controller)); _strategies.Add(new HighAvailabilityStrategy(controller)); + _strategies.Add(new SimplyChooseByStatisticsStrategy(controller)); // TODO: load DLL plugins } public IList GetStrategies() diff --git a/shadowsocks-csharp/Data/cn.txt b/shadowsocks-csharp/Data/cn.txt index da498e42..9f6c7152 100644 --- a/shadowsocks-csharp/Data/cn.txt +++ b/shadowsocks-csharp/Data/cn.txt @@ -25,6 +25,7 @@ Quit=退出 Edit Servers=编辑服务器 Load Balance=负载均衡 High Availability=高可用 +Simply Choose By Statistics=根据统计 # Config Form diff --git a/shadowsocks-csharp/Model/Configuration.cs b/shadowsocks-csharp/Model/Configuration.cs index e545582e..1ccba56c 100755 --- a/shadowsocks-csharp/Model/Configuration.cs +++ b/shadowsocks-csharp/Model/Configuration.cs @@ -22,6 +22,7 @@ namespace Shadowsocks.Model public int localPort; public string pacUrl; public bool useOnlinePac; + public bool availabilityStatistics; private static string CONFIG_FILE = "gui-config.json"; diff --git a/shadowsocks-csharp/View/MenuViewController.cs b/shadowsocks-csharp/View/MenuViewController.cs index 620519a6..b8c9bb81 100755 --- a/shadowsocks-csharp/View/MenuViewController.cs +++ b/shadowsocks-csharp/View/MenuViewController.cs @@ -29,6 +29,7 @@ namespace Shadowsocks.View private MenuItem enableItem; private MenuItem modeItem; private MenuItem AutoStartupItem; + private MenuItem AvailabilityStatistics; private MenuItem ShareOverLANItem; private MenuItem SeperatorItem; private MenuItem ConfigItem; @@ -177,6 +178,7 @@ namespace Shadowsocks.View }), new MenuItem("-"), this.AutoStartupItem = CreateMenuItem("Start on Boot", new EventHandler(this.AutoStartupItem_Click)), + this.AvailabilityStatistics = CreateMenuItem("Availability Statistics", new EventHandler(this.AvailabilityStatisticsItem_Click)), this.ShareOverLANItem = CreateMenuItem("Allow Clients from LAN", new EventHandler(this.ShareOverLANItem_Click)), new MenuItem("-"), CreateMenuItem("Show Logs...", new EventHandler(this.ShowLogItem_Click)), @@ -260,6 +262,7 @@ namespace Shadowsocks.View PACModeItem.Checked = !config.global; ShareOverLANItem.Checked = config.shareOverLan; AutoStartupItem.Checked = AutoStartup.Check(); + AvailabilityStatistics.Checked = config.availabilityStatistics; onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; localPACItem.Checked = !onlinePACItem.Checked; UpdatePACItemsEnabledStatus(); @@ -524,6 +527,11 @@ namespace Shadowsocks.View } } + private void AvailabilityStatisticsItem_Click(object sender, EventArgs e) { + AvailabilityStatistics.Checked = !AvailabilityStatistics.Checked; + controller.ToggleAvailabilityStatistics(AvailabilityStatistics.Checked); + } + private void LocalPACItem_Click(object sender, EventArgs e) { if (!localPACItem.Checked) diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index cfcd70a1..4a4b6db7 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -123,7 +123,9 @@ + + From b36b25af069dbfe800ac88f9fb47c1510d5499a2 Mon Sep 17 00:00:00 2001 From: icylogic Date: Tue, 11 Aug 2015 10:56:12 +0800 Subject: [PATCH 2/6] Avoid output when disabled --- .../Controller/Strategy/SimplyChooseByStatisticsStrategy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs index 9ca63576..21a26666 100644 --- a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -106,7 +106,6 @@ namespace Shadowsocks.Controller.Strategy ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); Logging.Debug(string.Format("best server {0}: {1}", bestResult.server.FriendlyName(), bestResult.score)); - Console.WriteLine("Switch to server by statistics: {0}", bestResult.server.FriendlyName()); _currentServer = bestResult.server; } catch (Exception e) @@ -127,6 +126,7 @@ namespace Shadowsocks.Controller.Strategy Server IStrategy.GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) { + Console.WriteLine("Switch to server by statistics: {0}", _currentServer.FriendlyName()); return _currentServer; //current server cached for CachedInterval } From 295beaf8ee1322be103c651a0cd555b245ab556f Mon Sep 17 00:00:00 2001 From: icylogic Date: Tue, 11 Aug 2015 11:13:19 +0800 Subject: [PATCH 3/6] prevent null reference exception log --- .../Controller/Strategy/SimplyChooseByStatisticsStrategy.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs index 21a26666..faf86d23 100644 --- a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -93,6 +93,10 @@ namespace Shadowsocks.Controller.Strategy private void ChooseNewServer(List servers) { + if (statistics == null) + { + return; + } try { var bestResult = (from server in servers From 40cb772823d6542b21fa6506052567c551a73ffe Mon Sep 17 00:00:00 2001 From: icylogic Date: Tue, 11 Aug 2015 11:41:27 +0800 Subject: [PATCH 4/6] adjust description --- .../Controller/Strategy/SimplyChooseByStatisticsStrategy.cs | 2 +- shadowsocks-csharp/Data/cn.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs index faf86d23..77b2d2dc 100644 --- a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -125,7 +125,7 @@ namespace Shadowsocks.Controller.Strategy string IStrategy.Name { - get { return I18N.GetString("Simply Choose By Statics"); } + get { return I18N.GetString("Choose By Total Package Loss"); } } Server IStrategy.GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) diff --git a/shadowsocks-csharp/Data/cn.txt b/shadowsocks-csharp/Data/cn.txt index 9f6c7152..b4c7a6ee 100644 --- a/shadowsocks-csharp/Data/cn.txt +++ b/shadowsocks-csharp/Data/cn.txt @@ -25,7 +25,7 @@ Quit=退出 Edit Servers=编辑服务器 Load Balance=负载均衡 High Availability=高可用 -Simply Choose By Statistics=根据统计 +Choose By Total Package Loss=累计丢包率 # Config Form From 56ec95862c451b88d1339e95056774c590aeb3ea Mon Sep 17 00:00:00 2001 From: icylogic Date: Tue, 11 Aug 2015 12:16:26 +0800 Subject: [PATCH 5/6] implicitly implement --- .../SimplyChooseByStatisticsStrategy.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs index 77b2d2dc..dc098b8b 100644 --- a/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -16,12 +16,14 @@ namespace Shadowsocks.Controller.Strategy private Server _currentServer; private Timer timer; private Dictionary statistics; - private static readonly int CachedInterval = 60 * 60 * 1000; //choose a new server every 60 minutes + private static readonly int CachedInterval = 30 * 60 * 1000; //choose a new server every 30 minutes public SimplyChooseByStatisticsStrategy(ShadowsocksController controller) { _controller = controller; - _currentServer = null; //we can also choose a server randomly at first + var servers = controller.GetCurrentConfiguration().configs; + int randomIndex = new Random().Next() % servers.Count(); + _currentServer = servers[randomIndex]; //choose a server randomly at first timer = new Timer(ReloadStatisticsAndChooseAServer); } @@ -109,7 +111,10 @@ namespace Shadowsocks.Controller.Strategy } ).Aggregate((result1, result2) => result1.score > result2.score ? result1 : result2); - Logging.Debug(string.Format("best server {0}: {1}", bestResult.server.FriendlyName(), bestResult.score)); + if (_controller.GetCurrentStrategy().ID == ID && _currentServer != bestResult.server) //output when enabled + { + Console.WriteLine("Switch to server: {0} by package loss:{1}", bestResult.server.FriendlyName(), 1 - bestResult.score); + } _currentServer = bestResult.server; } catch (Exception e) @@ -118,44 +123,51 @@ namespace Shadowsocks.Controller.Strategy } } - string IStrategy.ID + public string ID { get { return "com.shadowsocks.strategy.scbs"; } } - string IStrategy.Name + public string Name { get { return I18N.GetString("Choose By Total Package Loss"); } } - Server IStrategy.GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) { - Console.WriteLine("Switch to server by statistics: {0}", _currentServer.FriendlyName()); + var oldServer = _currentServer; + if (oldServer == null) + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + } + if (oldServer != _currentServer) + { + } return _currentServer; //current server cached for CachedInterval } - void IStrategy.ReloadServers() + public void ReloadServers() { ChooseNewServer(_controller.GetCurrentConfiguration().configs); timer?.Change(0, CachedInterval); } - void IStrategy.SetFailure(Server server) + public void SetFailure(Server server) { Logging.Debug(String.Format("failure: {0}", server.FriendlyName())); } - void IStrategy.UpdateLastRead(Server server) + public void UpdateLastRead(Server server) { //TODO: combine this part of data with ICMP statics } - void IStrategy.UpdateLastWrite(Server server) + public void UpdateLastWrite(Server server) { //TODO: combine this part of data with ICMP statics } - void IStrategy.UpdateLatency(Server server, TimeSpan latency) + public void UpdateLatency(Server server, TimeSpan latency) { //TODO: combine this part of data with ICMP statics } From 26557bfdc35e6073b87bd72627e0d511150a07bf Mon Sep 17 00:00:00 2001 From: icylogic Date: Tue, 11 Aug 2015 12:17:03 +0800 Subject: [PATCH 6/6] adjust interval of statistics --- shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs index 52ae5c4b..5a95dbc2 100644 --- a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs +++ b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs @@ -15,7 +15,7 @@ namespace Shadowsocks.Controller private static readonly string Delimiter = ","; private static readonly int Timeout = 500; private static readonly int Repeat = 4; //repeat times every evaluation - private static readonly int Interval = 60 * 60 * 1000; //evaluate proxies every 60 minutes + private static readonly int Interval = 10 * 60 * 1000; //evaluate proxies every 15 minutes private Timer timer = null; private State state = null; private List servers;