diff --git a/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs b/shadowsocks-csharp/Controller/Service/AvailabilityStatistics.cs new file mode 100644 index 00000000..5a95dbc2 --- /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 = 10 * 60 * 1000; //evaluate proxies every 15 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..dc098b8b --- /dev/null +++ b/shadowsocks-csharp/Controller/Strategy/SimplyChooseByStatisticsStrategy.cs @@ -0,0 +1,176 @@ +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 = 30 * 60 * 1000; //choose a new server every 30 minutes + + public SimplyChooseByStatisticsStrategy(ShadowsocksController controller) + { + _controller = controller; + 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); + } + + 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) + { + if (statistics == null) + { + return; + } + 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); + + 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) + { + Logging.LogUsefulException(e); + } + } + + public string ID + { + get { return "com.shadowsocks.strategy.scbs"; } + } + + public string Name + { + get { return I18N.GetString("Choose By Total Package Loss"); } + } + + public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint) + { + var oldServer = _currentServer; + if (oldServer == null) + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + } + if (oldServer != _currentServer) + { + } + return _currentServer; //current server cached for CachedInterval + } + + public void ReloadServers() + { + ChooseNewServer(_controller.GetCurrentConfiguration().configs); + timer?.Change(0, CachedInterval); + } + + public void SetFailure(Server server) + { + Logging.Debug(String.Format("failure: {0}", server.FriendlyName())); + } + + public void UpdateLastRead(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + public void UpdateLastWrite(Server server) + { + //TODO: combine this part of data with ICMP statics + } + + public void 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..b4c7a6ee 100644 --- a/shadowsocks-csharp/Data/cn.txt +++ b/shadowsocks-csharp/Data/cn.txt @@ -25,6 +25,7 @@ Quit=退出 Edit Servers=编辑服务器 Load Balance=负载均衡 High Availability=高可用 +Choose By Total Package Loss=累计丢包率 # 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 @@ + +