Add availability statistics service and a simple strategy
Strategy: Simply Choose By Statistics
This commit is contained in:
@@ -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<Server> 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<KeyValuePair<string, string>>();
|
||||
state.data.Add(new KeyValuePair<string, string>("Timestamp", timestamp));
|
||||
state.data.Add(new KeyValuePair<string, string>("Server", server.FriendlyName()));
|
||||
state.data.Add(new KeyValuePair<string, string>("Status", reply.Status.ToString()));
|
||||
state.data.Add(new KeyValuePair<string, string>("RoundtripTime", reply.RoundtripTime.ToString()));
|
||||
//state.data.Add(new KeyValuePair<string, string>("data", reply.Buffer.ToString())); // The data of reply
|
||||
Append(state.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Append(List<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> data = new List<KeyValuePair<string, string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, StatisticsData> 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<Server> 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<Server> 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace Shadowsocks.Controller.Strategy
|
||||
_strategies = new List<IStrategy>();
|
||||
_strategies.Add(new BalancingStrategy(controller));
|
||||
_strategies.Add(new HighAvailabilityStrategy(controller));
|
||||
_strategies.Add(new SimplyChooseByStatisticsStrategy(controller));
|
||||
// TODO: load DLL plugins
|
||||
}
|
||||
public IList<IStrategy> GetStrategies()
|
||||
|
||||
@@ -25,6 +25,7 @@ Quit=退出
|
||||
Edit Servers=编辑服务器
|
||||
Load Balance=负载均衡
|
||||
High Availability=高可用
|
||||
Simply Choose By Statistics=根据统计
|
||||
|
||||
# Config Form
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -123,7 +123,9 @@
|
||||
<Compile Include="3rd\zxing\ResultPoint.cs" />
|
||||
<Compile Include="3rd\zxing\ResultPointCallback.cs" />
|
||||
<Compile Include="3rd\zxing\WriterException.cs" />
|
||||
<Compile Include="Controller\Service\AvailabilityStatistics.cs" />
|
||||
<Compile Include="Controller\Strategy\HighAvailabilityStrategy.cs" />
|
||||
<Compile Include="Controller\Strategy\SimplyChooseByStatisticsStrategy.cs" />
|
||||
<Compile Include="Controller\System\AutoStartup.cs" />
|
||||
<Compile Include="Controller\FileManager.cs" />
|
||||
<Compile Include="Controller\Service\GFWListUpdater.cs" />
|
||||
|
||||
Reference in New Issue
Block a user