From 0e4f0f17cd290855e5feb8dfe768c166852e4485 Mon Sep 17 00:00:00 2001 From: icylogic Date: Mon, 10 Aug 2015 19:01:05 +0800 Subject: [PATCH] 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 @@ + +