diff --git a/.gitignore b/.gitignore index 3d73a62..72bc3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ *.suo *.user *.sln.docstates +.vs/ # Build diff --git a/Netling.Client/App.config b/Netling.Client/App.config index 29ccdb5..bb7f757 100644 --- a/Netling.Client/App.config +++ b/Netling.Client/App.config @@ -1,11 +1,11 @@ - + - + diff --git a/Netling.Client/MainWindow.xaml b/Netling.Client/MainWindow.xaml index 1bd2d0f..4541f69 100644 --- a/Netling.Client/MainWindow.xaml +++ b/Netling.Client/MainWindow.xaml @@ -24,34 +24,6 @@ 10000 runs on 1 thread - - - - 1 - 2 - 3 - 4 - 5 - 10 - 20 - 30 - 40 - 50 - 60 - 70 - 80 - 90 - 100 - 200 - 300 - 400 - 500 - - - - - - diff --git a/Netling.Client/MainWindow.xaml.cs b/Netling.Client/MainWindow.xaml.cs index 650d7cb..e81a317 100644 --- a/Netling.Client/MainWindow.xaml.cs +++ b/Netling.Client/MainWindow.xaml.cs @@ -9,6 +9,7 @@ using System.Windows.Input; using Netling.Core; using Netling.Core.Models; +using Netling.Core.SocketWorker; namespace Netling.Client { @@ -55,8 +56,6 @@ private async void StartButton_Click(object sender, RoutedEventArgs e) var duration = default(TimeSpan); int? count = null; var threads = Convert.ToInt32(((KeyValuePair)Threads.SelectionBoxItem).Key); - var threadAffinity = ThreadAffinity.IsChecked.HasValue && ThreadAffinity.IsChecked.Value; - var pipelining = Convert.ToInt32(Pipelining.SelectionBoxItem); var durationText = (string)((ComboBoxItem)Duration.SelectedItem).Content; StatusProgressbar.IsIndeterminate = false; @@ -107,16 +106,12 @@ private async void StartButton_Click(object sender, RoutedEventArgs e) if (string.IsNullOrWhiteSpace(Url.Text)) return; - Uri uri; - - if (!Uri.TryCreate(Url.Text.Trim(), UriKind.Absolute, out uri)) + if (!Uri.TryCreate(Url.Text.Trim(), UriKind.Absolute, out var uri)) return; Threads.IsEnabled = false; Duration.IsEnabled = false; Url.IsEnabled = false; - Pipelining.IsEnabled = false; - ThreadAffinity.IsEnabled = false; StartButton.Content = "Cancel"; _running = true; @@ -126,10 +121,12 @@ private async void StartButton_Click(object sender, RoutedEventArgs e) StatusProgressbar.Value = 0; StatusProgressbar.Visibility = Visibility.Visible; + var worker = new Worker(new SocketWorkerJob(uri)); + if (count.HasValue) - _task = Worker.Run(uri, count.Value, cancellationToken); + _task = worker.Run(count.Value, cancellationToken); else - _task = Worker.Run(uri, threads, threadAffinity, pipelining, duration, cancellationToken); + _task = worker.Run(threads, duration, cancellationToken); _task.GetAwaiter().OnCompleted(async () => { @@ -172,12 +169,13 @@ private void Urls_OnKeyUp(object sender, KeyEventArgs e) private async Task JobCompleted() { + if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) + _cancellationTokenSource.Cancel(); + _running = false; Threads.IsEnabled = true; Duration.IsEnabled = true; Url.IsEnabled = true; - Pipelining.IsEnabled = true; - ThreadAffinity.IsEnabled = true; StartButton.IsEnabled = false; _cancellationTokenSource = null; diff --git a/Netling.Client/Netling.Client.csproj b/Netling.Client/Netling.Client.csproj index 850c19e..46bbf23 100644 --- a/Netling.Client/Netling.Client.csproj +++ b/Netling.Client/Netling.Client.csproj @@ -9,7 +9,7 @@ Properties Netling.Client Netling.Client - v4.6.1 + v4.7.1 512 {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 @@ -110,6 +110,7 @@ + @@ -193,6 +194,14 @@ + + {1098eaa8-0341-44e9-8fde-d05a2a56e083} + Netling.Core.HttpClientWorker + + + {10e2f65d-8993-409a-abe5-59cc2697cb32} + Netling.Core.SocketWorker + {84ceec3f-5fcf-4ca5-a6d7-f83cf7ea0b52} Netling.Core diff --git a/Netling.Client/Properties/Resources.Designer.cs b/Netling.Client/Properties/Resources.Designer.cs index 13396ab..b659e0d 100644 --- a/Netling.Client/Properties/Resources.Designer.cs +++ b/Netling.Client/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Netling.Client.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/Netling.Client/Properties/Settings.Designer.cs b/Netling.Client/Properties/Settings.Designer.cs index ed7f539..114e4c9 100644 --- a/Netling.Client/Properties/Settings.Designer.cs +++ b/Netling.Client/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Netling.Client.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/Netling.Client/ResultWindow.xaml b/Netling.Client/ResultWindow.xaml index 4b23dba..ff70bcd 100644 --- a/Netling.Client/ResultWindow.xaml +++ b/Netling.Client/ResultWindow.xaml @@ -17,8 +17,6 @@ - - diff --git a/Netling.Client/ResultWindow.xaml.cs b/Netling.Client/ResultWindow.xaml.cs index 623eec3..7e5112a 100644 --- a/Netling.Client/ResultWindow.xaml.cs +++ b/Netling.Client/ResultWindow.xaml.cs @@ -32,8 +32,6 @@ public async Task Load(WorkerResult workerResult) Title = "Netling - " + _resultWindowItem.Url; ThreadsValueUserControl.Value = _resultWindowItem.Threads.ToString(); - PipeliningValueUserControl.Value = _resultWindowItem.Pipelining.ToString(); - ThreadAffinityValueUserControl.Value = _resultWindowItem.ThreadAffinity ? "ON" : "OFF"; RequestsValueUserControl.Value = _resultWindowItem.JobsPerSecond.ToString("#,0"); ElapsedValueUserControl.Value = $"{_resultWindowItem.ElapsedSeconds:0}"; @@ -94,8 +92,6 @@ private void ClearBaseline(object sender, RoutedEventArgs e) { _sender.ResultWindowItem = null; ThreadsValueUserControl.BaselineValue = null; - PipeliningValueUserControl.BaselineValue = null; - ThreadAffinityValueUserControl.BaselineValue = null; RequestsValueUserControl.BaselineValue = null; RequestsValueUserControl.BaseLine = BaseLine.Equal; @@ -125,8 +121,6 @@ private void ClearBaseline(object sender, RoutedEventArgs e) private void LoadBaseline(ResultWindowItem baseline) { ThreadsValueUserControl.BaselineValue = baseline.Threads.ToString(); - PipeliningValueUserControl.BaselineValue = baseline.Pipelining.ToString(); - ThreadAffinityValueUserControl.BaselineValue = baseline.ThreadAffinity ? "ON" : "OFF"; RequestsValueUserControl.BaselineValue = $"{baseline.JobsPerSecond:#,0}"; RequestsValueUserControl.BaseLine = GetBaseline(_resultWindowItem.JobsPerSecond, baseline.JobsPerSecond); diff --git a/Netling.Client/ResultWindowItem.cs b/Netling.Client/ResultWindowItem.cs index 5e0822a..8d414a6 100644 --- a/Netling.Client/ResultWindowItem.cs +++ b/Netling.Client/ResultWindowItem.cs @@ -8,10 +8,7 @@ public static ResultWindowItem Parse(WorkerResult result) { return new ResultWindowItem { - Url = result.Url, Threads = result.Threads, - Pipelining = result.Pipelining, - ThreadAffinity = result.ThreadAffinity, JobsPerSecond = result.RequestsPerSecond, ElapsedSeconds = result.Elapsed.TotalSeconds, @@ -29,8 +26,6 @@ public static ResultWindowItem Parse(WorkerResult result) public double Bandwidth { get; set; } public double ElapsedSeconds { get; set; } public double JobsPerSecond { get; set; } - public bool ThreadAffinity { get; set; } - public int Pipelining { get; set; } public int Threads { get; set; } public string Url { get; set; } public double Max { get; set; } diff --git a/Netling.ConsoleClient/Netling.ConsoleClient.csproj b/Netling.ConsoleClient/Netling.ConsoleClient.csproj index a972000..d1e4226 100644 --- a/Netling.ConsoleClient/Netling.ConsoleClient.csproj +++ b/Netling.ConsoleClient/Netling.ConsoleClient.csproj @@ -1,138 +1,19 @@ - - - + + - Debug - AnyCPU - {170E12EF-4094-4B8B-93D3-4A399F81BE9D} Exe - Properties - Netling.ConsoleClient - netling - v4.6.1 - 512 - true - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - false - true + netcoreapp2.1 + true - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - true - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - true - - - - ..\packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll - True - - - - - - - - - - - - - - - - - - + - - False - Microsoft .NET Framework 4.6.1 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - + + - - {84ceec3f-5fcf-4ca5-a6d7-f83cf7ea0b52} - Netling.Core - + + + - - - \ No newline at end of file + + diff --git a/Netling.ConsoleClient/Program.cs b/Netling.ConsoleClient/Program.cs index 43d2df3..2ec45c7 100644 --- a/Netling.ConsoleClient/Program.cs +++ b/Netling.ConsoleClient/Program.cs @@ -3,9 +3,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using NDesk.Options; +using CommandLine.Options; using Netling.Core; using Netling.Core.Models; +using Netling.Core.SocketWorker; namespace Netling.ConsoleClient { @@ -17,20 +18,17 @@ static void Main(string[] args) Thread.CurrentThread.CurrentCulture = new CultureInfo("en"); var threads = 1; - var pipelining = 1; var duration = 10; int? count = null; var p = new OptionSet() { {"t|threads=", (int v) => threads = v}, - {"p|pipelining=", (int v) => pipelining = v}, {"d|duration=", (int v) => duration = v}, - {"c|count=", (int? v) => count = v}, + {"c|count=", (int? v) => count = v} }; var extraArgs = p.Parse(args); - var threadAffinity = extraArgs.Contains("-a"); var url = extraArgs.FirstOrDefault(e => e.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || e.StartsWith("https://", StringComparison.OrdinalIgnoreCase)); Uri uri = null; @@ -39,7 +37,7 @@ static void Main(string[] args) else if (url != null && count.HasValue) RunWithCount(uri, count.Value).Wait(); else if (url != null) - RunWithDuration(uri, threads, threadAffinity, pipelining, TimeSpan.FromSeconds(duration)).Wait(); + RunWithDuration(uri, threads, TimeSpan.FromSeconds(duration)).Wait(); else ShowHelp(); } @@ -52,23 +50,24 @@ private static void ShowHelp() private static Task RunWithCount(Uri uri, int count) { Console.WriteLine(StartRunWithCountString, count, uri); - return Run(uri, 1, false, 1, TimeSpan.MaxValue, count); + return Run(uri, 1, TimeSpan.MaxValue, count); } - private static Task RunWithDuration(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan duration) + private static Task RunWithDuration(Uri uri, int threads, TimeSpan duration) { - Console.WriteLine(StartRunWithDurationString, duration.TotalSeconds, uri, threads, pipelining, threadAffinity ? "ON" : "OFF"); - return Run(uri, threads, threadAffinity, pipelining, duration, null); + Console.WriteLine(StartRunWithDurationString, duration.TotalSeconds, uri, threads); + return Run(uri, threads, duration, null); } - private static async Task Run(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan duration, int? count) + private static async Task Run(Uri uri, int threads, TimeSpan duration, int? count) { WorkerResult result; + var worker = new Worker(new SocketWorkerJob(uri)); if (count.HasValue) - result = await Worker.Run(uri, count.Value, new CancellationToken()); - else - result = await Worker.Run(uri, threads, threadAffinity, pipelining, duration, new CancellationToken()); + result = await worker.Run(count.Value, new CancellationToken()); + else + result = await worker.Run(threads, duration, new CancellationToken()); Console.WriteLine(ResultString, result.Count, @@ -109,29 +108,24 @@ private static string GetAsciiHistogram(WorkerResult workerResult) } private const string HelpString = @" -Usage: netling [-t threads] [-d duration] [-p pipelining] [-a] url +Usage: netling [-t threads] [-d duration] url Options: -t count Number of threads to spawn. -d count Duration of the run in seconds. -c count Amount of requests to send on a single thread. - -p count Number of requests to pipeline. - -a Use thread affinity on the worker threads. -Examples: - netling http://localhost -t 8 -d 60 - netling http://localhost -c 3000 - netling http://localhost +Examples: + netling http://localhost:5000/ + netling http://localhost:5000/ -t 8 -d 60 + netling http://localhost:5000/ -c 3000 "; private const string StartRunWithCountString = @" Running {0} test @ {1}"; private const string StartRunWithDurationString = @" -Running {0}s test @ {1} - Threads: {2} - Pipelining: {3} - Thread affinity: {4}"; +Running {0}s test with {2} threads @ {1}"; private const string ResultString = @" {0} requests in {1:0.##}s diff --git a/Netling.ConsoleClient/Properties/AssemblyInfo.cs b/Netling.ConsoleClient/Properties/AssemblyInfo.cs deleted file mode 100644 index bbfbb6c..0000000 --- a/Netling.ConsoleClient/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Netling.ConsoleClient")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Netling.ConsoleClient")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("170e12ef-4094-4b8b-93d3-4a399f81be9d")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Netling.Core.HttpClientWorker/HttpClientWorkerJob.cs b/Netling.Core.HttpClientWorker/HttpClientWorkerJob.cs new file mode 100644 index 0000000..2892c8e --- /dev/null +++ b/Netling.Core.HttpClientWorker/HttpClientWorkerJob.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using Netling.Core.Models; + +namespace Netling.Core.HttpClientWorker +{ + public class HttpClientWorkerJob : IWorkerJob + { + private readonly int _index; + private readonly Uri _uri; + private readonly Stopwatch _stopwatch; + private readonly Stopwatch _localStopwatch; + private readonly WorkerThreadResult _workerThreadResult; + private readonly HttpClient _httpClient; + + // Used to approximately calculate bandwidth + private static readonly int MissingHeaderLength = "HTTP/1.1 200 OK\r\nContent-Length: 123\r\nContent-Type: text/plain\r\n\r\n".Length; + + public HttpClientWorkerJob(Uri uri) + { + _uri = uri; + } + + private HttpClientWorkerJob(int index, Uri uri, WorkerThreadResult workerThreadResult) + { + _index = index; + _uri = uri; + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + _localStopwatch = new Stopwatch(); + _workerThreadResult = workerThreadResult; + _httpClient = new HttpClient(); + } + + public async Task DoWork() + { + _localStopwatch.Restart(); + + using (var response = await _httpClient.GetAsync(_uri)) + { + var contentStream = await response.Content.ReadAsStreamAsync(); + var length = contentStream.Length + response.Headers.ToString().Length + MissingHeaderLength; + var responseTime = (float)_localStopwatch.ElapsedTicks / Stopwatch.Frequency * 1000; + + if ((int)response.StatusCode < 400) + _workerThreadResult.Add((int)_stopwatch.ElapsedMilliseconds, length, responseTime, _index < 10); + else + _workerThreadResult.AddError((int)_stopwatch.ElapsedMilliseconds, responseTime, _index < 10); + } + } + + public WorkerThreadResult GetResults() + { + return _workerThreadResult; + } + + public Task Init(int index, WorkerThreadResult workerThreadResult) + { + return Task.FromResult(new HttpClientWorkerJob(index, _uri, workerThreadResult)); + } + } +} \ No newline at end of file diff --git a/Netling.Core.HttpClientWorker/Netling.Core.HttpClientWorker.csproj b/Netling.Core.HttpClientWorker/Netling.Core.HttpClientWorker.csproj new file mode 100644 index 0000000..1b4921d --- /dev/null +++ b/Netling.Core.HttpClientWorker/Netling.Core.HttpClientWorker.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Netling.Core/Extensions/ByteExtensions.cs b/Netling.Core.SocketWorker/Extensions/ByteExtensions.cs similarity index 63% rename from Netling.Core/Extensions/ByteExtensions.cs rename to Netling.Core.SocketWorker/Extensions/ByteExtensions.cs index 32dc585..f24584a 100644 --- a/Netling.Core/Extensions/ByteExtensions.cs +++ b/Netling.Core.SocketWorker/Extensions/ByteExtensions.cs @@ -1,8 +1,8 @@ -namespace Netling.Core.Extensions +namespace Netling.Core.SocketWorker.Extensions { - internal static class ByteExtensions + public static class ByteExtensions { - public static int ConvertToInt(this byte[] bytes, int start, int length, int end) + public static int ConvertToInt(byte[] bytes, int start, int length, int end) { var result = 0; diff --git a/Netling.Core.SocketWorker/Netling.Core.SocketWorker.csproj b/Netling.Core.SocketWorker/Netling.Core.SocketWorker.csproj new file mode 100644 index 0000000..1b4921d --- /dev/null +++ b/Netling.Core.SocketWorker/Netling.Core.SocketWorker.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Netling.Core/Performance/HttpHeaders.cs b/Netling.Core.SocketWorker/Performance/HttpHeaders.cs similarity index 74% rename from Netling.Core/Performance/HttpHeaders.cs rename to Netling.Core.SocketWorker/Performance/HttpHeaders.cs index c2b65e5..2c872ad 100644 --- a/Netling.Core/Performance/HttpHeaders.cs +++ b/Netling.Core.SocketWorker/Performance/HttpHeaders.cs @@ -1,8 +1,8 @@ using System.Text; -namespace Netling.Core.Performance +namespace Netling.Core.SocketWorker.Performance { - internal static class HttpHeaders + public static class HttpHeaders { public static readonly byte[] ContentLength = Encoding.ASCII.GetBytes("\r\nContent-Length: "); public static readonly byte[] TransferEncoding = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: "); diff --git a/Netling.Core/Performance/HttpHelper.cs b/Netling.Core.SocketWorker/Performance/HttpHelper.cs similarity index 72% rename from Netling.Core/Performance/HttpHelper.cs rename to Netling.Core.SocketWorker/Performance/HttpHelper.cs index f7591e8..badc161 100644 --- a/Netling.Core/Performance/HttpHelper.cs +++ b/Netling.Core.SocketWorker/Performance/HttpHelper.cs @@ -1,32 +1,26 @@ -using System; -using System.IO; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using Netling.Core.Extensions; +using System.Runtime.CompilerServices; +using Netling.Core.SocketWorker.Extensions; -namespace Netling.Core.Performance +namespace Netling.Core.SocketWorker.Performance { - internal static class HttpHelper + public static class HttpHelper { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ResponseType GetResponseType(byte[] buffer, int start, int end) { - int index; - int length; - - if (SeekHeader(buffer, HttpHeaders.ContentLength, start, end, out index, out length)) + if (SeekHeader(buffer, HttpHeaders.ContentLength, start, end, out _, out _)) return ResponseType.ContentLength; - if (SeekHeader(buffer, HttpHeaders.TransferEncoding, start, end, out index, out length)) + if (SeekHeader(buffer, HttpHeaders.TransferEncoding, start, end, out _, out _)) return ResponseType.Chunked; return ResponseType.Unknown; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetStatusCode(byte[] buffer, int start, int end) { - return buffer.ConvertToInt(start + 9, 3, end); + return ByteExtensions.ConvertToInt(buffer, start + 9, 3, end); } // HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\nHello, World! @@ -62,7 +56,8 @@ public static bool SeekHeader(byte[] buffer, byte[] header, int start, int end, length = r - index; return true; } - + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int SeekReturn(byte[] buffer, int start, int end) { while (start + 1 < end) @@ -76,6 +71,7 @@ private static int SeekReturn(byte[] buffer, int start, int end) return -1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SeekHeaderEnd(byte[] buffer, int start, int end) { while (start + 3 < end) @@ -91,16 +87,5 @@ public static int SeekHeaderEnd(byte[] buffer, int start, int end) return -1; } - - public static Stream GetStream(TcpClient client, Uri uri) - { - if (uri.Scheme == Uri.UriSchemeHttp) - return client.GetStream(); - - var stream = new SslStream(client.GetStream()); - var xc = new X509Certificate2Collection(); - stream.AuthenticateAsClient(uri.Host, xc, SslProtocols.Tls, false); - return stream; - } } } diff --git a/Netling.Core.SocketWorker/Performance/HttpHelperChunked.cs b/Netling.Core.SocketWorker/Performance/HttpHelperChunked.cs new file mode 100644 index 0000000..9a0d77c --- /dev/null +++ b/Netling.Core.SocketWorker/Performance/HttpHelperChunked.cs @@ -0,0 +1,34 @@ +namespace Netling.Core.SocketWorker.Performance +{ + internal static class HttpHelperChunked + { + public static bool IsEndOfChunkedStream(byte[] buffer, int end) + { + if (end < 5) + return true; + + return buffer[end - 5] == 48 && + buffer[end - 4] == 13 && + buffer[end - 3] == 10 && + buffer[end - 2] == 13 && + buffer[end - 1] == 10; + } + + public static int SeekEndOfChunkedStream(byte[] buffer, int start, int end) + { + while (start + 4 < end) + { + if (buffer[start + 0] == 48 && + buffer[start + 1] == 13 && + buffer[start + 2] == 10 && + buffer[start + 3] == 13 && + buffer[start + 4] == 10) + return start + 5; + + start++; + } + + return -1; + } + } +} diff --git a/Netling.Core/Performance/HttpHelperContentLength.cs b/Netling.Core.SocketWorker/Performance/HttpHelperContentLength.cs similarity index 56% rename from Netling.Core/Performance/HttpHelperContentLength.cs rename to Netling.Core.SocketWorker/Performance/HttpHelperContentLength.cs index 297c7f0..a832ca4 100644 --- a/Netling.Core/Performance/HttpHelperContentLength.cs +++ b/Netling.Core.SocketWorker/Performance/HttpHelperContentLength.cs @@ -1,17 +1,18 @@ -using Netling.Core.Extensions; +using System.Runtime.CompilerServices; +using Netling.Core.SocketWorker.Extensions; -namespace Netling.Core.Performance +namespace Netling.Core.SocketWorker.Performance { - internal static class HttpHelperContentLength + public static class HttpHelperContentLength { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetHeaderContentLength(byte[] buffer, int start, int end) { - int index; - int length; - HttpHelper.SeekHeader(buffer, HttpHeaders.ContentLength, start, end, out index, out length); - return buffer.ConvertToInt(index, length, end); + HttpHelper.SeekHeader(buffer, HttpHeaders.ContentLength, start, end, out var index, out var length); + return ByteExtensions.ConvertToInt(buffer, index, length, end); } - + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetResponseLength(byte[] buffer, int start, int end) { var headerEnd = HttpHelper.SeekHeaderEnd(buffer, start, end); diff --git a/Netling.Core/Performance/HttpMethod.cs b/Netling.Core.SocketWorker/Performance/HttpMethod.cs similarity index 74% rename from Netling.Core/Performance/HttpMethod.cs rename to Netling.Core.SocketWorker/Performance/HttpMethod.cs index ae84bd2..5584d11 100644 --- a/Netling.Core/Performance/HttpMethod.cs +++ b/Netling.Core.SocketWorker/Performance/HttpMethod.cs @@ -1,4 +1,4 @@ -namespace Netling.Core.Performance +namespace Netling.Core.SocketWorker.Performance { public enum HttpMethod { diff --git a/Netling.Core.SocketWorker/Performance/HttpWorker.cs b/Netling.Core.SocketWorker/Performance/HttpWorker.cs new file mode 100644 index 0000000..ab1ef43 --- /dev/null +++ b/Netling.Core.SocketWorker/Performance/HttpWorker.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using Netling.Core.Exceptions; + +namespace Netling.Core.SocketWorker.Performance +{ + public class HttpWorker : IDisposable + { + private readonly IHttpWorkerClient _client; + private readonly byte[] _request; + private readonly byte[] _buffer; + private ResponseType _responseType; + + public HttpWorker(IHttpWorkerClient client, Uri uri, HttpMethod httpMethod = HttpMethod.Get, Dictionary headers = null, byte[] data = null) + { + _client = client; + _buffer = new byte[8192]; + _responseType = ResponseType.Unknown; + var headersString = string.Empty; + var contentLength = data?.Length ?? 0; + + if (headers != null && headers.Any()) + headersString = string.Concat(headers.Select(h => "\r\n" + h.Key.Trim() + ": " + h.Value.Trim())); + + _request = Encoding.UTF8.GetBytes($"{httpMethod.ToString().ToUpper()} {uri.PathAndQuery} HTTP/1.1\r\nAccept-Encoding: gzip, deflate, sdch\r\nHost: {uri.Host}\r\nContent-Length: {contentLength}{headersString}\r\n\r\n"); + + if (data != null) + { + var tmpRequest = new byte[_request.Length + data.Length]; + Buffer.BlockCopy(_request, 0, tmpRequest, 0, _request.Length); + Buffer.BlockCopy(data, 0, tmpRequest, _request.Length, data.Length); + _request = tmpRequest; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write() + { + _client.Write(_request, 0, _request.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Flush() + { + _client.Flush(); + } + + public int Read(out int statusCode) + { + var read = _client.Read(_buffer, 0, _buffer.Length); + var length = read; + statusCode = HttpHelper.GetStatusCode(_buffer, 0, read); + _responseType = HttpHelper.GetResponseType(_buffer, 0, read); + + if (_responseType == ResponseType.ContentLength) + { + var responseLength = HttpHelperContentLength.GetResponseLength(_buffer, 0, read); + + while (length < responseLength) + { + length += _client.Read(_buffer, 0, _buffer.Length); + } + + return length; + } + + if (_responseType == ResponseType.Chunked) + { + while (!HttpHelperChunked.IsEndOfChunkedStream(_buffer, read)) + { + read = _client.Read(_buffer, 0, _buffer.Length); + length += read; + } + + return length; + } + + throw new UnknownResponseTypeException(); + } + + public void Dispose() + { + _client?.Dispose(); + } + } +} diff --git a/Netling.Core.SocketWorker/Performance/HttpWorkerClient.cs b/Netling.Core.SocketWorker/Performance/HttpWorkerClient.cs new file mode 100644 index 0000000..34bce8f --- /dev/null +++ b/Netling.Core.SocketWorker/Performance/HttpWorkerClient.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace Netling.Core.SocketWorker.Performance +{ + public class HttpWorkerClient : IHttpWorkerClient + { + private TcpClient _client; + private readonly IPEndPoint _endPoint; + private readonly Uri _uri; + + public HttpWorkerClient(IPEndPoint endPoint, Uri uri) + { + _endPoint = endPoint; + _uri = uri; + CheckInit(); + } + + public Stream Stream { get; private set; } + + private void CheckInit() + { + if (_client != null && _client.Connected) + return; + + _client?.Close(); + _client = new TcpClient(); + + try + { + const int sioLoopbackFastPath = -1744830448; + var optionInValue = BitConverter.GetBytes(1); + _client.Client.IOControl(sioLoopbackFastPath, optionInValue, null); + } + catch (SocketException) { } + + _client.NoDelay = true; + _client.SendTimeout = 10000; + _client.ReceiveTimeout = 10000; + _client.Connect(_endPoint); + Stream = GetStream(_uri); + } + + private Stream GetStream(Uri uri) + { + if (uri.Scheme == Uri.UriSchemeHttp) + return _client.GetStream(); + + var stream = new SslStream(_client.GetStream()); + var xc = new X509Certificate2Collection(); + stream.AuthenticateAsClient(uri.Host, xc, SslProtocols.Tls12, false); + return stream; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(byte[] buffer, int offset, int count) + { + CheckInit(); + Stream.Write(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Flush() + { + Stream.Flush(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read(byte[] buffer, int offset, int count) + { + return Stream.Read(buffer, offset, count); + } + + public void Dispose() + { + _client?.Close(); + } + } +} \ No newline at end of file diff --git a/Netling.Core.SocketWorker/Performance/IHttpWorkerClient.cs b/Netling.Core.SocketWorker/Performance/IHttpWorkerClient.cs new file mode 100644 index 0000000..d249571 --- /dev/null +++ b/Netling.Core.SocketWorker/Performance/IHttpWorkerClient.cs @@ -0,0 +1,11 @@ +using System; + +namespace Netling.Core.SocketWorker.Performance +{ + public interface IHttpWorkerClient : IDisposable + { + int Read(byte[] buffer, int offset, int count); + void Write(byte[] buffer, int offset, int count); + void Flush(); + } +} \ No newline at end of file diff --git a/Netling.Core.SocketWorker/Performance/ResponseType.cs b/Netling.Core.SocketWorker/Performance/ResponseType.cs new file mode 100644 index 0000000..09d0dc4 --- /dev/null +++ b/Netling.Core.SocketWorker/Performance/ResponseType.cs @@ -0,0 +1,9 @@ +namespace Netling.Core.SocketWorker.Performance +{ + public enum ResponseType + { + Unknown, + Chunked, + ContentLength + } +} \ No newline at end of file diff --git a/Netling.Core.SocketWorker/SocketWorkerJob.cs b/Netling.Core.SocketWorker/SocketWorkerJob.cs new file mode 100644 index 0000000..d201196 --- /dev/null +++ b/Netling.Core.SocketWorker/SocketWorkerJob.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Netling.Core.Models; +using Netling.Core.SocketWorker.Performance; + +namespace Netling.Core.SocketWorker +{ + public class SocketWorkerJob : IWorkerJob + { + private readonly int _index; + private readonly Uri _uri; + private readonly Stopwatch _stopwatch; + private readonly Stopwatch _localStopwatch; + private readonly WorkerThreadResult _workerThreadResult; + private readonly HttpWorker _httpWorker; + + public SocketWorkerJob(Uri uri) + { + _uri = uri; + } + + private SocketWorkerJob(int index, Uri uri, WorkerThreadResult workerThreadResult) + { + _index = index; + _uri = uri; + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + _localStopwatch = new Stopwatch(); + _workerThreadResult = workerThreadResult; + + IPAddress ip; + + if (_uri.HostNameType == UriHostNameType.Dns) + { + var host = Dns.GetHostEntry(_uri.Host); + ip = host.AddressList.First(i => i.AddressFamily == AddressFamily.InterNetwork); + } + else + { + ip = IPAddress.Parse(_uri.Host); + } + + var endPoint = new IPEndPoint(ip, _uri.Port); + _httpWorker = new HttpWorker(new HttpWorkerClient(endPoint, uri), uri); + } + + public Task DoWork() + { + _localStopwatch.Restart(); + _httpWorker.Write(); + _httpWorker.Flush(); + var length = _httpWorker.Read(out var statusCode); + + if (statusCode < 400) + _workerThreadResult.Add((int)_stopwatch.ElapsedMilliseconds, length, (float) _localStopwatch.ElapsedTicks / Stopwatch.Frequency * 1000, _index < 10); + else + _workerThreadResult.AddError((int)_stopwatch.ElapsedMilliseconds, (float) _localStopwatch.ElapsedTicks / Stopwatch.Frequency * 1000, _index < 10); + + return Task.CompletedTask; + } + + public WorkerThreadResult GetResults() + { + return _workerThreadResult; + } + + public Task Init(int index, WorkerThreadResult workerThreadResult) + { + return Task.FromResult(new SocketWorkerJob(index, _uri, workerThreadResult)); + } + } +} \ No newline at end of file diff --git a/Netling.Core/IWorkerJob.cs b/Netling.Core/IWorkerJob.cs new file mode 100644 index 0000000..3f5abc5 --- /dev/null +++ b/Netling.Core/IWorkerJob.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Netling.Core.Models; + +namespace Netling.Core +{ + public interface IWorkerJob + { + Task Init(int index, WorkerThreadResult workerThreadResult); + Task DoWork(); + WorkerThreadResult GetResults(); + } +} \ No newline at end of file diff --git a/Netling.Core/Models/CombinedWorkerThreadResult.cs b/Netling.Core/Models/CombinedWorkerThreadResult.cs index 9eb303a..69b405e 100644 --- a/Netling.Core/Models/CombinedWorkerThreadResult.cs +++ b/Netling.Core/Models/CombinedWorkerThreadResult.cs @@ -4,7 +4,7 @@ namespace Netling.Core.Models { - internal class CombinedWorkerThreadResult + public class CombinedWorkerThreadResult { public Dictionary Seconds { get; private set; } public List> ResponseTimes { get; private set; } diff --git a/Netling.Core/Models/Second.cs b/Netling.Core/Models/Second.cs index cdb4497..4e0cbfc 100644 --- a/Netling.Core/Models/Second.cs +++ b/Netling.Core/Models/Second.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace Netling.Core.Models @@ -7,17 +6,14 @@ public class Second { public long Count { get; set; } public long Bytes { get; private set; } + public long Errors { get; private set; } + public int Elapsed { get; } public List ResponseTimes { get; private set; } - public Dictionary StatusCodes { get; private set; } - public Dictionary Exceptions { get; private set; } - public int Elapsed { get; private set; } public Second(int elapsed) { Elapsed = elapsed; ResponseTimes = new List(); - StatusCodes = new Dictionary(); - Exceptions = new Dictionary(); } internal void ClearResponseTimes() @@ -25,52 +21,29 @@ internal void ClearResponseTimes() ResponseTimes = new List(); } - public void Add(long bytes, float responseTime, int statusCode, bool trackResponseTime) + public void Add(long bytes, float responseTime, bool trackResponseTime) { Count++; Bytes += bytes; if (trackResponseTime) ResponseTimes.Add(responseTime); - - if (StatusCodes.ContainsKey(statusCode)) - StatusCodes[statusCode]++; - else - StatusCodes.Add(statusCode, 1); } - public void AddError(float responseTime, Exception exception) + public void AddError(float responseTime, bool trackResponseTime) { Count++; - ResponseTimes.Add(responseTime); + Errors++; - var exceptionType = exception.GetType(); - if (Exceptions.ContainsKey(exceptionType)) - Exceptions[exceptionType]++; - else - Exceptions.Add(exceptionType, 1); + if (trackResponseTime) + ResponseTimes.Add(responseTime); } public void AddMerged(Second second) { Count += second.Count; Bytes += second.Bytes; - - foreach (var statusCode in second.StatusCodes) - { - if (StatusCodes.ContainsKey(statusCode.Key)) - StatusCodes[statusCode.Key] += statusCode.Value; - else - StatusCodes.Add(statusCode.Key, statusCode.Value); - } - - foreach (var exception in second.Exceptions) - { - if (Exceptions.ContainsKey(exception.Key)) - Exceptions[exception.Key] += exception.Value; - else - Exceptions.Add(exception.Key, exception.Value); - } + Errors += second.Errors; } } } \ No newline at end of file diff --git a/Netling.Core/Models/WorkerResult.cs b/Netling.Core/Models/WorkerResult.cs index b77ca73..f4de11e 100644 --- a/Netling.Core/Models/WorkerResult.cs +++ b/Netling.Core/Models/WorkerResult.cs @@ -7,12 +7,9 @@ namespace Netling.Core.Models { public class WorkerResult { - public WorkerResult(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan elapsed) + public WorkerResult(int threads, TimeSpan elapsed) { - Url = uri.ToString(); Threads = threads; - ThreadAffinity = threadAffinity; - Pipelining = pipelining; Elapsed = elapsed; Seconds = new Dictionary(); Histogram = new int[0]; @@ -20,19 +17,16 @@ public WorkerResult(Uri uri, int threads, bool threadAffinity, int pipelining, T Exceptions = new Dictionary(); } - public string Url { get; private set; } - public int Threads { get; private set; } - public bool ThreadAffinity { get; private set; } - public int Pipelining { get; private set; } - public TimeSpan Elapsed { get; private set; } + public int Threads { get; } + public TimeSpan Elapsed { get; } public long Count { get; private set; } public long Errors { get; private set; } public double RequestsPerSecond { get; private set; } public double BytesPrSecond { get; private set; } - public Dictionary StatusCodes { get; private set; } - public Dictionary Exceptions { get; private set; } + public Dictionary StatusCodes { get; } + public Dictionary Exceptions { get; } public Dictionary Seconds { get; set; } public double Median { get; private set; } @@ -41,36 +35,34 @@ public WorkerResult(Uri uri, int threads, bool threadAffinity, int pipelining, T public double Max { get; private set; } public int[] Histogram { get; private set; } - public double Bandwidth - { - get { return Math.Round(BytesPrSecond * 8 / 1024 / 1024, MidpointRounding.AwayFromZero); } - } + public double Bandwidth => Math.Round(BytesPrSecond * 8 / 1024 / 1024, MidpointRounding.AwayFromZero); - internal void Process(CombinedWorkerThreadResult wtResult) + public void Process(CombinedWorkerThreadResult wtResult) { Seconds = wtResult.Seconds; var items = wtResult.Seconds.Select(r => r.Value).DefaultIfEmpty(new Second(0)).ToList(); Count = items.Sum(s => s.Count); + Errors = items.Sum(s => s.Errors); RequestsPerSecond = Count / (Elapsed.TotalMilliseconds / 1000); BytesPrSecond = items.Sum(s => s.Bytes) / (Elapsed.TotalMilliseconds / 1000); - foreach (var statusCode in items.SelectMany(s => s.StatusCodes)) - { - if (StatusCodes.ContainsKey(statusCode.Key)) - StatusCodes[statusCode.Key] += statusCode.Value; - else - StatusCodes.Add(statusCode.Key, statusCode.Value); - } - - foreach (var exception in items.SelectMany(s => s.Exceptions)) - { - if (Exceptions.ContainsKey(exception.Key)) - Exceptions[exception.Key] += exception.Value; - else - Exceptions.Add(exception.Key, exception.Value); - } - - Errors = StatusCodes.Where(s => s.Key >= 400).Sum(s => s.Value) + Exceptions.Sum(e => e.Value); + //foreach (var statusCode in items.SelectMany(s => s.StatusCodes)) + //{ + // if (StatusCodes.ContainsKey(statusCode.Key)) + // StatusCodes[statusCode.Key] += statusCode.Value; + // else + // StatusCodes.Add(statusCode.Key, statusCode.Value); + //} + + //foreach (var exception in items.SelectMany(s => s.Exceptions)) + //{ + // if (Exceptions.ContainsKey(exception.Key)) + // Exceptions[exception.Key] += exception.Value; + // else + // Exceptions.Add(exception.Key, exception.Value); + //} + + //Errors = StatusCodes.Where(s => s.Key >= 400).Sum(s => s.Value) + Exceptions.Sum(e => e.Value); var responseTimes = GetResponseTimes(wtResult.ResponseTimes); if (!responseTimes.Any()) diff --git a/Netling.Core/Models/WorkerThreadResult.cs b/Netling.Core/Models/WorkerThreadResult.cs index 7715211..7dfadac 100644 --- a/Netling.Core/Models/WorkerThreadResult.cs +++ b/Netling.Core/Models/WorkerThreadResult.cs @@ -1,26 +1,24 @@ -using System; -using System.Collections.Concurrent; using System.Collections.Generic; namespace Netling.Core.Models { - internal class WorkerThreadResult + public class WorkerThreadResult { - public Dictionary Seconds { get; private set; } + public Dictionary Seconds { get; } public WorkerThreadResult() { Seconds = new Dictionary(); } - public void Add(int elapsed, long bytes, float responsetime, int statusCode, bool trackResponseTime) + public void Add(int elapsed, long bytes, float responsetime, bool trackResponseTime) { - GetItem(elapsed).Add(bytes, responsetime, statusCode, trackResponseTime); + GetItem(elapsed).Add(bytes, responsetime, trackResponseTime); } - public void AddError(int elapsed, float responsetime, Exception exception) + public void AddError(int elapsed, float responsetime, bool trackResponseTime) { - GetItem(elapsed).AddError(responsetime, exception); + GetItem(elapsed).AddError(responsetime, trackResponseTime); } private Second GetItem(int elapsed) diff --git a/Netling.Core/Netling.Core.csproj b/Netling.Core/Netling.Core.csproj index 5b8c239..deaac91 100644 --- a/Netling.Core/Netling.Core.csproj +++ b/Netling.Core/Netling.Core.csproj @@ -1,122 +1,13 @@ - - - + + - Debug - AnyCPU - {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52} - Library - Properties - Netling.Core - Netling.Core - v4.6.1 - 512 - + netstandard2.0 + true + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - true - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - prompt - MinimumRecommendedRules.ruleset - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - prompt - MinimumRecommendedRules.ruleset - - - - - - - - ..\packages\System.Numerics.Vectors.4.1.1\lib\net46\System.Numerics.Vectors.dll - True - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.0.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - \ No newline at end of file + + diff --git a/Netling.Core/Performance/HttpHelperChunked.cs b/Netling.Core/Performance/HttpHelperChunked.cs deleted file mode 100644 index 951862e..0000000 --- a/Netling.Core/Performance/HttpHelperChunked.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Numerics; -using Netling.Core.Utils; - -namespace Netling.Core.Performance -{ - internal static class HttpHelperChunked - { - public static bool IsEndOfChunkedStream(byte[] buffer, int end) - { - if (end < 5) - return true; - - return buffer[end - 5] == 48 && - buffer[end - 4] == 13 && - buffer[end - 3] == 10 && - buffer[end - 2] == 13 && - buffer[end - 1] == 10; - } - - private static Vector _zeroReturnVector = new Vector(48 + (13 << 8)); - private static Vector _zeroReturnVectorFiller = new Vector(48 + (13 << 8)); - - public static int SeekEndOfChunkedStream(byte[] buffer, int start, int end) - { - while (start + 4 < end) - { - if (!ByteHelpers.ContainsPossibly(buffer, start, end, ref _zeroReturnVector)) - { - start += Vector.Count; - continue; - } - - var c = 0; - - while (start + 4 < end && c < Vector.Count) - { - if (buffer[start + 0] == 48 && - buffer[start + 1] == 13 && - buffer[start + 2] == 10 && - buffer[start + 3] == 13 && - buffer[start + 4] == 10) - return start + 5; - - c++; - start++; - } - } - - return -1; - } - } -} diff --git a/Netling.Core/Performance/HttpWorker.cs b/Netling.Core/Performance/HttpWorker.cs deleted file mode 100644 index e14e89a..0000000 --- a/Netling.Core/Performance/HttpWorker.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using Netling.Core.Exceptions; - -namespace Netling.Core.Performance -{ - internal class HttpWorker : IDisposable - { - private readonly Uri _uri; - private readonly IPEndPoint _endPoint; - private readonly byte[] _request; - private TcpClient _client; - private Stream _stream; - private readonly byte[] _buffer; - private int _bufferIndex; - private int _read; - private ResponseType _responseType; - private byte[] _requestPipelining = null; - - public HttpWorker(Uri uri, HttpMethod httpMethod = HttpMethod.Get, Dictionary headers = null, byte[] data = null) - { - _buffer = new byte[8192]; - _bufferIndex = 0; - _read = 0; - _responseType = ResponseType.Unknown; - _uri = uri; - IPAddress ip; - var headersString = string.Empty; - var contentLength = data != null ? data.Length : 0; - - if (headers != null && headers.Any()) - headersString = string.Concat(headers.Select(h => "\r\n" + h.Key.Trim() + ": " + h.Value.Trim())); - - if (_uri.HostNameType == UriHostNameType.Dns) - { - var host = Dns.GetHostEntry(_uri.Host); - ip = host.AddressList.First(i => i.AddressFamily == AddressFamily.InterNetwork); - } - else - { - ip = IPAddress.Parse(_uri.Host); - } - - _endPoint = new IPEndPoint(ip, _uri.Port); - _request = Encoding.UTF8.GetBytes($"{httpMethod.ToString().ToUpper()} {_uri.PathAndQuery} HTTP/1.1\r\nAccept-Encoding: gzip, deflate, sdch\r\nHost: {_uri.Host}\r\nContent-Length: {contentLength}{headersString}\r\n\r\n"); - - if (data == null) - return; - - var tmpRequest = new byte[_request.Length + data.Length]; - Buffer.BlockCopy(_request, 0, tmpRequest, 0, _request.Length); - Buffer.BlockCopy(data, 0, tmpRequest, _request.Length, data.Length); - _request = tmpRequest; - } - - private byte[] GetPipelineBuffer(int count) - { - var result = new byte[_request.Length * count]; - - for (var i = 0; i < count; i++) - { - Buffer.BlockCopy(_request, 0, result, _request.Length * i, _request.Length); - } - - return result; - } - - public void WritePipelined(int pipelining) - { - if (_requestPipelining == null) - { - _requestPipelining = GetPipelineBuffer(pipelining); - } - - InitClient(); - _stream.Write(_requestPipelining, 0, _requestPipelining.Length); - } - - public void Write() - { - InitClient(); - _stream.Write(_request, 0, _request.Length); - } - - public void Flush() - { - _stream.Flush(); - } - - public int Read(out int statusCode) - { - var read = _stream.Read(_buffer, 0, _buffer.Length); - var length = read; - statusCode = HttpHelper.GetStatusCode(_buffer, 0, read); - _responseType = HttpHelper.GetResponseType(_buffer, 0, read); - - if (_responseType == ResponseType.ContentLength) - { - var responseLength = HttpHelperContentLength.GetResponseLength(_buffer, 0, read); - - while (length < responseLength) - { - length += _stream.Read(_buffer, 0, _buffer.Length); - } - } - else if (_responseType == ResponseType.Chunked) - { - while (!HttpHelperChunked.IsEndOfChunkedStream(_buffer, read)) - { - read = _stream.Read(_buffer, 0, _buffer.Length); - length += read; - } - } - else - { - throw new UnknownResponseTypeException(); - } - - return length; - } - - public int ReadPipelined(out int statusCode) - { - if (_bufferIndex == 0) - { - _read = _stream.Read(_buffer, 0, _buffer.Length); - } - - _responseType = HttpHelper.GetResponseType(_buffer, _bufferIndex, _read); - - while (_responseType == ResponseType.Unknown) - { - // Shift the buffer if we are running out of space - if (_bufferIndex > _buffer.Length / 2) - { - Buffer.BlockCopy(_buffer, _bufferIndex, _buffer, 0, _read - _bufferIndex); - _read -= _bufferIndex; - _bufferIndex = 0; - } - - _read += _stream.Read(_buffer, _read, _buffer.Length - _read); - _responseType = HttpHelper.GetResponseType(_buffer, _bufferIndex, _read); - } - - statusCode = HttpHelper.GetStatusCode(_buffer, _bufferIndex, _read); - - if (_responseType == ResponseType.ContentLength) - { - var length = _read - _bufferIndex; - var responseLength = HttpHelperContentLength.GetResponseLength(_buffer, _bufferIndex, _read); - - while (responseLength < 0) - { - // Shift the buffer if we are running out of space - if (_bufferIndex > _buffer.Length / 2) - { - Buffer.BlockCopy(_buffer, _bufferIndex, _buffer, 0, _read - _bufferIndex); - _read -= _bufferIndex; - _bufferIndex = 0; - } - - _read += _stream.Read(_buffer, _read, _buffer.Length - _read); - length = _read - _bufferIndex; - responseLength = HttpHelperContentLength.GetResponseLength(_buffer, _bufferIndex, _read); - } - - while (length < responseLength) - { - _read = _stream.Read(_buffer, 0, _buffer.Length); - length += _read; - } - - var end = _read - (length - responseLength); - _bufferIndex = _read > end ? end : 0; - return responseLength; - } - else if (_responseType == ResponseType.Chunked) - { - var length = 0; - var streamEnd = HttpHelperChunked.SeekEndOfChunkedStream(_buffer, _bufferIndex, _read); - - if (streamEnd >= 0) - length = streamEnd - _bufferIndex; - - while (streamEnd < 0) - { - _read = _stream.Read(_buffer, 0, _buffer.Length); - length += _read; - streamEnd = HttpHelperChunked.SeekEndOfChunkedStream(_buffer, 0, _read); - - if (streamEnd >= 0) - length += streamEnd; - } - - _bufferIndex = _read > streamEnd ? streamEnd : 0; - return length; - } - else - { - throw new UnknownResponseTypeException(); - } - } - - private void InitClient() - { - if (_client != null && _client.Connected) - return; - - _client?.Close(); - _client = new TcpClient(); - - const int sioLoopbackFastPath = -1744830448; - var optionInValue = BitConverter.GetBytes(1); - - try - { - _client.Client.IOControl(sioLoopbackFastPath, optionInValue, null); - } - catch (SocketException) { } - - _client.NoDelay = true; - _client.SendTimeout = 10000; - _client.ReceiveTimeout = 10000; - _client.Connect(_endPoint); - _stream = HttpHelper.GetStream(_client, _uri); - } - - public void Dispose() - { - _client?.Close(); - } - } -} diff --git a/Netling.Core/Performance/ResponseType.cs b/Netling.Core/Performance/ResponseType.cs deleted file mode 100644 index b6a94d7..0000000 --- a/Netling.Core/Performance/ResponseType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Netling.Core.Performance -{ - internal enum ResponseType - { - Unknown, - Chunked, - ContentLength - } -} \ No newline at end of file diff --git a/Netling.Core/Properties/AssemblyInfo.cs b/Netling.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 8471498..0000000 --- a/Netling.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Netling.Core")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Netling.Core")] -[assembly: AssemblyCopyright("Copyright © 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("fef6721a-0464-4e09-baed-d4824b258ea5")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Netling.Core/Utils/ByteHelpers.cs b/Netling.Core/Utils/ByteHelpers.cs deleted file mode 100644 index eea094d..0000000 --- a/Netling.Core/Utils/ByteHelpers.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace Netling.Core.Utils -{ - internal static class ByteHelpers - { - public static unsafe bool ContainsPossibly(byte[] buffer, int index, int end, ref Vector value) - { - if (!Vector.IsHardwareAccelerated || index + Vector.Count + 1 >= end || index + Vector.Count + 1 >= buffer.Length) - return true; - - fixed (byte* bufferPtr = buffer) - { - return - !Vector.Equals(Unsafe.Read>(bufferPtr + index), value).Equals(Vector.Zero) || - !Vector.Equals(Unsafe.Read>(bufferPtr + index + 1), value).Equals(Vector.Zero); - } - } - - public static bool ContainsPossibly(byte[] buffer, int index, int end, ref Vector value) - { - if (!Vector.IsHardwareAccelerated || index + Vector.Count >= end || index + Vector.Count >= buffer.Length) - return true; - - return !Vector.Equals(new Vector(buffer, index), value).Equals(Vector.Zero); - } - } -} diff --git a/Netling.Core/Utils/ThreadHelper.cs b/Netling.Core/Utils/ThreadHelper.cs deleted file mode 100644 index a5a2762..0000000 --- a/Netling.Core/Utils/ThreadHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Netling.Core.Utils -{ - internal static class ThreadHelper - { - public static void QueueThread(int i, bool useThreadAffinity, Action action) - { - var thread = new Thread(() => { - if (useThreadAffinity) - { - Thread.BeginThreadAffinity(); - var affinity = GetAffinity(i + 1, Environment.ProcessorCount); - GetCurrentThread().ProcessorAffinity = new IntPtr(1 << affinity); - } - - action.Invoke(i); - - if (useThreadAffinity) - Thread.EndThreadAffinity(); - }); - thread.Start(); - } - - [DllImport("kernel32.dll")] - private static extern int GetCurrentThreadId(); - - private static ProcessThread GetCurrentThread() - { - var id = GetCurrentThreadId(); - return - (from ProcessThread th in System.Diagnostics.Process.GetCurrentProcess().Threads - where th.Id == id - select th).Single(); - } - - private static int GetAffinity(int i, int cores) - { - var affinity = i * 2 % cores; - - if (i % cores >= cores / 2) - affinity++; - - return affinity; - } - } -} diff --git a/Netling.Core/Worker.cs b/Netling.Core/Worker.cs index a94b406..4717a0f 100644 --- a/Netling.Core/Worker.cs +++ b/Netling.Core/Worker.cs @@ -6,35 +6,40 @@ using System.Threading; using System.Threading.Tasks; using Netling.Core.Models; -using Netling.Core.Performance; -using Netling.Core.Utils; namespace Netling.Core { - public static class Worker + public class Worker { - public static Task Run(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan duration, CancellationToken cancellationToken) + private readonly IWorkerJob _workerJob; + + public Worker(IWorkerJob workerJob) + { + _workerJob = workerJob; + } + + public Task Run(int threads, TimeSpan duration, CancellationToken cancellationToken) { - return Run(uri, threads, threadAffinity, pipelining, duration, null, cancellationToken); + return Run(threads, duration, null, cancellationToken); } - public static Task Run(Uri uri, int count, CancellationToken cancellationToken) + public Task Run(int count, CancellationToken cancellationToken) { - return Run(uri, 1, false, 1, TimeSpan.MaxValue, count, cancellationToken); + return Run(1, TimeSpan.MaxValue, count, cancellationToken); } - private static Task Run(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan duration, int? count, CancellationToken cancellationToken) + private Task Run(int threads, TimeSpan duration, int? count, CancellationToken cancellationToken) { return Task.Run(() => { - var combinedWorkerThreadResult = QueueWorkerThreads(uri, threads, threadAffinity, pipelining, duration, count, cancellationToken); - var workerResult = new WorkerResult(uri, threads, threadAffinity, pipelining, combinedWorkerThreadResult.Elapsed); + var combinedWorkerThreadResult = QueueWorkerThreads(threads, duration, count, cancellationToken); + var workerResult = new WorkerResult(threads, combinedWorkerThreadResult.Elapsed); workerResult.Process(combinedWorkerThreadResult); return workerResult; }); } - private static CombinedWorkerThreadResult QueueWorkerThreads(Uri uri, int threads, bool threadAffinity, int pipelining, TimeSpan duration, int? count, CancellationToken cancellationToken) + private CombinedWorkerThreadResult QueueWorkerThreads(int threads, TimeSpan duration, int? count, CancellationToken cancellationToken) { var results = new ConcurrentQueue(); var events = new List(); @@ -45,11 +50,14 @@ private static CombinedWorkerThreadResult QueueWorkerThreads(Uri uri, int thread { var resetEvent = new ManualResetEventSlim(false); - ThreadHelper.QueueThread(i, threadAffinity, (threadIndex) => - { - DoWork(uri, duration, count, pipelining, results, sw, cancellationToken, resetEvent, threadIndex); - }); + Thread thread; + if (count.HasValue) + thread = new Thread(async (index) => await DoWork_Count(count.Value, results, cancellationToken, resetEvent, (int)index)); + else + thread = new Thread(async (index) => await DoWork_Duration(duration, sw, results, cancellationToken, resetEvent, (int)index)); + + thread.Start(i); events.Add(resetEvent); } @@ -58,112 +66,54 @@ private static CombinedWorkerThreadResult QueueWorkerThreads(Uri uri, int thread var group = events.Skip(i).Take(50).Select(r => r.WaitHandle).ToArray(); WaitHandle.WaitAll(group); } - sw.Stop(); return new CombinedWorkerThreadResult(results, sw.Elapsed); } - private static void DoWork(Uri uri, TimeSpan duration, int? count, int pipelining, ConcurrentQueue results, Stopwatch sw, CancellationToken cancellationToken, ManualResetEventSlim resetEvent, int workerIndex) + private async Task DoWork_Duration(TimeSpan duration, Stopwatch sw, ConcurrentQueue results, CancellationToken cancellationToken, ManualResetEventSlim resetEvent, int workerIndex) { - var result = new WorkerThreadResult(); - var sw2 = new Stopwatch(); - var sw3 = new Stopwatch(); - var worker = new HttpWorker(uri); - var current = 0; - - // To save memory we only track response times from the first 20 workers - var trackResponseTime = workerIndex < 20; + IWorkerJob job; + var workerThreadResult = new WorkerThreadResult(); - // Priming connection ... - if (!count.HasValue) + try { - try - { - int tmpStatusCode; - - if (pipelining > 1) - { - worker.WritePipelined(pipelining); - worker.Flush(); - for (var j = 0; j < pipelining; j++) - { - worker.ReadPipelined(out tmpStatusCode); - } - } - else - { - worker.Write(); - worker.Flush(); - worker.Read(out tmpStatusCode); - } - - } - catch (Exception) { } + job = await _workerJob.Init(workerIndex, workerThreadResult); } - - if (pipelining == 1) + catch (Exception) { - while (!cancellationToken.IsCancellationRequested && duration.TotalMilliseconds > sw.Elapsed.TotalMilliseconds && (!count.HasValue || current < count.Value)) - { - current++; - try - { - sw2.Restart(); - worker.Write(); - worker.Flush(); - int statusCode; - var length = worker.Read(out statusCode); - result.Add((int)(sw.ElapsedTicks / Stopwatch.Frequency), length, (float) sw2.ElapsedTicks / Stopwatch.Frequency * 1000, statusCode, trackResponseTime); - } - catch (Exception ex) - { - result.AddError((int)(sw.ElapsedTicks / Stopwatch.Frequency), (float) sw2.ElapsedTicks / Stopwatch.Frequency * 1000, ex); - } - } + workerThreadResult.AddError((int)sw.ElapsedMilliseconds, 0, false); + results.Enqueue(workerThreadResult); + resetEvent.Set(); + return; } - else + + while (!cancellationToken.IsCancellationRequested && duration.TotalMilliseconds > sw.Elapsed.TotalMilliseconds) { try { - sw2.Restart(); - worker.WritePipelined(pipelining); - worker.Flush(); + await job.DoWork(); } - catch (Exception ex) + catch (Exception) { - result.AddError((int)(sw.ElapsedTicks / Stopwatch.Frequency), (float)sw2.ElapsedTicks / Stopwatch.Frequency * 1000, ex); + workerThreadResult.AddError((int)sw.ElapsedMilliseconds, 0, false); } + } + + results.Enqueue(job.GetResults()); + resetEvent.Set(); + } - while (!cancellationToken.IsCancellationRequested && duration.TotalMilliseconds > sw.Elapsed.TotalMilliseconds) - { - try - { - for (var j = 0; j < pipelining; j++) - { - int statusCode; - var length = worker.ReadPipelined(out statusCode); - result.Add((int)Math.Floor((float)sw.ElapsedTicks / Stopwatch.Frequency), length, (float)sw2.ElapsedTicks / Stopwatch.Frequency * 1000, statusCode, trackResponseTime); - - if (j == 0 && !cancellationToken.IsCancellationRequested && duration.TotalMilliseconds > sw.Elapsed.TotalMilliseconds) - { - sw3.Restart(); - worker.WritePipelined(pipelining); - worker.Flush(); - } - } - - var tmp = sw2; - sw2 = sw3; - sw3 = tmp; - } - catch (Exception ex) - { - result.AddError((int)(sw.ElapsedTicks / Stopwatch.Frequency), (float)sw2.ElapsedTicks / Stopwatch.Frequency * 1000, ex); - } - } + private async Task DoWork_Count(int count, ConcurrentQueue results, CancellationToken cancellationToken, ManualResetEventSlim resetEvent, int workerIndex) + { + var workerThreadResult = new WorkerThreadResult(); + var job = await _workerJob.Init(workerIndex, workerThreadResult); + + for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; i++) + { + await job.DoWork(); } - results.Enqueue(result); + results.Enqueue(job.GetResults()); resetEvent.Set(); } } diff --git a/Netling.Tests/FakeHttpWorkerClient.cs b/Netling.Tests/FakeHttpWorkerClient.cs new file mode 100644 index 0000000..ec589cb --- /dev/null +++ b/Netling.Tests/FakeHttpWorkerClient.cs @@ -0,0 +1,40 @@ +using System; +using System.Text; +using Netling.Core.SocketWorker.Performance; + +namespace Netling.Tests +{ + public class FakeHttpWorkerClient : IHttpWorkerClient + { + private readonly string[] _results; + private int _index; + + public FakeHttpWorkerClient(params string[] results) + { + _results = results; + _index = 0; + } + + public void Dispose() + { + + } + + public void Flush() + { + } + + public int Read(byte[] buffer, int offset, int count) + { + var source = Encoding.UTF8.GetBytes(_results[_index++]); + var length = Math.Min(count, source.Length); + Buffer.BlockCopy(source, 0, buffer, offset, length); + return length; + } + + public void Write(byte[] buffer, int offset, int count) + { + _index = 0; + } + } +} diff --git a/Netling.Tests/HttpWorkerClientTest.cs b/Netling.Tests/HttpWorkerClientTest.cs new file mode 100644 index 0000000..841fe6d --- /dev/null +++ b/Netling.Tests/HttpWorkerClientTest.cs @@ -0,0 +1,50 @@ +using System.Text; +using NUnit.Framework; + +namespace Netling.Tests +{ + [TestFixture] + public class HttpWorkerClientTest + { + private byte[] _request; + private string _response; + + [SetUp] + protected void SetUp() + { + _request = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nAccept-Encoding: gzip, deflate, sdch\r\nHost: test.netling\r\nContent-Length: 5\r\n\r\n12345"); + _response = "HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\nHello, World!"; + } + + [Test] + public void ReadOneRequest() + { + var client = new FakeHttpWorkerClient(_response); + var buffer = new byte[8192]; + client.Write(_request, 0, _request.Length); + client.Flush(); + + var length = client.Read(buffer, 0, buffer.Length); + var response = Encoding.UTF8.GetString(buffer, 0, length); + + Assert.AreEqual(132, length); + Assert.AreEqual(_response, response); + } + + [Test] + public void ReadOneRequestSplit() + { + var client = new FakeHttpWorkerClient("HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\n", "Hello, World!"); + var buffer = new byte[8192]; + client.Write(_request, 0, _request.Length); + client.Flush(); + + var length = client.Read(buffer, 0, buffer.Length); + length += client.Read(buffer, length, buffer.Length); + var response = Encoding.UTF8.GetString(buffer, 0, length); + + Assert.AreEqual(132, length); + Assert.AreEqual(_response, response); + } + } +} diff --git a/Netling.Tests/HttpWorkerTest.cs b/Netling.Tests/HttpWorkerTest.cs new file mode 100644 index 0000000..1a78c5f --- /dev/null +++ b/Netling.Tests/HttpWorkerTest.cs @@ -0,0 +1,32 @@ +using System; +using Netling.Core.SocketWorker.Performance; +using NUnit.Framework; + +namespace Netling.Tests +{ + [TestFixture] + public class HttpWorkerTest + { + [Test] + public void ReadResponse() + { + var client = new FakeHttpWorkerClient("HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\nHello, World!"); + var httpWorker = new HttpWorker(client, new Uri("http://netling.test", UriKind.Absolute)); + + var length = httpWorker.Read(out var statusCode); + Assert.AreEqual(200, statusCode); + Assert.AreEqual(132, length); + } + + [Test] + public void ReadResponseSplit() + { + var client = new FakeHttpWorkerClient("HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\n", "Hello, World!"); + var httpWorker = new HttpWorker(client, new Uri("http://netling.test", UriKind.Absolute)); + + var length = httpWorker.Read(out var statusCode); + Assert.AreEqual(200, statusCode); + Assert.AreEqual(132, length); + } + } +} diff --git a/Netling.Tests/MiscTest.cs b/Netling.Tests/MiscTest.cs new file mode 100644 index 0000000..d8b174d --- /dev/null +++ b/Netling.Tests/MiscTest.cs @@ -0,0 +1,60 @@ +using System.Text; +using Netling.Core.SocketWorker.Extensions; +using Netling.Core.SocketWorker.Performance; +using NUnit.Framework; + +namespace Netling.Tests +{ + [TestFixture] + public class MiscTest + { + private byte[] _request; + private byte[] _response; + + [SetUp] + protected void SetUp() + { + _request = Encoding.UTF8.GetBytes("GET / HTTP/1.1\r\nAccept-Encoding: gzip, deflate, sdch\r\nHost: test.netling\r\nContent-Length: 5\r\n\r\n12345"); + _response = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: Wed, 06 Jul 2016 18:26:27 GMT\r\nContent-Length: 13\r\nContent-Type: text/plain\r\nServer: Kestrel\r\n\r\nHello, World!"); + } + + [Test] + public void ByteExtensions_ConvertToInt() + { + Assert.AreEqual(5, ByteExtensions.ConvertToInt(_request, 90, 1, _request.Length)); + Assert.AreEqual(12345, ByteExtensions.ConvertToInt(_request, 95, 5,_request.Length)); + } + + [Test] + public void HttpHelper_GetResponseType() + { + Assert.AreEqual(ResponseType.ContentLength, HttpHelper.GetResponseType(_response, 0, _response.Length)); + } + + [Test] + public void HttpHelper_GetStatusCode() + { + Assert.AreEqual(200, HttpHelper.GetStatusCode(_response, 0, _response.Length)); + } + + [Test] + public void HttpHelper_SeekHeader() + { + HttpHelper.SeekHeader(_response, HttpHeaders.ContentLength, 0, _response.Length, out var index, out var length); + Assert.AreEqual(70, index); + Assert.AreEqual(2, length); + } + + [Test] + public void HttpHelperContentLength_GetHeaderContentLength() + { + Assert.AreEqual(13, HttpHelperContentLength.GetHeaderContentLength(_response, 0, _response.Length)); + } + + [Test] + public void HttpHelperContentLength_GetResponseLength() + { + Assert.AreEqual(132, HttpHelperContentLength.GetResponseLength(_response, 0, _response.Length)); + } + } +} diff --git a/Netling.Tests/Netling.Tests.csproj b/Netling.Tests/Netling.Tests.csproj new file mode 100644 index 0000000..364e326 --- /dev/null +++ b/Netling.Tests/Netling.Tests.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + diff --git a/Netling.sln b/Netling.sln index 43e404b..5c12d14 100644 --- a/Netling.sln +++ b/Netling.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2026 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netling.Core", "Netling.Core\Netling.Core.csproj", "{84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Netling.Core", "Netling.Core\Netling.Core.csproj", "{84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netling.Client", "Netling.Client\Netling.Client.csproj", "{4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}" EndProject @@ -14,28 +14,49 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{92A6BD .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netling.ConsoleClient", "Netling.ConsoleClient\Netling.ConsoleClient.csproj", "{170E12EF-4094-4B8B-93D3-4A399F81BE9D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Netling.ConsoleClient", "Netling.ConsoleClient\Netling.ConsoleClient.csproj", "{170E12EF-4094-4B8B-93D3-4A399F81BE9D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Netling.Tests", "Netling.Tests\Netling.Tests.csproj", "{3EAFC61A-8664-4D9F-82F2-323DB8EA0F06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netling.Core.SocketWorker", "Netling.Core.SocketWorker\Netling.Core.SocketWorker.csproj", "{10E2F65D-8993-409A-ABE5-59CC2697CB32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Netling.Core.HttpClientWorker", "Netling.Core.HttpClientWorker\Netling.Core.HttpClientWorker.csproj", "{1098EAA8-0341-44E9-8FDE-D05A2A56E083}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Debug|x64.ActiveCfg = Debug|x64 - {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Debug|x64.Build.0 = Debug|x64 - {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Release|x64.ActiveCfg = Release|x64 - {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Release|x64.Build.0 = Release|x64 - {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Debug|x64.ActiveCfg = Debug|x64 - {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Debug|x64.Build.0 = Debug|x64 - {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Release|x64.ActiveCfg = Release|x64 - {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Release|x64.Build.0 = Release|x64 - {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Debug|x64.ActiveCfg = Debug|x64 - {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Debug|x64.Build.0 = Debug|x64 - {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Release|x64.ActiveCfg = Release|x64 - {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Release|x64.Build.0 = Release|x64 + {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84CEEC3F-5FCF-4CA5-A6D7-F83CF7EA0B52}.Release|Any CPU.Build.0 = Release|Any CPU + {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F98A1F2-5AAB-44E9-8DBB-8EE865B97AF3}.Release|Any CPU.Build.0 = Release|Any CPU + {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {170E12EF-4094-4B8B-93D3-4A399F81BE9D}.Release|Any CPU.Build.0 = Release|Any CPU + {3EAFC61A-8664-4D9F-82F2-323DB8EA0F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EAFC61A-8664-4D9F-82F2-323DB8EA0F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EAFC61A-8664-4D9F-82F2-323DB8EA0F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EAFC61A-8664-4D9F-82F2-323DB8EA0F06}.Release|Any CPU.Build.0 = Release|Any CPU + {10E2F65D-8993-409A-ABE5-59CC2697CB32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10E2F65D-8993-409A-ABE5-59CC2697CB32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10E2F65D-8993-409A-ABE5-59CC2697CB32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10E2F65D-8993-409A-ABE5-59CC2697CB32}.Release|Any CPU.Build.0 = Release|Any CPU + {1098EAA8-0341-44E9-8FDE-D05A2A56E083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1098EAA8-0341-44E9-8FDE-D05A2A56E083}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1098EAA8-0341-44E9-8FDE-D05A2A56E083}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1098EAA8-0341-44E9-8FDE-D05A2A56E083}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4BF077A9-5769-4DAD-87EA-6A676CE9DA87} + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 424739b..8968ce7 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ Netling is a load tester client for easy web testing. It is extremely fast while using little CPU or memory. -### Requirements -.NET 4.6.1 +## Requirements +.NET 4.7.1 +.NET Core 2.1 -### Usage +## Usage The base source is meant to support most scenarios. You can use the wpf client, console client or integrate netling.core into your custom solution. Need custom headers, data, etc? Fork and tweak it to your needs! :) -PS: Netling requires keep-alive. Connection: Close will result in errors. +### SocketWorker +This is the default worker. It uses raw sockets and is very fast. -### Screenshots +PS: SocketWorker requires keep-alive. Connection: Close will result in errors. + +### HttpClientWorker +This worker uses HttpClient and is easier to tweak. + +## Screenshots ![Client](http://i.imgur.com/uNwaVTu.png) @@ -19,7 +26,7 @@ PS: Netling requires keep-alive. Connection: Close will result in errors. ![Console application](http://i.imgur.com/8gbPkxK.png) -### License (MIT) +## License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal