├── .gitignore ├── LICENSE ├── README.md └── src ├── BeetleX.Http.Clients.UnitTest ├── BeetleX.Http.Clients.UnitTest.csproj └── HttpClientTest.cs ├── BeetleX.Http.Clients.csproj ├── BeetleX.HttpClients.sln ├── ClientActionHanler.cs ├── ControllerAttribute.cs ├── Cookies.cs ├── Header.cs ├── HostAttribute.cs ├── HttpApiBase.cs ├── HttpClient.cs ├── HttpClientException.cs ├── HttpClientPacket.cs ├── HttpClusterApiBase.cs ├── HttpParse.cs ├── IBodyFormater.cs ├── IHeader.cs ├── INodeSourcesHandler.cs ├── Northwind.Data ├── Customer.cs ├── Customers.txt ├── DataHelper.cs ├── Employee.cs ├── Employees.txt ├── Northwind.Data.csproj ├── Order.cs ├── OrderBase.txt └── Orders.txt ├── PostAttribute.cs ├── Request.cs ├── Response.cs ├── RouteAttribute.cs ├── RouteTemplateMatch.cs ├── StringUrlRequestExtension.cs ├── UploadFile.cs └── WebSockets ├── DataFrame.cs ├── DataPackeType.cs ├── JsonClient.cs ├── Response.cs ├── TextClient.cs ├── WSClient.cs ├── WSPacket.cs └── WSReceiveArgs.cs /.gitignore: -------------------------------------------------------------------------------- 1 | src/BeetleX.Http.Clients.csproj.user 2 | src/.vs/BeetleX.Http.Clients/v15/ 3 | src/bin/ 4 | src/obj/ 5 | src/Northwind.Data/bin/ 6 | src/Northwind.Data/obj/ 7 | src/BeetleX.Http.Clients.UnitTest/obj/ 8 | src/BeetleX.Http.Clients.UnitTest/bin/ 9 | src/.vs/BeetleX.Http.Clients/v16/ 10 | src/.vs/BeetleX.HttpClients/v16/ 11 | src/.vs/BeetleX.HttpClients/DesignTimeBuild/ 12 | src/Northwind.Data/Properties/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HttpClients 2 | BeetleX http and websocket clients for .net standard2.0 3 | ## Install 4 | ``` 5 | Install-Package BeetleX.Http.Clients -Version 1.6 6 | ``` 7 | ``` csharp 8 | var result = await "https://www.baidu.com/" 9 | .FormUrlRequest() 10 | .Get(); 11 | Console.WriteLine(result.Body); 12 | 13 | result = await "https://httpbin.org/get" 14 | .FormUrlRequest() 15 | .Get(); 16 | Console.WriteLine(result.Body); 17 | 18 | 19 | result = await "https://httpbin.org/post" 20 | .JsonRequest() 21 | .SetBody(DateTime.Now) 22 | .Post(); 23 | JToken rdata = result.GetResult()["data"]; 24 | 25 | Console.WriteLine(rdata); 26 | 27 | 28 | var buffer = await "https://httpbin.org/image" 29 | .BinaryRequest() 30 | .Download(); 31 | 32 | result = await "http://localhost/Upload" 33 | .FormDataRequest() 34 | .Upload("g:\\extension_1_4_3_0.rar", "g:\\extension_1_4_3_0_1.rar"); 35 | ``` 36 | 37 | ### Http Cluster 38 | ``` csharp 39 | HttpCluster httpCluster = new HttpCluster(); 40 | httpCluster.DefaultNode 41 | .Add("http://192.168.2.25:8080") 42 | .Add("http://192.168.2.26:8080"); 43 | var client = httpCluster.JsonRequest("/customers?count=10"); 44 | var data = await client.Get(); 45 | client = httpCluster.JsonRequest("/orders?size=10"); 46 | data = await client.Get(); 47 | ``` 48 | ### Http Cluster interface 49 | ``` csharp 50 | public interface INorthWind 51 | { 52 | Task GetEmployee(int id); 53 | [Post] 54 | Task Add(Employee emp); 55 | [Post] 56 | Task Login(string name, string value); 57 | [Post] 58 | Task Modify([CQuery]int id, Employee body); 59 | } 60 | ``` 61 | ``` csharp 62 | HttpCluster httpClusterApi = new HttpClusterApi(); 63 | httpCluster.DefaultNode.Add("http://localhost:8080"); 64 | northWind = httpCluster.Create(); 65 | var result = await northWind.GetEmployee(1); 66 | ``` 67 | ### Multi server 68 | ``` csharp 69 | httpCluster.DefaultNode 70 | .Add("http://192.168.2.25:8080") 71 | .Add("http://192.168.2.26:8080"); 72 | ``` 73 | ### Server weight 74 | ``` csharp 75 | .Add("http://192.168.2.25:8080",10) 76 | .Add("http://192.168.2.26:8080",10); 77 | .Add("http://192.168.2.27:8080",5); 78 | ``` 79 | ### Multi url route 80 | ``` 81 | httpClusterApi.GetUrlNode("/order.*") 82 | .Add("http://192.168.2.25:8080") 83 | .Add("http://192.168.2.26:8080"); 84 | httpClusterApi.GetUrlNode("/employee.*") 85 | .Add("http://192.168.2.27:8080") 86 | .Add("http://192.168.2.28:8080"); 87 | ``` 88 | ### github auth sample 89 | ``` csharp 90 | [FormUrlFormater] 91 | [Host("https://github.com")] 92 | public interface IGithubAuth 93 | { 94 | 95 | [Get(Route = "login/oauth/access_token")] 96 | Task GetToken(string client_id, string client_secret, string code); 97 | 98 | [Host("https://api.github.com")] 99 | [CHeader("User-Agent", "beetlex.io")] 100 | [Get(Route = "user")] 101 | Task GetUser(string access_token); 102 | } 103 | githubAuth = HttpApiClient.Create(); 104 | ``` 105 | 106 | ### Websocket 107 | ### Create wsclient 108 | ``` 109 | TextClient client = new TextClient("ws://echo.websocket.org"); 110 | ``` 111 | ### send text 112 | ``` 113 | await client.Send("hello"); 114 | ``` 115 | ### send and receive 116 | ``` 117 | var resutl = await wss.ReceiveFrom("hello henry"); 118 | ``` 119 | -------------------------------------------------------------------------------- /src/BeetleX.Http.Clients.UnitTest/BeetleX.Http.Clients.UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/BeetleX.Http.Clients.UnitTest/HttpClientTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | using System.Collections.Generic; 5 | using Northwind.Data; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace BeetleX.Http.Clients.UnitTest 9 | { 10 | public class HttpClientTest 11 | { 12 | 13 | [Fact] 14 | public async Task HttpBin_Delete() 15 | { 16 | HttpJsonClient client = new HttpJsonClient("http://httpbin.org/delete"); 17 | var result = await client.Delete(); 18 | Assert.Equal(null, result.Exception); 19 | 20 | } 21 | [Fact] 22 | public async Task HttpBin_Get() 23 | { 24 | HttpJsonClient client = new HttpJsonClient("http://httpbin.org/get"); 25 | var result = await client.Get(); 26 | Assert.Equal(null, result.Exception); 27 | } 28 | 29 | [Fact] 30 | public async Task HttpBin_Post() 31 | { 32 | HttpJsonClient client = new HttpJsonClient("http://httpbin.org/post"); 33 | var date = DateTime.Now; 34 | client.SetBody(date); 35 | var result = await client.Post(); 36 | JToken rdata = result.GetResult()["data"]; 37 | Assert.Equal(date, rdata.ToObject()); 38 | } 39 | [Fact] 40 | public async Task HttpBin_Put() 41 | { 42 | HttpJsonClient client = new HttpJsonClient("http://httpbin.org/post"); 43 | Employee emp = DataHelper.Defalut.Employees[0]; 44 | client.SetBody(emp); 45 | var result = await client.Post(); 46 | JToken rdata = result.GetResult()["data"]; 47 | Assert.Equal(emp.EmployeeID, rdata.ToObject().EmployeeID); 48 | } 49 | [Fact] 50 | public async Task GetImage() 51 | { 52 | HttpClient client = new HttpClient("http://httpbin.org/image"); 53 | var result = await client.Get(); 54 | var data = result.GetResult>(); 55 | using (System.IO.Stream write = System.IO.File.Create("test.jpg")) 56 | { 57 | write.Write(data.Array, data.Offset, data.Count); 58 | write.Flush(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/BeetleX.Http.Clients.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netcoreapp2.1;netcoreapp3.1;net5.0 5 | 7.3 6 | BeetleX.Http.Clients 7 | 1.6.2 8 | henryfan 9 | beetlex.io 10 | Copyright © beetlex.io 2019-2021 email:admin@beetlex.io 11 | https://github.com/beetlex-io/HttpClients 12 | http webapi and websocket client for .net 13 | 1.6.0.0 14 | 15 | beetlex200.png 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | True 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0.7.5 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/BeetleX.HttpClients.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31005.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BeetleX.Http.Clients", "BeetleX.Http.Clients.csproj", "{4448ECC2-FFF5-438B-8647-3A86E11E4752}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BeetleX.Http.Clients.UnitTest", "BeetleX.Http.Clients.UnitTest\BeetleX.Http.Clients.UnitTest.csproj", "{90CFD1FC-82A3-4EDC-9486-EF05FAA0DADA}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Northwind.Data", "Northwind.Data\Northwind.Data.csproj", "{9B447D7F-516F-4B75-9D4B-DEF75275314C}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {4448ECC2-FFF5-438B-8647-3A86E11E4752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {4448ECC2-FFF5-438B-8647-3A86E11E4752}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {4448ECC2-FFF5-438B-8647-3A86E11E4752}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {4448ECC2-FFF5-438B-8647-3A86E11E4752}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {90CFD1FC-82A3-4EDC-9486-EF05FAA0DADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {90CFD1FC-82A3-4EDC-9486-EF05FAA0DADA}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {90CFD1FC-82A3-4EDC-9486-EF05FAA0DADA}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {90CFD1FC-82A3-4EDC-9486-EF05FAA0DADA}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {9B447D7F-516F-4B75-9D4B-DEF75275314C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {9B447D7F-516F-4B75-9D4B-DEF75275314C}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {9B447D7F-516F-4B75-9D4B-DEF75275314C}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {9B447D7F-516F-4B75-9D4B-DEF75275314C}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {35BAD3BA-33E1-40EC-AFF1-1E3CE470AC5D} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/ClientActionHanler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using static BeetleX.Http.Clients.RouteTemplateMatch; 7 | 8 | namespace BeetleX.Http.Clients 9 | { 10 | class ClientActionHanler 11 | { 12 | 13 | private List mRouteParameters = new List(); 14 | 15 | private List mHeaderParameters = new List(); 16 | 17 | private List mQueryStringParameters = new List(); 18 | 19 | private List mDataParameters = new List(); 20 | 21 | public string Method { get; set; } 22 | 23 | public string Name { get; set; } 24 | 25 | public string BaseUrl { get; set; } 26 | 27 | private Dictionary mHeaders = new Dictionary(); 28 | 29 | private Dictionary mQueryString = new Dictionary(); 30 | 31 | public ControllerAttribute Controller { get; set; } 32 | 33 | public IBodyFormater Formater { get; set; } 34 | 35 | public MethodInfo MethodInfo { get; set; } 36 | 37 | public RouteTemplateMatch RouteTemplateMatch { get; set; } 38 | 39 | public Type DeclaringType 40 | { get; set; } 41 | 42 | public bool Async { get; set; } 43 | 44 | public HttpHost Host { get; set; } 45 | 46 | public Type ReturnType { get; set; } 47 | 48 | public Type MethodType { get; set; } 49 | 50 | public ClientActionHanler(MethodInfo method) 51 | { 52 | MethodInfo = method; 53 | Method = "GET"; 54 | Name = method.Name; 55 | DeclaringType = method.DeclaringType; 56 | var host = DeclaringType.GetCustomAttribute(false); 57 | MethodType = MethodInfo.ReturnType; 58 | var mhost = method.GetCustomAttribute(false); 59 | if (mhost != null) 60 | host = mhost; 61 | if (host != null) 62 | { 63 | this.Host = HttpHost.GetHttpHost(host.Host);// new HttpHost(host.Host); 64 | } 65 | Async = false; 66 | if (MethodInfo.ReturnType != typeof(void)) 67 | { 68 | if (MethodInfo.ReturnType.Name == "Task`1" || MethodInfo.ReturnType == typeof(Task)) 69 | { 70 | Async = true; 71 | if (MethodInfo.ReturnType.IsGenericType) 72 | ReturnType = MethodInfo.ReturnType.GetGenericArguments()[0]; 73 | } 74 | else 75 | { 76 | 77 | ReturnType = MethodInfo.ReturnType; 78 | } 79 | } 80 | foreach (HeaderAttribute h in DeclaringType.GetCustomAttributes()) 81 | { 82 | if (!string.IsNullOrEmpty(h.Name) && !string.IsNullOrEmpty(h.Value)) 83 | { 84 | mHeaders[h.Name] = h.Value; 85 | } 86 | } 87 | 88 | foreach (HeaderAttribute h in method.GetCustomAttributes()) 89 | { 90 | if (!string.IsNullOrEmpty(h.Name) && !string.IsNullOrEmpty(h.Value)) 91 | { 92 | mHeaders[h.Name] = h.Value; 93 | } 94 | } 95 | 96 | foreach (QueryAttribute q in DeclaringType.GetCustomAttributes()) 97 | { 98 | if (!string.IsNullOrEmpty(q.Name) && !string.IsNullOrEmpty(q.Value)) 99 | { 100 | mQueryString[q.Name] = q.Value; 101 | } 102 | } 103 | 104 | foreach (QueryAttribute q in method.GetCustomAttributes()) 105 | { 106 | if (!string.IsNullOrEmpty(q.Name) && !string.IsNullOrEmpty(q.Value)) 107 | { 108 | mQueryString[q.Name] = q.Value; 109 | } 110 | } 111 | 112 | Formater = method.GetCustomAttribute(); 113 | if (Formater == null) 114 | Formater = DeclaringType.GetCustomAttribute(); 115 | if (Formater == null) 116 | Formater = new JsonFormater(); 117 | var get = method.GetCustomAttribute(); 118 | if (get != null) 119 | { 120 | Method = Request.GET; 121 | if (!string.IsNullOrEmpty(get.Route)) 122 | RouteTemplateMatch = new RouteTemplateMatch(get.Route); 123 | } 124 | var post = method.GetCustomAttribute(); 125 | if (post != null) 126 | { 127 | Method = Request.POST; 128 | if (!string.IsNullOrEmpty(post.Route)) 129 | RouteTemplateMatch = new RouteTemplateMatch(post.Route); 130 | } 131 | var del = method.GetCustomAttribute(); 132 | if (del != null) 133 | { 134 | Method = Request.DELETE; 135 | if (!string.IsNullOrEmpty(del.Route)) 136 | RouteTemplateMatch = new RouteTemplateMatch(del.Route); 137 | } 138 | var put = method.GetCustomAttribute(); 139 | if (put != null) 140 | { 141 | Method = Request.PUT; 142 | if (!string.IsNullOrEmpty(put.Route)) 143 | RouteTemplateMatch = new RouteTemplateMatch(put.Route); 144 | } 145 | Controller = this.DeclaringType.GetCustomAttribute(); 146 | if (Controller != null) 147 | { 148 | if (!string.IsNullOrEmpty(Controller.BaseUrl)) 149 | BaseUrl = Controller.BaseUrl; 150 | } 151 | if (string.IsNullOrEmpty(BaseUrl)) 152 | BaseUrl = "/"; 153 | if (BaseUrl[0] != '/') 154 | BaseUrl = "/" + BaseUrl; 155 | if (BaseUrl.Substring(BaseUrl.Length - 1, 1) != "/") 156 | BaseUrl += "/"; 157 | int index = 0; 158 | foreach (var p in method.GetParameters()) 159 | { 160 | ClientActionParameter cap = new ClientActionParameter(); 161 | cap.Name = p.Name; 162 | cap.ParameterType = p.ParameterType; 163 | cap.Index = index; 164 | index++; 165 | HeaderAttribute cHeader = p.GetCustomAttribute(); 166 | if (cHeader != null) 167 | { 168 | if (!string.IsNullOrEmpty(cHeader.Name)) 169 | cap.Name = cHeader.Name; 170 | mHeaderParameters.Add(cap); 171 | } 172 | else 173 | { 174 | QueryAttribute cQuery = p.GetCustomAttribute(); 175 | if (cQuery != null) 176 | { 177 | if (!string.IsNullOrEmpty(cQuery.Name)) 178 | cap.Name = cQuery.Name; 179 | mQueryStringParameters.Add(cap); 180 | } 181 | else 182 | { 183 | if (RouteTemplateMatch != null && RouteTemplateMatch.Items.Find(i => i.Name == p.Name) != null) 184 | { 185 | mRouteParameters.Add(cap); 186 | } 187 | else 188 | { 189 | mDataParameters.Add(cap); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | public RequestInfo GetRequestInfo(object[] parameters) 197 | { 198 | RequestInfo result = new RequestInfo(); 199 | if (mHeaders.Count > 0) 200 | { 201 | if (result.Header == null) 202 | result.Header = new Dictionary(); 203 | foreach (var kv in mHeaders) 204 | result.Header[kv.Key] = kv.Value; 205 | } 206 | if (mQueryString.Count > 0) 207 | { 208 | if (result.QueryString == null) 209 | result.QueryString = new Dictionary(); 210 | foreach (var kv in mQueryString) 211 | result.QueryString[kv.Key] = kv.Value; 212 | } 213 | result.Method = this.Method; 214 | StringBuilder sb = new StringBuilder(); 215 | sb.Append(BaseUrl); 216 | if (RouteTemplateMatch != null) 217 | { 218 | if (RouteTemplateMatch.Items.Count > 0) 219 | { 220 | List items = RouteTemplateMatch.Items; 221 | for (int i = 0; i < items.Count; i++) 222 | { 223 | var item = items[i]; 224 | if (!string.IsNullOrEmpty(item.Start)) 225 | sb.Append(item.Start); 226 | ClientActionParameter cap = mRouteParameters.Find(p => p.Name == item.Name); 227 | if (cap != null) 228 | sb.Append(parameters[cap.Index]); 229 | 230 | if (!string.IsNullOrEmpty(item.Eof)) 231 | sb.Append(item.Eof); 232 | } 233 | } 234 | else 235 | { 236 | sb.Append(RouteTemplateMatch.Template); 237 | } 238 | } 239 | else 240 | { 241 | sb.Append(MethodInfo.Name); 242 | } 243 | if (mDataParameters.Count > 0) 244 | { 245 | if (Method == Request.DELETE || Method == Request.GET) 246 | { 247 | if (result.QueryString == null) 248 | result.QueryString = new Dictionary(); 249 | foreach (var item in mDataParameters) 250 | { 251 | if (parameters[item.Index] != null) 252 | result.QueryString[item.Name] = parameters[item.Index].ToString(); 253 | } 254 | } 255 | else 256 | { 257 | var data = new Dictionary(); 258 | foreach (var item in mDataParameters) 259 | { 260 | if (parameters[item.Index] != null) 261 | data[item.Name] = parameters[item.Index]; 262 | } 263 | result.Data = data; 264 | } 265 | } 266 | if (mHeaderParameters.Count > 0) 267 | { 268 | if (result.Header == null) 269 | result.Header = new Dictionary(); 270 | foreach (var item in mHeaderParameters) 271 | { 272 | if (parameters[item.Index] != null) 273 | result.Header[item.Name] = parameters[item.Index].ToString(); 274 | } 275 | } 276 | if (mQueryStringParameters.Count > 0) 277 | { 278 | if (result.QueryString == null) 279 | result.QueryString = new Dictionary(); 280 | foreach (var item in mQueryStringParameters) 281 | { 282 | if (parameters[item.Index] != null) 283 | result.QueryString[item.Name] = parameters[item.Index].ToString(); 284 | } 285 | } 286 | result.Type = ReturnType; 287 | result.Url = sb.ToString(); 288 | result.Formatter = this.Formater; 289 | return result; 290 | } 291 | 292 | public struct RequestInfo 293 | { 294 | public string Method; 295 | 296 | public Type Type; 297 | 298 | public string Url; 299 | 300 | public IBodyFormater Formatter; 301 | 302 | public Dictionary Data; 303 | 304 | public Dictionary Header; 305 | 306 | public Dictionary QueryString; 307 | 308 | public Request GetRequest(HttpHost httpApiClient) 309 | { 310 | switch (Method) 311 | { 312 | case Request.POST: 313 | return httpApiClient.Post(Url, Header, QueryString, Data, Formatter, Type); 314 | case Request.PUT: 315 | return httpApiClient.Put(Url, Header, QueryString, Data, Formatter, Type); 316 | case Request.DELETE: 317 | return httpApiClient.Delete(Url, Header, QueryString, Formatter, Type); 318 | default: 319 | return httpApiClient.Get(Url, Header, QueryString, Formatter, Type); 320 | } 321 | } 322 | } 323 | 324 | public ApiNodeAgent NodeAgent { get; set; } 325 | 326 | } 327 | 328 | class ClientActionParameter 329 | { 330 | 331 | public long Version { get; set; } 332 | 333 | public string Name { get; set; } 334 | 335 | public Type ParameterType { get; set; } 336 | 337 | public int Index { get; set; } 338 | 339 | } 340 | 341 | class ClientActionFactory 342 | { 343 | static System.Collections.Concurrent.ConcurrentDictionary mHandlers = new System.Collections.Concurrent.ConcurrentDictionary(); 344 | 345 | public static ClientActionHanler GetHandler(MethodInfo method) 346 | { 347 | ClientActionHanler result; 348 | if (!mHandlers.TryGetValue(method, out result)) 349 | { 350 | result = new ClientActionHanler(method); 351 | mHandlers[method] = result; 352 | } 353 | return result; 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/ControllerAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] 8 | public class ControllerAttribute : Attribute 9 | { 10 | public ControllerAttribute() 11 | { 12 | 13 | } 14 | public string BaseUrl { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Cookies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | public class Cookies 8 | { 9 | private Dictionary mItems = new Dictionary(4); 10 | 11 | public string this[string name] 12 | { 13 | get 14 | { 15 | return GetValue(name); 16 | } 17 | } 18 | 19 | public void Clear() 20 | { 21 | mItems.Clear(); 22 | } 23 | 24 | private string GetValue(string name) 25 | { 26 | string result = null; 27 | mItems.TryGetValue(name, out result); 28 | return result; 29 | } 30 | 31 | internal void Add(string name, string value) 32 | { 33 | name = System.Web.HttpUtility.UrlDecode(name); 34 | value = System.Web.HttpUtility.UrlDecode(value); 35 | mItems[name] = value; 36 | } 37 | public override string ToString() 38 | { 39 | StringBuilder sb = new StringBuilder(); 40 | foreach (var item in mItems) 41 | { 42 | sb.AppendFormat("{0}={1}\r\n", item.Key, item.Value); 43 | } 44 | return sb.ToString(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Header.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace BeetleX.Http.Clients 8 | { 9 | 10 | public interface IHeaderItem 11 | { 12 | void Write(PipeStream stream); 13 | } 14 | 15 | public class HeaderItem : IHeaderItem 16 | { 17 | public HeaderItem(string value) 18 | { 19 | mData = Encoding.UTF8.GetBytes(value); 20 | } 21 | 22 | private byte[] mData; 23 | 24 | public void Write(PipeStream stream) 25 | { 26 | stream.Write(mData, 0, mData.Length); 27 | } 28 | } 29 | 30 | 31 | public class HeaderTypeFactory 32 | { 33 | 34 | public static byte[] NULL_CONTENT_LENGTH_BYTES; 35 | 36 | public static byte[] CONTENT_LENGTH_BYTES; 37 | 38 | public static byte[] TOW_LINE_BYTES; 39 | 40 | public static byte[] CHUNKED_BYTES; 41 | 42 | public static byte[] LINE_BYTES; 43 | 44 | public static byte[] HTTP_V11_BYTES; 45 | 46 | public static byte[] SPACE_BYTES; 47 | 48 | public static byte[] HEADER_SPLIT; 49 | 50 | public const byte _LINE_R = 13; 51 | 52 | public const byte _LINE_N = 10; 53 | 54 | public const byte _SPACE_BYTE = 32; 55 | 56 | public const byte _AND = 38; 57 | 58 | public const byte _QMARK = 63; 59 | 60 | public const byte _EQ = 61; 61 | 62 | public const int HEADERNAME_MAXLENGTH = 32; 63 | 64 | public static byte[] SERVAR_HEADER_BYTES; 65 | 66 | 67 | #region header 68 | public const string AUTHORIZATION = "Authorization"; 69 | 70 | public const string WWW_AUTHENTICATE = "WWW-Authenticate"; 71 | 72 | public const string ORIGIN = "Origin"; 73 | 74 | public const string DATE = "Date"; 75 | 76 | public const string AGE = "Age"; 77 | 78 | public const string LOCATION = "Location"; 79 | 80 | public const string SEC_WEBSOCKT_ACCEPT = "Sec-WebSocket-Accept"; 81 | 82 | public const string CLIENT_IPADDRESS = "X-Real-IP"; 83 | 84 | public const string SEC_WEBSOCKET_VERSION = "Sec_WebSocket_Version"; 85 | 86 | public const string SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; 87 | 88 | public const string SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; 89 | 90 | public const string ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; 91 | 92 | public const string ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; 93 | 94 | public const string ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; 95 | 96 | public const string UPGRADE = "Upgrade"; 97 | 98 | public const string ACCEPT = "Accept"; 99 | 100 | public const string ACCEPT_ENCODING = "Accept-Encoding"; 101 | 102 | public const string ACCEPT_LANGUAGE = "Accept-Language"; 103 | 104 | public const string ACCEPT_CHARSET = "Accept-Charset"; 105 | 106 | public const string CACHE_CONTROL = "Cache-Control"; 107 | 108 | public const string CONNECTION = "Connection"; 109 | 110 | public const string COOKIE = "Cookie"; 111 | 112 | public const string HOST = "Host"; 113 | 114 | public const string REFERER = "Referer"; 115 | 116 | public const string USER_AGENT = "User-Agent"; 117 | 118 | public const string STATUS = "Status"; 119 | 120 | public const string CONTENT_TYPE = "Content-Type"; 121 | 122 | public const string ETAG = "ETag"; 123 | 124 | public const string CONTENT_LENGTH = "Content-Length"; 125 | 126 | public const string CONTENT_ENCODING = "Content-Encoding"; 127 | 128 | public const string TRANSFER_ENCODING = "Transfer-Encoding"; 129 | 130 | public const string IF_NONE_MATCH = "If-None-Match"; 131 | 132 | public const string SERVER = "Server"; 133 | 134 | public const string SET_COOKIE = "Set-Cookie"; 135 | #endregion 136 | 137 | static HeaderTypeFactory() 138 | { 139 | SPACE_BYTES = Encoding.UTF8.GetBytes(" "); 140 | HEADER_SPLIT = Encoding.UTF8.GetBytes(": "); 141 | LINE_BYTES = Encoding.UTF8.GetBytes("\r\n"); 142 | NULL_CONTENT_LENGTH_BYTES = Encoding.UTF8.GetBytes("Content-Length: 0\r\n"); 143 | CHUNKED_BYTES = Encoding.UTF8.GetBytes("0\r\n\r\n"); 144 | CONTENT_LENGTH_BYTES = Encoding.UTF8.GetBytes("Content-Length: "); 145 | TOW_LINE_BYTES = Encoding.UTF8.GetBytes("\r\n\r\n"); 146 | SERVAR_HEADER_BYTES = Encoding.UTF8.GetBytes("Server: BeetleX\r\n"); 147 | HTTP_V11_BYTES = Encoding.UTF8.GetBytes("HTTP/1.1"); 148 | Add(HeaderTypeFactory.AGE); 149 | Add(HeaderTypeFactory.AUTHORIZATION); 150 | Add(HeaderTypeFactory.WWW_AUTHENTICATE); 151 | Add(HeaderTypeFactory.ACCEPT); 152 | Add(HeaderTypeFactory.ACCEPT_ENCODING); 153 | Add(HeaderTypeFactory.ACCEPT_LANGUAGE); 154 | Add(HeaderTypeFactory.ACCEPT_CHARSET); 155 | Add(HeaderTypeFactory.ACCESS_CONTROL_ALLOW_CREDENTIALS); 156 | Add(HeaderTypeFactory.ACCESS_CONTROL_ALLOW_HEADERS); 157 | Add(HeaderTypeFactory.ACCESS_CONTROL_ALLOW_ORIGIN); 158 | Add(HeaderTypeFactory.CACHE_CONTROL); 159 | Add(HeaderTypeFactory.CLIENT_IPADDRESS); 160 | Add(HeaderTypeFactory.CONNECTION); 161 | Add(HeaderTypeFactory.CONTENT_ENCODING); 162 | Add(HeaderTypeFactory.CONTENT_LENGTH); 163 | Add(HeaderTypeFactory.CONTENT_TYPE); 164 | Add(HeaderTypeFactory.COOKIE); 165 | Add(HeaderTypeFactory.DATE); 166 | Add(HeaderTypeFactory.HOST); 167 | Add(HeaderTypeFactory.ETAG); 168 | Add(HeaderTypeFactory.IF_NONE_MATCH); 169 | Add(HeaderTypeFactory.LOCATION); 170 | Add(HeaderTypeFactory.ORIGIN); 171 | Add(HeaderTypeFactory.REFERER); 172 | Add(HeaderTypeFactory.SEC_WEBSOCKET_EXTENSIONS); 173 | Add(HeaderTypeFactory.SEC_WEBSOCKET_KEY); 174 | Add(HeaderTypeFactory.SEC_WEBSOCKET_VERSION); 175 | Add(HeaderTypeFactory.SEC_WEBSOCKT_ACCEPT); 176 | Add(HeaderTypeFactory.SERVER); 177 | Add(HeaderTypeFactory.SET_COOKIE); 178 | Add(HeaderTypeFactory.STATUS); 179 | Add(HeaderTypeFactory.TRANSFER_ENCODING); 180 | Add(HeaderTypeFactory.UPGRADE); 181 | Add(HeaderTypeFactory.USER_AGENT); 182 | } 183 | 184 | private static System.Collections.Generic.Dictionary mHeaderTypes = new Dictionary(); 185 | 186 | private static int mCount; 187 | 188 | private static void Add(String name) 189 | { 190 | lock (mHeaderTypes) 191 | { 192 | HeaderType type = new HeaderType(name); 193 | mHeaderTypes[type.ID] = type; 194 | } 195 | } 196 | 197 | private static void Add(string name, HeaderType type) 198 | { 199 | if (mCount < 5000) 200 | { 201 | lock (mHeaderTypes) 202 | { 203 | long id = HeaderType.GetNameCode(name); 204 | mHeaderTypes[id] = type; 205 | } 206 | System.Threading.Interlocked.Increment(ref mCount); 207 | } 208 | } 209 | 210 | public static HeaderType Find(string name) 211 | { 212 | HeaderType type; 213 | long id = HeaderType.GetNameCode(name); 214 | if (mHeaderTypes.TryGetValue(id, out type)) 215 | return type; 216 | HeaderType[] items; 217 | lock (mHeaderTypes) 218 | { 219 | if (mHeaderTypes.TryGetValue(id, out type)) 220 | return type; 221 | items = mHeaderTypes.Values.ToArray(); 222 | foreach (var item in items) 223 | { 224 | if (item.Compare(name)) 225 | { 226 | type = item; 227 | } 228 | } 229 | } 230 | if (type == null) 231 | type = new HeaderType(name); 232 | Add(name, type); 233 | return type; 234 | } 235 | 236 | public static void Write(string name, PipeStream stream) 237 | { 238 | HeaderType type = Find(name); 239 | stream.Write(type.Bytes,0,type.Bytes.Length); 240 | } 241 | } 242 | 243 | public class Header 244 | { 245 | 246 | private Dictionary mValues = new Dictionary(); 247 | 248 | public void Add(string name, string value) 249 | { 250 | if (value == null) 251 | value = string.Empty; 252 | Find(name).Value = value; 253 | } 254 | 255 | public int Count 256 | { 257 | get 258 | { 259 | return mValues.Count; 260 | } 261 | } 262 | 263 | public void Clear() 264 | { 265 | mValues.Clear(); 266 | } 267 | 268 | private HeaderValue Find(string name) 269 | { 270 | HeaderType type = HeaderTypeFactory.Find(name); 271 | HeaderValue value; 272 | if (mValues.TryGetValue(type.ID, out value)) 273 | return value; 274 | value = new HeaderValue(type, null); 275 | mValues[type.ID] = value; 276 | return value; 277 | } 278 | 279 | private HeaderValue FindOnly(string name) 280 | { 281 | HeaderValue result; 282 | long id = HeaderType.GetNameCode(name); 283 | mValues.TryGetValue(id, out result); 284 | return result; 285 | } 286 | 287 | public void Remove(string name) 288 | { 289 | long id = HeaderType.GetNameCode(name); 290 | mValues.Remove(id); 291 | } 292 | 293 | public string this[string name] 294 | { 295 | get 296 | { 297 | HeaderValue headerValue = FindOnly(name); 298 | if (headerValue != null) 299 | return headerValue.Value; 300 | else 301 | return null; 302 | } 303 | set 304 | { 305 | Find(name).Value = value; 306 | } 307 | } 308 | 309 | public void CopyTo(Header header) 310 | { 311 | foreach (var item in mValues.Values) 312 | { 313 | header.Add(item.Type.Name, item.Value); 314 | } 315 | } 316 | 317 | public bool Read(PipeStream stream, Cookies cookies) 318 | { 319 | string lineData; 320 | while (stream.TryReadLine(out lineData)) 321 | { 322 | if (lineData.Length == 0) 323 | { 324 | return true; 325 | } 326 | else 327 | { 328 | ReadOnlySpan line = lineData.AsSpan(); 329 | Tuple result = HttpParse.AnalyzeHeader(line); 330 | this[result.Item1] = result.Item2; 331 | if (line[0] == 'C' && line[5] == 'e' && line[1] == 'o' && line[2] == 'o' && line[3] == 'k' && line[4] == 'i') 332 | { 333 | HttpParse.AnalyzeCookie(line.Slice(8, line.Length - 8), cookies); 334 | } 335 | } 336 | } 337 | return false; 338 | } 339 | 340 | public void Write(PipeStream stream) 341 | { 342 | foreach (var item in mValues.Values) 343 | { 344 | item.Write(stream); 345 | } 346 | } 347 | 348 | public override string ToString() 349 | { 350 | StringBuilder sb = new StringBuilder(); 351 | foreach (var item in mValues.Values) 352 | { 353 | sb.AppendFormat("{0}={1}\r\n", item.Type.Name, item.Value); 354 | } 355 | return sb.ToString(); 356 | } 357 | } 358 | 359 | public class HeaderValue 360 | { 361 | public HeaderValue(HeaderType type, string value) 362 | { 363 | Type = type; 364 | Value = value; 365 | } 366 | 367 | public HeaderType Type { get; set; } 368 | 369 | public string Value { get; set; } 370 | 371 | public void Write(PipeStream stream) 372 | { 373 | byte[] buffer = HttpParse.GetByteBuffer(); 374 | int count = Type.Bytes.Length; 375 | System.Buffer.BlockCopy(Type.Bytes, 0, buffer, 0, count); 376 | count = count + Encoding.UTF8.GetBytes(Value, 0, Value.Length, buffer, count); 377 | buffer[count] = HeaderTypeFactory._LINE_R; 378 | buffer[count + 1] = HeaderTypeFactory._LINE_N; 379 | stream.Write(buffer, 0, count + 2); 380 | } 381 | } 382 | 383 | public class HeaderType 384 | { 385 | 386 | public static long GetNameCode(string name) 387 | { 388 | return (long)name.GetHashCode() << 16 | (ushort)name.Length; 389 | } 390 | 391 | public HeaderType(string name) 392 | { 393 | Name = name; 394 | Bytes = Encoding.UTF8.GetBytes(name + ": "); 395 | ID = GetNameCode(name); 396 | } 397 | 398 | public string Name { get; set; } 399 | 400 | public byte[] Bytes { get; set; } 401 | 402 | public long ID { get; set; } 403 | 404 | public bool Compare(string value) 405 | { 406 | return string.Compare(Name, value, true) == 0; 407 | } 408 | } 409 | } 410 | 411 | 412 | 413 | -------------------------------------------------------------------------------- /src/HostAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | [AttributeUsage(AttributeTargets.Interface| AttributeTargets.Method| AttributeTargets.Class)] 8 | public class HostAttribute:Attribute 9 | { 10 | public HostAttribute(string name) 11 | { 12 | Host = new Uri(name); 13 | } 14 | 15 | public Uri Host { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/HttpApiBase.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Tasks; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BeetleX.Http.Clients 9 | { 10 | 11 | public class HttpInterfaceProxy : System.Reflection.DispatchProxy 12 | { 13 | public HttpInterfaceProxy() 14 | { 15 | TimeOut = 10000; 16 | } 17 | 18 | public HttpHost Host { get; set; } 19 | 20 | public int TimeOut { get; set; } 21 | 22 | protected override object Invoke(MethodInfo targetMethod, object[] args) 23 | { 24 | ClientActionHanler handler = ClientActionFactory.GetHandler((MethodInfo)targetMethod); 25 | var rinfo = handler.GetRequestInfo(args); 26 | var host = handler.Host != null ? handler.Host : Host; 27 | if (host == null) 28 | throw new Exception("The service host is not defined!"); 29 | var request = rinfo.GetRequest(host); 30 | request.TimeOut = TimeOut; 31 | var task = request.Execute(); 32 | if (!handler.Async) 33 | { 34 | throw new HttpClientException(request, host.Uri, $"{targetMethod.Name} method invoke not supported, the return value must be task!"); 35 | } 36 | else 37 | { 38 | IAnyCompletionSource source = CompletionSourceFactory.Create(handler.ReturnType, TimeOut); 39 | source.Wait(task, (c, t) => 40 | { 41 | if (t.Result.Exception != null) 42 | { 43 | c.Error(t.Result.Exception); 44 | } 45 | else 46 | { 47 | c.Success(t.Result.Body); 48 | } 49 | }); 50 | return source.GetTask(); 51 | } 52 | } 53 | } 54 | 55 | public class HttpApiClient 56 | { 57 | internal HttpApiClient(string host) 58 | { 59 | Host = HttpHost.GetHttpHost(host); //new HttpHost(host); 60 | } 61 | 62 | private static Dictionary mClients = new Dictionary(StringComparer.OrdinalIgnoreCase); 63 | 64 | public static T Create(string host, int timeout = 10000) 65 | { 66 | lock (typeof(HttpApiClient)) 67 | { 68 | if (!mClients.TryGetValue(host, out HttpApiClient client)) 69 | { 70 | client = new HttpApiClient(host); 71 | client.TimeOut = timeout; 72 | } 73 | object result; 74 | result = client.Create(); 75 | ((HttpInterfaceProxy)result).TimeOut = timeout; 76 | return (T)result; 77 | } 78 | } 79 | 80 | public static T Create(int timeout = 10000) 81 | { 82 | object result; 83 | result = DispatchProxy.Create(); 84 | ((HttpInterfaceProxy)result).TimeOut = timeout; 85 | return (T)result; 86 | } 87 | 88 | public int TimeOut { get; set; } = 10000; 89 | 90 | public HttpHost Host { get; set; } 91 | 92 | protected async Task OnExecute(MethodBase targetMethod, params object[] args) 93 | { 94 | var rinfo = ClientActionFactory.GetHandler((MethodInfo)targetMethod).GetRequestInfo(args); 95 | var request = rinfo.GetRequest(Host); 96 | var respnse = await request.Execute(); 97 | if (respnse.Exception != null) 98 | throw respnse.Exception; 99 | return (T)respnse.Body; 100 | } 101 | 102 | private System.Collections.Concurrent.ConcurrentDictionary mAPI = new System.Collections.Concurrent.ConcurrentDictionary(); 103 | 104 | public T Create() 105 | { 106 | Type type = typeof(T); 107 | object result; 108 | if (!mAPI.TryGetValue(type, out result)) 109 | { 110 | result = DispatchProxy.Create(); 111 | 112 | mAPI[type] = result; 113 | ((HttpInterfaceProxy)result).Host = Host; 114 | ((HttpInterfaceProxy)result).TimeOut = TimeOut; 115 | } 116 | return (T)result; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/HttpClientException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | public class HttpClientException : Exception 8 | { 9 | public HttpClientException(string message, Exception innerError = null) : base($"{message}", innerError) 10 | { 11 | 12 | } 13 | 14 | public HttpClientException(Request request, Uri host, string message, Exception innerError = null) : base($"request {host} error {message}", innerError) 15 | { 16 | Request = request; 17 | Host = host; 18 | SocketError = false; 19 | if (innerError != null && (innerError is System.Net.Sockets.SocketException || innerError is ObjectDisposedException)) 20 | { 21 | SocketError = true; 22 | } 23 | } 24 | 25 | public int Code { get; internal set; } 26 | 27 | public Uri Host { get; internal set; } 28 | 29 | public Request Request { get; internal set; } 30 | 31 | public bool SocketError { get; internal set; } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/HttpClientPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using BeetleX.Buffers; 6 | using BeetleX.Clients; 7 | 8 | namespace BeetleX.Http.Clients 9 | { 10 | public class HttpClientPacket : IClientPacket 11 | { 12 | public HttpClientPacket() 13 | { 14 | 15 | } 16 | 17 | private Response response; 18 | 19 | public EventClientPacketCompleted Completed { get; set; } 20 | 21 | public IClient Client { get; set; } 22 | 23 | public IClientPacket Clone() 24 | { 25 | if (pipeStream != null) 26 | { 27 | pipeStream.Dispose(); 28 | pipeStream = null; 29 | } 30 | HttpClientPacket result = new HttpClientPacket(); 31 | result.Client = this.Client; 32 | this.Client = null; 33 | return result; 34 | } 35 | 36 | private BeetleX.Buffers.PipeStream pipeStream; 37 | 38 | private int chunkeLength; 39 | 40 | private bool end = false; 41 | 42 | private void loadChunkedData(PipeStream stream) 43 | { 44 | 45 | Next: 46 | string line; 47 | if (chunkeLength > 0) 48 | { 49 | if (pipeStream == null) 50 | pipeStream = new PipeStream(); 51 | while (true) 52 | { 53 | byte[] buffer = HttpParse.GetByteBuffer(); 54 | int count = buffer.Length; 55 | if (count > chunkeLength) 56 | count = chunkeLength; 57 | int read = stream.Read(buffer, 0, count); 58 | if (read == 0) 59 | return; 60 | pipeStream.Write(buffer, 0, read); 61 | chunkeLength -= read; 62 | if (chunkeLength == 0) 63 | { 64 | chunkeLength = 0; 65 | break; 66 | } 67 | } 68 | } 69 | else 70 | { 71 | if (!stream.TryReadWith(HeaderTypeFactory.LINE_BYTES, out line)) 72 | return; 73 | if (string.IsNullOrEmpty(line)) 74 | { 75 | if (end) 76 | { 77 | var item = response; 78 | pipeStream.Flush(); 79 | item.Stream = pipeStream; 80 | response = null; 81 | pipeStream = null; 82 | end = false; 83 | Completed?.Invoke(Client, item); 84 | return; 85 | } 86 | else 87 | { 88 | goto Next; 89 | } 90 | } 91 | else 92 | { 93 | try 94 | { 95 | chunkeLength = int.Parse(line, System.Globalization.NumberStyles.HexNumber); 96 | if (chunkeLength == 0) 97 | { 98 | end = true; 99 | } 100 | else 101 | response.Length += chunkeLength; 102 | } 103 | catch (Exception e_) 104 | { 105 | throw e_; 106 | } 107 | } 108 | } 109 | if (stream.Length > 0) 110 | goto Next; 111 | } 112 | 113 | public void Decode(IClient client, Stream stream) 114 | { 115 | try 116 | { 117 | var pipeStream = stream.ToPipeStream(); 118 | if (response == null) 119 | { 120 | response = new Response(); 121 | } 122 | if (response.Load(pipeStream) == LoadedState.Completed) 123 | { 124 | if (response.Chunked) 125 | { 126 | loadChunkedData(pipeStream); 127 | } 128 | else 129 | { 130 | if (response.Length == 0) 131 | { 132 | var item = response; 133 | response = null; 134 | Completed?.Invoke(Client, item); 135 | } 136 | else 137 | { 138 | if (pipeStream.Length >= response.Length) 139 | { 140 | var item = response; 141 | item.Stream = pipeStream; 142 | response = null; 143 | Completed?.Invoke(Client, item); 144 | } 145 | } 146 | } 147 | } 148 | } 149 | catch(Exception e_) 150 | { 151 | if (mIsDisposed) 152 | { 153 | throw new HttpClientException("Protocol data processing error, connection closed", e_); 154 | } 155 | else 156 | { 157 | throw new HttpClientException("Protocol data processing error", e_); 158 | } 159 | } 160 | } 161 | 162 | private bool mIsDisposed = false; 163 | 164 | public void Dispose() 165 | { 166 | if (!mIsDisposed) 167 | { 168 | Client = null; 169 | mIsDisposed = true; 170 | if (pipeStream != null) 171 | { 172 | pipeStream.Dispose(); 173 | pipeStream = null; 174 | } 175 | } 176 | } 177 | 178 | public void Encode(object data, IClient client, Stream stream) 179 | { 180 | Request request = (Request)data; 181 | request.Execute(stream.ToPipeStream()); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/HttpClusterApiBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Concurrent; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Text.RegularExpressions; 8 | using BeetleX.Tasks; 9 | 10 | namespace BeetleX.Http.Clients 11 | { 12 | public class HttpClusterApi : IDisposable 13 | { 14 | public HttpClusterApi() 15 | { 16 | DefaultNode = new ApiNode("*"); 17 | 18 | DetectionTime = 2000; 19 | mDetectionTimer = new System.Threading.Timer(OnVerifyClients, null, DetectionTime, DetectionTime); 20 | 21 | TimeOut = 30000; 22 | } 23 | 24 | private long mVersion; 25 | 26 | 27 | 28 | private System.Collections.Concurrent.ConcurrentDictionary mHandlers = new System.Collections.Concurrent.ConcurrentDictionary(); 29 | 30 | private void ChangeVersion() 31 | { 32 | System.Threading.Interlocked.Increment(ref mVersion); 33 | } 34 | 35 | public int TimeOut { get; set; } 36 | 37 | public long Version => mVersion; 38 | 39 | internal ClientActionHanler GetHandler(MethodInfo method) 40 | { 41 | ClientActionHanler result; 42 | if (!mHandlers.TryGetValue(method, out result)) 43 | { 44 | result = new ClientActionHanler(method); 45 | mHandlers[method] = result; 46 | } 47 | return result; 48 | } 49 | 50 | private System.Threading.Timer mUploadNodeTimer; 51 | 52 | private ApiClusterInfo mLastClusterInfo = new ApiClusterInfo(); 53 | 54 | public int DetectionTime { get; set; } 55 | 56 | public INodeSourceHandler NodeSourceHandler { get; set; } 57 | 58 | public bool UpdateSuccess { get; set; } 59 | 60 | public Exception UpdateExption { get; set; } 61 | 62 | private void UpdateNodeInfo(ApiClusterInfo info) 63 | { 64 | 65 | List removeUrls = new List(); 66 | removeUrls.Add("*"); 67 | foreach (string key in mNodes.Keys) 68 | { 69 | removeUrls.Add(key); 70 | } 71 | foreach (var item in info.Urls) 72 | { 73 | string url = item.Name.ToLower(); 74 | if (item.Hosts.Count > 0) 75 | { 76 | removeUrls.Remove(url); 77 | SetNode(url, item.GetNode()); 78 | } 79 | } 80 | foreach (string item in removeUrls) 81 | { 82 | RemoveNode(item); 83 | } 84 | ChangeVersion(); 85 | } 86 | 87 | private async void OnUploadNode_Callback(object sate) 88 | { 89 | mUploadNodeTimer.Change(-1, -1); 90 | try 91 | { 92 | var info = await NodeSourceHandler.Load(); 93 | if (info.Version != mLastClusterInfo.Version) 94 | { 95 | mLastClusterInfo = info; 96 | UpdateNodeInfo(info); 97 | } 98 | UpdateSuccess = true; 99 | UpdateExption = null; 100 | } 101 | catch (Exception e_) 102 | { 103 | UpdateSuccess = false; 104 | UpdateExption = e_; 105 | } 106 | finally 107 | { 108 | mUploadNodeTimer.Change(NodeSourceHandler.UpdateTime * 1000, NodeSourceHandler.UpdateTime * 1000); 109 | } 110 | } 111 | 112 | public async Task LoadNodeSource(string cluster, params string[] hosts) 113 | { 114 | HTTPRemoteSourceHandler hTTPRemoteSourceHandler = new HTTPRemoteSourceHandler(cluster, hosts); 115 | var result = await LoadNodeSource(hTTPRemoteSourceHandler); 116 | return result; 117 | 118 | } 119 | 120 | public async Task LoadNodeSource(INodeSourceHandler nodeSourceHandler) 121 | { 122 | NodeSourceHandler = nodeSourceHandler; 123 | var info = await NodeSourceHandler.Load(); 124 | UpdateNodeInfo(info); 125 | mLastClusterInfo = info; 126 | mUploadNodeTimer = new System.Threading.Timer(OnUploadNode_Callback, null, NodeSourceHandler.UpdateTime * 1000, NodeSourceHandler.UpdateTime * 1000); 127 | return this; 128 | } 129 | 130 | private System.Threading.Timer mDetectionTimer; 131 | 132 | 133 | private ConcurrentDictionary mAgents = new ConcurrentDictionary(); 134 | 135 | private ConcurrentDictionary mNodes = new ConcurrentDictionary(); 136 | 137 | public ConcurrentDictionary Nodes => mNodes; 138 | 139 | public IApiNode DefaultNode { get; internal set; } 140 | 141 | private IApiNode OnGetNode(string url) 142 | { 143 | url = url.ToLower(); 144 | if (mNodes.TryGetValue(url, out IApiNode node)) 145 | return node; 146 | return null; 147 | } 148 | 149 | private void RemoveNode(string url) 150 | { 151 | ChangeVersion(); 152 | if (url == "*") 153 | DefaultNode = new ApiNode("*"); 154 | else 155 | mNodes.TryRemove(url, out IApiNode apiNode); 156 | if (mAgents.TryGetValue(url, out ApiNodeAgent agent)) 157 | { 158 | agent.Node = DefaultNode;//new ApiNode(url); 159 | } 160 | } 161 | 162 | private IApiNode MatchNode(string url) 163 | { 164 | foreach (string key in mNodes.Keys) 165 | { 166 | if (Regex.IsMatch(url, key, RegexOptions.IgnoreCase)) 167 | { 168 | return mNodes[key]; 169 | } 170 | } 171 | return null; 172 | } 173 | 174 | public ApiNodeAgent GetAgent(string url) 175 | { 176 | IApiNode node = MatchNode(url); 177 | if (node == null) 178 | node = DefaultNode; 179 | if (!mAgents.TryGetValue(node.Url, out ApiNodeAgent agent)) 180 | { 181 | agent = new ApiNodeAgent(); 182 | agent.Url = node.Url; 183 | agent.Node = node; 184 | mAgents[node.Url] = agent; 185 | } 186 | agent.Version = this.Version; 187 | return agent; 188 | } 189 | 190 | public HttpClusterApi SetNode(string url, IApiNode node) 191 | { 192 | if (url == "*") 193 | DefaultNode = node; 194 | else 195 | mNodes[url.ToLower()] = node; 196 | if (mAgents.TryGetValue(url, out ApiNodeAgent agent)) 197 | { 198 | agent.Node = node; 199 | } 200 | else 201 | { 202 | agent = new ApiNodeAgent(); 203 | agent.Url = url; 204 | agent.Node = node; 205 | mAgents[url] = agent; 206 | } 207 | ChangeVersion(); 208 | return this; 209 | } 210 | 211 | private IApiNode CreateUrlNode(string url) 212 | { 213 | IApiNode node = new ApiNode(url); 214 | mNodes[node.Url] = node; 215 | ApiNodeAgent nodeAgent = new ApiNodeAgent(); 216 | nodeAgent.Node = node; 217 | nodeAgent.Url = url; 218 | mAgents[node.Url] = nodeAgent; 219 | ChangeVersion(); 220 | return node; 221 | } 222 | 223 | public IApiNode GetUrlNode(string url) 224 | { 225 | url = url.ToLower(); 226 | if (url == "*") 227 | return DefaultNode; 228 | IApiNode node = OnGetNode(url); 229 | if (node == null) 230 | { 231 | node = CreateUrlNode(url); 232 | } 233 | return node; 234 | } 235 | public HttpClusterApi AddHost(string url, string host, int weight = 10) 236 | { 237 | ChangeVersion(); 238 | if (url == "*") 239 | { 240 | DefaultNode.Add(host, weight); 241 | } 242 | else 243 | { 244 | url = url.ToLower(); 245 | IApiNode node = OnGetNode(url); 246 | if (node == null) 247 | { 248 | node = CreateUrlNode(url); 249 | } 250 | node.Add(host, weight); 251 | } 252 | return this; 253 | } 254 | 255 | public HttpClusterApi AddHost(string url, params string[] host) 256 | { 257 | ChangeVersion(); 258 | if (url == "*") 259 | { 260 | if (host != null) 261 | { 262 | foreach (string item in host) 263 | { 264 | DefaultNode.Add(item); 265 | } 266 | } 267 | } 268 | else 269 | { 270 | if (host != null) 271 | { 272 | foreach (string item in host) 273 | { 274 | AddHost(url, item); 275 | } 276 | } 277 | } 278 | return this; 279 | } 280 | 281 | private System.Collections.Concurrent.ConcurrentDictionary mAPI = new System.Collections.Concurrent.ConcurrentDictionary(); 282 | 283 | public T Create() 284 | { 285 | Type type = typeof(T); 286 | object result; 287 | if (!mAPI.TryGetValue(type, out result)) 288 | { 289 | result = DispatchProxy.Create(); 290 | mAPI[type] = result; 291 | ((HttpClusterApiProxy)result).Cluster = this; 292 | } 293 | return (T)result; 294 | } 295 | 296 | private void OnVerifyClients(object state) 297 | { 298 | mDetectionTimer.Change(-1, -1); 299 | try 300 | { 301 | foreach (IApiNode node in mNodes.Values) 302 | { 303 | node.Verify(); 304 | } 305 | 306 | if (DefaultNode != null) 307 | DefaultNode.Verify(); 308 | } 309 | catch { } 310 | finally 311 | { 312 | mDetectionTimer.Change(DetectionTime, DetectionTime); 313 | } 314 | 315 | } 316 | 317 | public ClusterStats Status() 318 | { 319 | ClusterStats result = new ClusterStats(); 320 | foreach (var node in mNodes.Values) 321 | { 322 | result.Items.Add(new ClusterStats.UrlStats() { Url = node.Url, Node = node }); 323 | } 324 | result.Items.Add(new ClusterStats.UrlStats() { Url = "*", Node = DefaultNode }); 325 | result.Items.Sort(); 326 | return result; 327 | } 328 | 329 | public void Dispose() 330 | { 331 | if (mUploadNodeTimer != null) 332 | mUploadNodeTimer.Dispose(); 333 | if (mDetectionTimer != null) 334 | mDetectionTimer.Dispose(); 335 | mHandlers.Clear(); 336 | 337 | } 338 | } 339 | 340 | public class ClusterStats 341 | { 342 | 343 | public ClusterStats() 344 | { 345 | Items = new List(); 346 | } 347 | public List Items { get; private set; } 348 | 349 | public class UrlStats : IComparable 350 | { 351 | 352 | public string Url { get; set; } 353 | 354 | public IApiNode Node { get; set; } 355 | 356 | public int CompareTo(object obj) 357 | { 358 | return Url.CompareTo(((UrlStats)obj).Url); 359 | } 360 | } 361 | 362 | public override string ToString() 363 | { 364 | StringBuilder stringBuilder = new StringBuilder(); 365 | stringBuilder.AppendLine("HttpClusterApi"); 366 | for (int i = 0; i < Items.Count; i++) 367 | { 368 | var item = Items[i]; 369 | stringBuilder.AppendLine($" |-Url:{item.Url}"); 370 | foreach (var client in item.Node.Hosts.ToArray()) 371 | { 372 | string available = client.Available ? "Y" : "N"; 373 | stringBuilder.AppendLine($" |--{client.Host}[{available}][W:{client.Weight}] [{client}]"); 374 | } 375 | } 376 | return stringBuilder.ToString(); 377 | } 378 | } 379 | 380 | public class HttpClusterApiProxy : System.Reflection.DispatchProxy, IHeaderHandler 381 | { 382 | public HttpClusterApiProxy() 383 | { 384 | 385 | } 386 | 387 | public HttpClusterApi Cluster { get; set; } 388 | 389 | public Dictionary Header { get; private set; } = new Dictionary(); 390 | 391 | protected override object Invoke(MethodInfo targetMethod, object[] args) 392 | { 393 | ClientActionHanler handler = Cluster.GetHandler((MethodInfo)targetMethod); 394 | var rinfo = handler.GetRequestInfo(args); 395 | if (handler.NodeAgent == null || handler.NodeAgent.Version != Cluster.Version) 396 | handler.NodeAgent = Cluster.GetAgent(rinfo.Url); 397 | HttpHost host = handler.NodeAgent.Node.GetClient(); 398 | if (host == null) 399 | { 400 | Exception error = new HttpClientException(null, null, $"request {rinfo.Url} no http nodes are available"); 401 | if (handler.Async) 402 | { 403 | //Type gtype = typeof(AnyCompletionSource<>); 404 | //Type type = gtype.MakeGenericType(handler.ReturnType); 405 | IAnyCompletionSource source = CompletionSourceFactory.Create(handler.ReturnType, Cluster.TimeOut); //(IAnyCompletionSource)Activator.CreateInstance(type); 406 | source.Error(error); 407 | return source.GetTask(); 408 | } 409 | else 410 | { 411 | throw error; 412 | } 413 | } 414 | if (!handler.Async) 415 | { 416 | throw new Exception($"{rinfo.Method} method is not supported and the return value must be task!"); 417 | } 418 | else 419 | { 420 | var request = rinfo.GetRequest(host); 421 | foreach (var item in Header) 422 | { 423 | request.Header[item.Key] = item.Value; 424 | } 425 | var task = request.Execute(); 426 | IAnyCompletionSource source = CompletionSourceFactory.Create(handler.ReturnType, Cluster.TimeOut); 427 | source.Wait(task, (c, t) => 428 | { 429 | if (t.Result.Exception != null) 430 | { 431 | c.Error(t.Result.Exception); 432 | } 433 | else 434 | { 435 | c.Success(t.Result.Body); 436 | } 437 | }); 438 | return source.GetTask(); 439 | } 440 | 441 | } 442 | } 443 | 444 | public class ApiNodeAgent 445 | { 446 | public long Version { get; set; } 447 | 448 | public string Url { get; set; } 449 | 450 | public IApiNode Node { get; set; } 451 | } 452 | 453 | public interface IApiNode 454 | { 455 | string Url { get; set; } 456 | 457 | IApiNode Add(string host, int weight); 458 | 459 | IApiNode Add(string host); 460 | 461 | IApiNode Add(IEnumerable hosts); 462 | 463 | HttpHost GetClient(); 464 | 465 | List Hosts { get; } 466 | 467 | void Verify(); 468 | } 469 | 470 | public class ApiNode : IApiNode 471 | { 472 | public ApiNode(string url) 473 | { 474 | Url = url.ToLower(); 475 | } 476 | public string Url { get; set; } 477 | 478 | public List Hosts => mClients; 479 | 480 | private List mClients = new List(); 481 | 482 | private long mID = 1; 483 | 484 | public const int TABLE_SIZE = 50; 485 | 486 | private HttpHost[] mHttpHostTable; 487 | 488 | public bool Available { get; private set; } 489 | 490 | public long Status 491 | { 492 | get 493 | { 494 | long result = 0; 495 | foreach (var item in mClients.ToArray()) 496 | { 497 | if (item.Available) 498 | result |= item.ID; 499 | else 500 | result |= 0; 501 | } 502 | if (result > 0) 503 | Available = true; 504 | return result; 505 | } 506 | } 507 | 508 | private long mLastStatus = 0; 509 | 510 | internal void RefreshWeightTable() 511 | { 512 | var status = Status; 513 | if (mLastStatus == status) 514 | return; 515 | else 516 | mLastStatus = status; 517 | HttpHost[] table = new HttpHost[TABLE_SIZE]; 518 | int sum = 0; 519 | mClients.Sort((x, y) => y.Weight.CompareTo(x.Weight)); 520 | List aclients = new List(); 521 | for (int i = 0; i < mClients.Count; i++) 522 | { 523 | if (mClients[i].Available) 524 | { 525 | sum += mClients[i].Weight; 526 | aclients.Add(mClients[i]); 527 | } 528 | } 529 | int count = 0; 530 | for (int i = 0; i < aclients.Count; i++) 531 | { 532 | int size = (int)((double)aclients[i].Weight / (double)sum * (double)TABLE_SIZE); 533 | for (int k = 0; k < size; k++) 534 | { 535 | table[count] = aclients[i]; 536 | count++; 537 | if (count >= TABLE_SIZE) 538 | goto END; 539 | } 540 | } 541 | int index = 0; 542 | while (count < TABLE_SIZE) 543 | { 544 | table[count] = aclients[index % aclients.Count]; 545 | index++; 546 | count++; 547 | } 548 | END: 549 | Shuffle(table); 550 | mHttpHostTable = table; 551 | } 552 | 553 | private void Shuffle(HttpHost[] list) 554 | { 555 | Random rng = new Random(); 556 | int n = list.Length; 557 | while (n > 1) 558 | { 559 | n--; 560 | int k = rng.Next(n + 1); 561 | HttpHost value = list[k]; 562 | list[k] = list[n]; 563 | list[n] = value; 564 | } 565 | } 566 | 567 | public IApiNode Add(IEnumerable hosts) 568 | { 569 | if (hosts != null) 570 | { 571 | foreach (var item in hosts) 572 | { 573 | Add(item, 10); 574 | } 575 | } 576 | return this; 577 | } 578 | 579 | public IApiNode Add(string host, int weight) 580 | { 581 | Uri url = new Uri(host); 582 | if (mClients.Find(c => c.Host == url.Host && c.Port == url.Port) == null) 583 | { 584 | var item = HttpHost.GetHttpHost(host);//new HttpHost(host); 585 | item.ID = mID << mClients.Count; 586 | item.Weight = weight; 587 | item.MaxRPS = 0; 588 | mClients.Add(item); 589 | RefreshWeightTable(); 590 | } 591 | return this; 592 | } 593 | 594 | public IApiNode Add(string host) 595 | { 596 | Add(host, 10); 597 | return this; 598 | } 599 | 600 | private long mIndex; 601 | 602 | public HttpHost GetClient() 603 | { 604 | if (Available) 605 | { 606 | HttpHost[] table = mHttpHostTable; 607 | if (table == null || table.Length == 0) 608 | return null; 609 | int count = 0; 610 | long index = System.Threading.Interlocked.Increment(ref mIndex); 611 | while (count < TABLE_SIZE) 612 | { 613 | 614 | HttpHost client = table[index % TABLE_SIZE]; 615 | if (client.Available) 616 | return client; 617 | index++; 618 | count++; 619 | } 620 | } 621 | return null; 622 | } 623 | 624 | public void Verify() 625 | { 626 | int count = 0; 627 | for (int i = 0; i < mClients.Count; i++) 628 | { 629 | HttpHost client = mClients[i]; 630 | if (!client.Available && !client.InVerify) 631 | { 632 | client.InVerify = true; 633 | count++; 634 | Task.Run(() => OnVerify(client)); 635 | } 636 | } 637 | RefreshWeightTable(); 638 | } 639 | 640 | private async void OnVerify(HttpHost host) 641 | { 642 | try 643 | { 644 | var request = host.Get("/", null, null, null, null); 645 | var result = await request.Execute(); 646 | } 647 | catch 648 | { 649 | 650 | } 651 | finally 652 | { 653 | host.InVerify = false; 654 | } 655 | } 656 | } 657 | 658 | } 659 | -------------------------------------------------------------------------------- /src/HttpParse.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace BeetleX.Http.Clients 8 | { 9 | public class HttpParse 10 | { 11 | public const string GET_TAG = "GET"; 12 | 13 | public const string POST_TAG = "POST"; 14 | 15 | public const string DELETE_TAG = "DELETE"; 16 | 17 | public const string PUT_TAG = "PUT"; 18 | 19 | public const string OPTIONS_TAG = "OPTIONS"; 20 | 21 | [ThreadStatic] 22 | private static char[] mCharCacheBuffer; 23 | 24 | public static char[] GetCharBuffer() 25 | { 26 | if (mCharCacheBuffer == null) 27 | mCharCacheBuffer = new char[1024 * 4]; 28 | return mCharCacheBuffer; 29 | } 30 | 31 | [ThreadStatic] 32 | private static byte[] mByteBuffer; 33 | public static byte[] GetByteBuffer() 34 | { 35 | if (mByteBuffer == null) 36 | mByteBuffer = new byte[1024 * 4]; 37 | return mByteBuffer; 38 | } 39 | [ThreadStatic] 40 | private static char[] mToLowerBuffer; 41 | public static char[] GetToLowerBuffer() 42 | { 43 | if (mToLowerBuffer == null) 44 | { 45 | mToLowerBuffer = new char[1024]; 46 | } 47 | return mToLowerBuffer; 48 | } 49 | 50 | public static ReadOnlySpan ReadCharLine(IndexOfResult result) 51 | { 52 | int offset = 0; 53 | char[] data = HttpParse.GetCharBuffer(); 54 | IMemoryBlock memory = result.Start; 55 | for (int i = result.StartPostion; i < memory.Length; i++) 56 | { 57 | data[offset] = (char)result.Start.Data[i]; 58 | offset++; 59 | if (offset == result.Length) 60 | break; 61 | } 62 | if (offset < result.Length) 63 | { 64 | 65 | Next: 66 | memory = result.Start.NextMemory; 67 | int count; 68 | if (memory.ID == result.End.ID) 69 | { 70 | count = result.EndPostion + 1; 71 | } 72 | else 73 | { 74 | count = memory.Length; 75 | } 76 | for (int i = 0; i < count; i++) 77 | { 78 | data[offset] = (char)memory.Data[i]; 79 | offset++; 80 | if (offset == result.Length) 81 | break; 82 | } 83 | if (offset < result.Length) 84 | goto Next; 85 | } 86 | return new ReadOnlySpan(data, 0, result.Length - 2); 87 | 88 | } 89 | 90 | public static string CharToLower(ReadOnlySpan url) 91 | { 92 | char[] buffer = GetToLowerBuffer(); 93 | for (int i = 0; i < url.Length; i++) 94 | buffer[i] = Char.ToLower(url[i]); 95 | return new string(buffer, 0, url.Length); 96 | } 97 | 98 | public static unsafe string GetBaseUrl(ReadOnlySpan url) 99 | { 100 | fixed (char* purl = url) 101 | { 102 | for (int i = 0; i < url.Length; i++) 103 | { 104 | if (url[i] == '?') 105 | { 106 | return new string(purl, 0, i); 107 | } 108 | } 109 | return new string(purl, 0, url.Length); 110 | } 111 | } 112 | 113 | public static string GetBaseUrlToLower(ReadOnlySpan url) 114 | { 115 | for (int i = 0; i < url.Length; i++) 116 | { 117 | if (url[i] == '?') 118 | { 119 | return CharToLower(url.Slice(0, i)); 120 | } 121 | } 122 | return CharToLower(url); 123 | } 124 | 125 | public static string MD5Encrypt(string filename) 126 | { 127 | using (MD5 md5Hash = MD5.Create()) 128 | { 129 | byte[] hash = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(filename)); 130 | 131 | return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); 132 | } 133 | } 134 | 135 | public static string GetBaseUrlExt(ReadOnlySpan url) 136 | { 137 | int offset = 0; 138 | for (int i = 0; i < url.Length; i++) 139 | { 140 | if (url[i] == '.') 141 | { 142 | offset = i + 1; 143 | } 144 | } 145 | if (offset > 0) 146 | return CharToLower(url.Slice(offset, url.Length - offset)); 147 | return null; 148 | } 149 | 150 | public unsafe static void AnalyzeCookie(ReadOnlySpan cookieData, Cookies cookies) 151 | { 152 | fixed (char* pData = cookieData) 153 | { 154 | int offset = 0; 155 | string name = null, value = null; 156 | for (int i = 0; i < cookieData.Length; i++) 157 | { 158 | if (cookieData[i] == '=') 159 | { 160 | if (cookieData[offset] == ' ') 161 | offset++; 162 | name = new string(pData, offset, i - offset); 163 | offset = i + 1; 164 | } 165 | if (name != null && cookieData[i] == ';') 166 | { 167 | value = new string(pData, offset, i - offset); 168 | offset = i + 1; 169 | cookies.Add(name, value); 170 | name = null; 171 | } 172 | } 173 | if (name != null) 174 | { 175 | value = new string(pData, offset, cookieData.Length - offset); 176 | cookies.Add(name, value); 177 | } 178 | } 179 | } 180 | 181 | 182 | private unsafe static ContentHeaderProperty[] GetProperties(ReadOnlySpan line) 183 | { 184 | fixed (char* pline = line) 185 | { 186 | List proerties = new List(); 187 | int offset = 0; 188 | string name = null; 189 | string value; 190 | for (int i = 0; i < line.Length; i++) 191 | { 192 | if (line[i] == ' ') 193 | { 194 | offset++; 195 | continue; 196 | } 197 | if (line[i] == '=') 198 | { 199 | name = new string(pline, offset, i - offset); 200 | offset = i + 1; 201 | } 202 | else if (line[i] == ';') 203 | { 204 | if (!string.IsNullOrEmpty(name)) 205 | { 206 | value = new string(pline, offset + 1, i - offset - 2); 207 | proerties.Add(new ContentHeaderProperty() { Name = name, Value = value }); 208 | offset = i + 1; 209 | name = null; 210 | } 211 | } 212 | } 213 | if (name != null) 214 | { 215 | value = new string(pline, offset + 1, line.Length - offset - 2); 216 | proerties.Add(new ContentHeaderProperty() { Name = name, Value = value }); 217 | } 218 | return proerties.ToArray(); 219 | } 220 | } 221 | 222 | public unsafe static ContentHeader AnalyzeContentHeader(ReadOnlySpan line) 223 | { 224 | fixed (char* pline = line) 225 | { 226 | ContentHeader result = new ContentHeader(); 227 | ReadOnlySpan property = line; 228 | int offset = 0; 229 | for (int i = 0; i < line.Length; i++) 230 | { 231 | if (line[i] == ':') 232 | { 233 | result.Name = new string(pline, 0, i); 234 | offset = i + 1; 235 | } 236 | else if (offset > 0 && line[i] == ' ') 237 | offset = i + 1; 238 | else if (line[i] == ';') 239 | { 240 | result.Value = new string(pline, offset, i - offset); 241 | property = line.Slice(i + 1); 242 | offset = 0; 243 | break; 244 | } 245 | } 246 | if (offset > 0) 247 | { 248 | result.Value = new string(pline, offset, line.Length - offset); 249 | } 250 | if (property.Length != line.Length) 251 | { 252 | result.Properties = GetProperties(property); 253 | } 254 | return result; 255 | } 256 | } 257 | 258 | 259 | public unsafe static Tuple AnalyzeHeader(ReadOnlySpan line) 260 | { 261 | Span charbuffer = GetCharBuffer(); 262 | fixed (byte* pline = line) 263 | { 264 | fixed (char* pchar = charbuffer) 265 | { 266 | var len = Encoding.UTF8.GetChars(pline, line.Length, pchar, charbuffer.Length); 267 | return AnalyzeHeader(charbuffer.Slice(0, len)); 268 | } 269 | } 270 | } 271 | public unsafe static Tuple AnalyzeHeader(ReadOnlySpan line) 272 | { 273 | fixed (char* pline = line) 274 | { 275 | string name = null, value = null; 276 | int offset = 0; 277 | for (int i = 0; i < line.Length; i++) 278 | { 279 | if (line[i] == ':' && name == null) 280 | { 281 | name = new string(pline, offset, i - offset); 282 | offset = i + 1; 283 | } 284 | else 285 | { 286 | if (name != null) 287 | { 288 | if (line[i] == ' ') 289 | offset++; 290 | else 291 | break; 292 | } 293 | } 294 | } 295 | value = new string(pline, offset, line.Length - offset); 296 | return new Tuple(name, value); 297 | } 298 | } 299 | 300 | public unsafe static Tuple AnalyzeResponseLine(ReadOnlySpan line) 301 | { 302 | Span charbuffer = GetCharBuffer(); 303 | fixed (byte* pline = line) 304 | { 305 | fixed (char* pchar = charbuffer) 306 | { 307 | var len = Encoding.UTF8.GetChars(pline, line.Length, pchar, charbuffer.Length); 308 | return AnalyzeResponseLine(charbuffer.Slice(0, len)); 309 | } 310 | } 311 | } 312 | public unsafe static void AnalyzeResponseLine(ReadOnlySpan line, Response response) 313 | { 314 | fixed (char* pline = line) 315 | { 316 | int offset = 0; 317 | int count = 0; 318 | for (int i = 0; i < line.Length; i++) 319 | { 320 | if (line[i] == ' ') 321 | { 322 | if (count == 0) 323 | { 324 | response.HttpVersion = new string(pline,offset,i-offset); 325 | offset = i + 1; 326 | } 327 | else 328 | { 329 | response.Code = new string(pline, offset, i - offset); 330 | offset = i + 1; 331 | response.CodeMsg = new string(pline, offset, line.Length - offset); 332 | return; 333 | } 334 | count++; 335 | } 336 | } 337 | } 338 | } 339 | public unsafe static Tuple AnalyzeResponseLine(ReadOnlySpan line) 340 | { 341 | fixed (char* pline = line) 342 | { 343 | string httpversion = null, codemsg = null; 344 | int code = 200; 345 | int offset = 0; 346 | int count = 0; 347 | for (int i = 0; i < line.Length; i++) 348 | { 349 | if (line[i] == ' ') 350 | { 351 | if (count == 0) 352 | { 353 | httpversion = new string(pline, offset, i - offset); 354 | offset = i + 1; 355 | } 356 | else 357 | { 358 | code = int.Parse(new string(pline, offset, i - offset)); 359 | offset = i + 1; 360 | codemsg = new string(pline, offset, line.Length - offset); 361 | break; 362 | } 363 | count++; 364 | } 365 | } 366 | return new Tuple(httpversion, code, codemsg); 367 | } 368 | } 369 | 370 | 371 | 372 | 373 | public struct ContentHeader 374 | { 375 | public string Name; 376 | 377 | public string Value; 378 | 379 | public ContentHeaderProperty[] Properties { get; set; } 380 | 381 | } 382 | 383 | public struct ContentHeaderProperty 384 | { 385 | public string Name; 386 | public string Value; 387 | } 388 | 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /src/IBodyFormater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | using BeetleX.Buffers; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace BeetleX.Http.Clients 11 | { 12 | public interface IBodyFormater 13 | { 14 | string ContentType { get; } 15 | 16 | void Serialization(Request request, object data, PipeStream stream); 17 | 18 | object Deserialization(Response response, BeetleX.Buffers.PipeStream stream, Type type, int length); 19 | 20 | void Setting(Request request); 21 | } 22 | 23 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Class)] 24 | public abstract class FormaterAttribute : Attribute, IBodyFormater 25 | { 26 | public abstract string ContentType { get; } 27 | 28 | public abstract void Serialization(Request request, object data, PipeStream stream); 29 | 30 | public abstract object Deserialization(Response response, BeetleX.Buffers.PipeStream stream, Type type, int length); 31 | 32 | public virtual void Setting(Request request) { } 33 | } 34 | 35 | public class FormUrlFormater : FormaterAttribute 36 | { 37 | public override string ContentType => "application/x-www-form-urlencoded"; 38 | 39 | public override object Deserialization(Response response, PipeStream stream, Type type, int length) 40 | { 41 | return stream.ReadString(length); 42 | } 43 | public override void Serialization(Request request, object data, PipeStream stream) 44 | { 45 | if (data != null) 46 | { 47 | System.Collections.IDictionary keyValuePairs = data as IDictionary; 48 | if (keyValuePairs != null) 49 | { 50 | int i = 0; 51 | foreach (object key in keyValuePairs.Keys) 52 | { 53 | object value = keyValuePairs[key]; 54 | if (value != null) 55 | { 56 | if (i > 0) 57 | stream.Write("&"); 58 | stream.Write(key.ToString() + "="); 59 | if (value is string) 60 | { 61 | stream.Write(System.Net.WebUtility.UrlEncode((string)value)); 62 | } 63 | else 64 | { 65 | if (value is IEnumerable subitems) 66 | { 67 | List values = new List(); 68 | foreach (var v in subitems) 69 | values.Add(v.ToString()); 70 | stream.Write(string.Join(",", values)); 71 | } 72 | else 73 | { 74 | stream.Write(System.Net.WebUtility.UrlEncode(value.ToString())); 75 | } 76 | } 77 | i++; 78 | } 79 | } 80 | } 81 | else 82 | { 83 | stream.Write(data.ToString()); 84 | } 85 | } 86 | } 87 | } 88 | 89 | public class JsonFormater : FormaterAttribute 90 | { 91 | public override string ContentType => "application/json"; 92 | 93 | public override object Deserialization(Response response, PipeStream stream, Type type, int length) 94 | { 95 | using (stream.LockFree()) 96 | { 97 | if (type == null) 98 | { 99 | using (System.IO.StreamReader streamReader = new System.IO.StreamReader(stream)) 100 | using (JsonTextReader reader = new JsonTextReader(streamReader)) 101 | { 102 | JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(); 103 | object token = jsonSerializer.Deserialize(reader); 104 | return token; 105 | } 106 | } 107 | else 108 | { 109 | using (StreamReader streamReader = new StreamReader(stream)) 110 | { 111 | JsonSerializer serializer = JsonSerializer.CreateDefault(); 112 | object result = serializer.Deserialize(streamReader, type); 113 | return result; 114 | } 115 | } 116 | } 117 | } 118 | 119 | public override void Serialization(Request request, object data, PipeStream stream) 120 | { 121 | 122 | if (data != null) 123 | { 124 | if (data is string text) 125 | { 126 | stream.Write(text); 127 | } 128 | else if (data is StringBuilder sb) 129 | { 130 | stream.Write(sb); 131 | } 132 | else 133 | { 134 | using (stream.LockFree()) 135 | { 136 | using (StreamWriter writer = new StreamWriter(stream)) 137 | { 138 | IDictionary dictionary = data as IDictionary; 139 | JsonSerializer serializer = new JsonSerializer(); 140 | 141 | if (dictionary != null && dictionary.Count == 1) 142 | { 143 | object[] vlaues = new object[dictionary.Count]; 144 | dictionary.Values.CopyTo(vlaues, 0); 145 | serializer.Serialize(writer, vlaues[0]); 146 | } 147 | else 148 | { 149 | serializer.Serialize(writer, data); 150 | } 151 | 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | public class FromDataFormater : FormaterAttribute 160 | { 161 | public override string ContentType => "multipart/form-data"; 162 | 163 | public override void Setting(Request request) 164 | { 165 | request.Boundary = "----Beetlex.io" + DateTime.Now.ToString("yyyyMMddHHmmss"); 166 | var value = request.Header["Content-Type"]; 167 | value += "; boundary=" + request.Boundary; 168 | request.Header["Content-Type"] = value; 169 | base.Setting(request); 170 | 171 | } 172 | 173 | public override object Deserialization(Response response, PipeStream stream, Type type, int length) 174 | { 175 | return stream.ReadString(length); 176 | } 177 | 178 | public override void Serialization(Request request, object data, PipeStream stream) 179 | { 180 | if (data == null) 181 | return; 182 | if (data is IDictionary dictionary) 183 | { 184 | foreach (var item in dictionary) 185 | { 186 | stream.Write("--"); 187 | stream.WriteLine(request.Boundary); 188 | if (item.Value is UploadFile uploadFile) 189 | { 190 | stream.WriteLine($"Content-Disposition: form-data; name=\"{item.Key}\"; filename=\"{uploadFile.Name}\""); 191 | stream.WriteLine($"Content-Type: binary"); 192 | stream.WriteLine(""); 193 | stream.Write(uploadFile.Data.Array, uploadFile.Data.Offset, uploadFile.Data.Count); 194 | stream.WriteLine(""); 195 | } 196 | else if (item.Value is FileInfo file) 197 | { 198 | stream.WriteLine($"Content-Disposition: form-data; name=\"{item.Key}\"; filename=\"{file.Name}\""); 199 | stream.WriteLine($"Content-Type: binary"); 200 | stream.WriteLine(""); 201 | using (System.IO.Stream open = file.OpenRead()) 202 | { 203 | open.CopyTo(stream); 204 | } 205 | stream.WriteLine(""); 206 | } 207 | else 208 | { 209 | stream.WriteLine($"Content-Disposition: form-data; name=\"{item.Key}\""); 210 | stream.WriteLine(""); 211 | if (item.Value is IEnumerable subitems) 212 | { 213 | List values = new List(); 214 | foreach (var v in subitems) 215 | values.Add(v.ToString()); 216 | stream.Write(string.Join(",", values)); 217 | } 218 | else 219 | { 220 | stream.Write(item.Value.ToString()); 221 | } 222 | stream.WriteLine(""); 223 | } 224 | } 225 | if (dictionary.Count > 0) 226 | { 227 | stream.Write("--"); 228 | stream.Write(request.Boundary); 229 | stream.WriteLine("--"); 230 | } 231 | } 232 | else 233 | { 234 | throw new HttpClientException($"post data must be IDictionary!"); 235 | } 236 | } 237 | } 238 | 239 | public class BinaryFormater : FormaterAttribute 240 | { 241 | public override string ContentType => "application/octet-stream"; 242 | 243 | public override object Deserialization(Response response, PipeStream stream, Type type, int length) 244 | { 245 | var result = System.Buffers.ArrayPool.Shared.Rent(length); 246 | stream.Read(result, 0, length); 247 | return new ArraySegment(result, 0, length); 248 | } 249 | 250 | public override void Serialization(Request request, object data, PipeStream stream) 251 | { 252 | if (data is Byte[] buffer) 253 | { 254 | stream.Write(buffer, 0, buffer.Length); 255 | } 256 | else if (data is ArraySegment array) 257 | { 258 | stream.Write(array.Array, array.Offset, array.Count); 259 | } 260 | else 261 | { 262 | throw new Exception("Commit data must be byte[] or ArraySegment"); 263 | } 264 | } 265 | } 266 | 267 | 268 | } 269 | -------------------------------------------------------------------------------- /src/IHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | public interface IHeaderHandler 8 | { 9 | Dictionary Header { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/INodeSourcesHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace BeetleX.Http.Clients 7 | { 8 | public class NodeSource 9 | { 10 | public string Url { get; set; } 11 | 12 | public IApiNode Node { get; set; } 13 | } 14 | 15 | public interface INodeSourceHandler 16 | { 17 | int UpdateTime { get; set; } 18 | Task Load(); 19 | } 20 | 21 | [JsonFormater] 22 | public interface IHttpSourceApi 23 | { 24 | [Get] 25 | Task _GetCluster(string cluster = "default"); 26 | } 27 | 28 | public class HTTPRemoteSourceHandler : INodeSourceHandler 29 | { 30 | private HttpClusterApi mHttpClusterApi = new HttpClusterApi(); 31 | 32 | private IHttpSourceApi mRemoteSourceApi; 33 | 34 | public HTTPRemoteSourceHandler(string name, params string[] hosts) 35 | { 36 | mHttpClusterApi.AddHost("*", hosts); 37 | mRemoteSourceApi = mHttpClusterApi.Create(); 38 | Name = name; 39 | } 40 | 41 | public string Name { get; set; } 42 | 43 | public int UpdateTime { get; set; } = 5; 44 | 45 | public Task Load() 46 | { 47 | return mRemoteSourceApi._GetCluster(Name); 48 | } 49 | } 50 | 51 | 52 | public class ApiClusterInfo 53 | { 54 | public ApiClusterInfo() 55 | { 56 | Urls = new List(); 57 | } 58 | 59 | public string Name { get; set; } 60 | 61 | public string Version { get; set; } 62 | 63 | public List Urls { get; set; } 64 | } 65 | 66 | public class UrlNodeInfo 67 | { 68 | 69 | public UrlNodeInfo() 70 | { 71 | Hosts = new List(); 72 | } 73 | 74 | public string Name { get; set; } 75 | 76 | public List Hosts { get; set; } 77 | 78 | public ApiNode GetNode() 79 | { 80 | ApiNode result = new ApiNode(Name); 81 | foreach (var item in Hosts) 82 | { 83 | result.Add(item.Name, item.Weight); 84 | } 85 | return result; 86 | 87 | } 88 | } 89 | 90 | public class UrlHostInfo 91 | { 92 | public string Name { get; set; } 93 | 94 | public int Weight { get; set; } 95 | 96 | public int MaxRPS { get; set; } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Northwind.Data/Customer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using MessagePack; 7 | using ProtoBuf; 8 | namespace Northwind.Data 9 | { 10 | [MessagePackObject] 11 | [ProtoContract] 12 | public class Customer 13 | { 14 | [ProtoMember(1)] 15 | [Key(0)] 16 | public string CustomerID { get; set; } 17 | [ProtoMember(2)] 18 | [Key(1)] 19 | public string CompanyName { get; set; } 20 | [ProtoMember(3)] 21 | [Key(2)] 22 | public string ContactName { get; set; } 23 | [ProtoMember(4)] 24 | [Key(3)] 25 | public string ContactTitle { get; set; } 26 | [ProtoMember(5)] 27 | [Key(4)] 28 | public string Address { get; set; } 29 | [ProtoMember(6)] 30 | [Key(5)] 31 | public string City { get; set; } 32 | [ProtoMember(7)] 33 | [Key(6)] 34 | public string PostalCode { get; set; } 35 | [ProtoMember(8)] 36 | [Key(7)] 37 | public string Country { get; set; } 38 | [ProtoMember(9)] 39 | [Key(8)] 40 | public string Phone { get; set; } 41 | [ProtoMember(10)] 42 | [Key(9)] 43 | public string Fax { get; set; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Northwind.Data/Customers.txt: -------------------------------------------------------------------------------- 1 | [{"CustomerID":"ALFKI","CompanyName":"Alfreds Futterkiste","ContactName":"Maria Anders","ContactTitle":"Sales Representative","Address":"Obere Str. 57","City":"Berlin","PostalCode":"12209","Country":"Germany","Phone":"030-0074321","Fax":"030-0076545"},{"CustomerID":"ANATR","CompanyName":"Ana Trujillo Emparedados y helados","ContactName":"Ana Trujillo","ContactTitle":"Owner","Address":"Avda. de la Constitución 2222","City":"México D.F.","PostalCode":"05021","Country":"Mexico","Phone":"(5) 555-4729","Fax":"(5) 555-3745"},{"CustomerID":"ANTON","CompanyName":"Antonio Moreno Taquería","ContactName":"Antonio Moreno","ContactTitle":"Owner","Address":"Mataderos 2312","City":"México D.F.","PostalCode":"05023","Country":"Mexico","Phone":"(5) 555-3932","Fax":""},{"CustomerID":"AROUT","CompanyName":"Around the Horn","ContactName":"Thomas Hardy","ContactTitle":"Sales Representative","Address":"120 Hanover Sq.","City":"London","PostalCode":"WA1 1DP","Country":"UK","Phone":"(171) 555-7788","Fax":"(171) 555-6750"},{"CustomerID":"BERGS","CompanyName":"Berglunds snabbköp","ContactName":"Christina Berglund","ContactTitle":"Order Administrator","Address":"Berguvsvägen 8","City":"Luleå","PostalCode":"S-958 22","Country":"Sweden","Phone":"0921-12 34 65","Fax":"0921-12 34 67"},{"CustomerID":"BLAUS","CompanyName":"Blauer See Delikatessen","ContactName":"Hanna Moos","ContactTitle":"Sales Representative","Address":"Forsterstr. 57","City":"Mannheim","PostalCode":"68306","Country":"Germany","Phone":"0621-08460","Fax":"0621-08924"},{"CustomerID":"BLONP","CompanyName":"Blondel père et fils","ContactName":"Frédérique Citeaux","ContactTitle":"Marketing Manager","Address":"24, place Kléber","City":"Strasbourg","PostalCode":"67000","Country":"France","Phone":"88.60.15.31","Fax":"88.60.15.32"},{"CustomerID":"BOLID","CompanyName":"Bólido Comidas preparadas","ContactName":"Martín Sommer","ContactTitle":"Owner","Address":"C/ Araquil, 67","City":"Madrid","PostalCode":"28023","Country":"Spain","Phone":"(91) 555 22 82","Fax":"(91) 555 91 99"},{"CustomerID":"BONAP","CompanyName":"Bon app'","ContactName":"Laurence Lebihan","ContactTitle":"Owner","Address":"12, rue des Bouchers","City":"Marseille","PostalCode":"13008","Country":"France","Phone":"91.24.45.40","Fax":"91.24.45.41"},{"CustomerID":"BOTTM","CompanyName":"Bottom-Dollar Markets","ContactName":"Elizabeth Lincoln","ContactTitle":"Accounting Manager","Address":"23 Tsawassen Blvd.","City":"Tsawassen","PostalCode":"T2F 8M4","Country":"Canada","Phone":"(604) 555-4729","Fax":"(604) 555-3745"},{"CustomerID":"BSBEV","CompanyName":"B's Beverages","ContactName":"Victoria Ashworth","ContactTitle":"Sales Representative","Address":"Fauntleroy Circus","City":"London","PostalCode":"EC2 5NT","Country":"UK","Phone":"(171) 555-1212","Fax":""},{"CustomerID":"CACTU","CompanyName":"Cactus Comidas para llevar","ContactName":"Patricio Simpson","ContactTitle":"Sales Agent","Address":"Cerrito 333","City":"Buenos Aires","PostalCode":"1010","Country":"Argentina","Phone":"(1) 135-5555","Fax":"(1) 135-4892"},{"CustomerID":"CENTC","CompanyName":"Centro comercial Moctezuma","ContactName":"Francisco Chang","ContactTitle":"Marketing Manager","Address":"Sierras de Granada 9993","City":"México D.F.","PostalCode":"05022","Country":"Mexico","Phone":"(5) 555-3392","Fax":"(5) 555-7293"},{"CustomerID":"CHOPS","CompanyName":"Chop-suey Chinese","ContactName":"Yang Wang","ContactTitle":"Owner","Address":"Hauptstr. 29","City":"Bern","PostalCode":"3012","Country":"Switzerland","Phone":"0452-076545","Fax":""},{"CustomerID":"COMMI","CompanyName":"Comércio Mineiro","ContactName":"Pedro Afonso","ContactTitle":"Sales Associate","Address":"Av. dos Lusíadas, 23","City":"São Paulo","PostalCode":"05432-043","Country":"Brazil","Phone":"(11) 555-7647","Fax":""},{"CustomerID":"CONSH","CompanyName":"Consolidated Holdings","ContactName":"Elizabeth Brown","ContactTitle":"Sales Representative","Address":"Berkeley Gardens\n12 Brewery ","City":"London","PostalCode":"WX1 6LT","Country":"UK","Phone":"(171) 555-2282","Fax":"(171) 555-9199"},{"CustomerID":"DRACD","CompanyName":"Drachenblut Delikatessen","ContactName":"Sven Ottlieb","ContactTitle":"Order Administrator","Address":"Walserweg 21","City":"Aachen","PostalCode":"52066","Country":"Germany","Phone":"0241-039123","Fax":"0241-059428"},{"CustomerID":"DUMON","CompanyName":"Du monde entier","ContactName":"Janine Labrune","ContactTitle":"Owner","Address":"67, rue des Cinquante Otages","City":"Nantes","PostalCode":"44000","Country":"France","Phone":"40.67.88.88","Fax":"40.67.89.89"},{"CustomerID":"EASTC","CompanyName":"Eastern Connection","ContactName":"Ann Devon","ContactTitle":"Sales Agent","Address":"35 King George","City":"London","PostalCode":"WX3 6FW","Country":"UK","Phone":"(171) 555-0297","Fax":"(171) 555-3373"},{"CustomerID":"ERNSH","CompanyName":"Ernst Handel","ContactName":"Roland Mendel","ContactTitle":"Sales Manager","Address":"Kirchgasse 6","City":"Graz","PostalCode":"8010","Country":"Austria","Phone":"7675-3425","Fax":"7675-3426"},{"CustomerID":"FAMIA","CompanyName":"Familia Arquibaldo","ContactName":"Aria Cruz","ContactTitle":"Marketing Assistant","Address":"Rua Orós, 92","City":"São Paulo","PostalCode":"05442-030","Country":"Brazil","Phone":"(11) 555-9857","Fax":""},{"CustomerID":"FISSA","CompanyName":"FISSA Fabrica Inter. Salchichas S.A.","ContactName":"Diego Roel","ContactTitle":"Accounting Manager","Address":"C/ Moralzarzal, 86","City":"Madrid","PostalCode":"28034","Country":"Spain","Phone":"(91) 555 94 44","Fax":"(91) 555 55 93"},{"CustomerID":"FOLIG","CompanyName":"Folies gourmandes","ContactName":"Martine Rancé","ContactTitle":"Assistant Sales Agent","Address":"184, chaussée de Tournai","City":"Lille","PostalCode":"59000","Country":"France","Phone":"20.16.10.16","Fax":"20.16.10.17"},{"CustomerID":"FOLKO","CompanyName":"Folk och fä HB","ContactName":"Maria Larsson","ContactTitle":"Owner","Address":"Åkergatan 24","City":"Bräcke","PostalCode":"S-844 67","Country":"Sweden","Phone":"0695-34 67 21","Fax":""},{"CustomerID":"FRANK","CompanyName":"Frankenversand","ContactName":"Peter Franken","ContactTitle":"Marketing Manager","Address":"Berliner Platz 43","City":"München","PostalCode":"80805","Country":"Germany","Phone":"089-0877310","Fax":"089-0877451"},{"CustomerID":"FRANR","CompanyName":"France restauration","ContactName":"Carine Schmitt","ContactTitle":"Marketing Manager","Address":"54, rue Royale","City":"Nantes","PostalCode":"44000","Country":"France","Phone":"40.32.21.21","Fax":"40.32.21.20"},{"CustomerID":"FRANS","CompanyName":"Franchi S.p.A.","ContactName":"Paolo Accorti","ContactTitle":"Sales Representative","Address":"Via Monte Bianco 34","City":"Torino","PostalCode":"10100","Country":"Italy","Phone":"011-4988260","Fax":"011-4988261"},{"CustomerID":"FURIB","CompanyName":"Furia Bacalhau e Frutos do Mar","ContactName":"Lino Rodriguez ","ContactTitle":"Sales Manager","Address":"Jardim das rosas n. 32","City":"Lisboa","PostalCode":"1675","Country":"Portugal","Phone":"(1) 354-2534","Fax":"(1) 354-2535"},{"CustomerID":"GALED","CompanyName":"Galería del gastrónomo","ContactName":"Eduardo Saavedra","ContactTitle":"Marketing Manager","Address":"Rambla de Cataluña, 23","City":"Barcelona","PostalCode":"08022","Country":"Spain","Phone":"(93) 203 4560","Fax":"(93) 203 4561"},{"CustomerID":"GODOS","CompanyName":"Godos Cocina Típica","ContactName":"José Pedro Freyre","ContactTitle":"Sales Manager","Address":"C/ Romero, 33","City":"Sevilla","PostalCode":"41101","Country":"Spain","Phone":"(95) 555 82 82","Fax":""},{"CustomerID":"GOURL","CompanyName":"Gourmet Lanchonetes","ContactName":"André Fonseca","ContactTitle":"Sales Associate","Address":"Av. Brasil, 442","City":"Campinas","PostalCode":"04876-786","Country":"Brazil","Phone":"(11) 555-9482","Fax":""},{"CustomerID":"GREAL","CompanyName":"Great Lakes Food Market","ContactName":"Howard Snyder","ContactTitle":"Marketing Manager","Address":"2732 Baker Blvd.","City":"Eugene","PostalCode":"97403","Country":"USA","Phone":"(503) 555-7555","Fax":""},{"CustomerID":"GROSR","CompanyName":"GROSELLA-Restaurante","ContactName":"Manuel Pereira","ContactTitle":"Owner","Address":"5ª Ave. Los Palos Grandes","City":"Caracas","PostalCode":"1081","Country":"Venezuela","Phone":"(2) 283-2951","Fax":"(2) 283-3397"},{"CustomerID":"HANAR","CompanyName":"Hanari Carnes","ContactName":"Mario Pontes","ContactTitle":"Accounting Manager","Address":"Rua do Paço, 67","City":"Rio de Janeiro","PostalCode":"05454-876","Country":"Brazil","Phone":"(21) 555-0091","Fax":"(21) 555-8765"},{"CustomerID":"HILAA","CompanyName":"HILARIÓN-Abastos","ContactName":"Carlos Hernández","ContactTitle":"Sales Representative","Address":"Carrera 22 con Ave. Carlos Soublette #8-35","City":"San Cristóbal","PostalCode":"5022","Country":"Venezuela","Phone":"(5) 555-1340","Fax":"(5) 555-1948"},{"CustomerID":"HUNGC","CompanyName":"Hungry Coyote Import Store","ContactName":"Yoshi Latimer","ContactTitle":"Sales Representative","Address":"City Center Plaza\n516 Main St.","City":"Elgin","PostalCode":"97827","Country":"USA","Phone":"(503) 555-6874","Fax":"(503) 555-2376"},{"CustomerID":"HUNGO","CompanyName":"Hungry Owl All-Night Grocers","ContactName":"Patricia McKenna","ContactTitle":"Sales Associate","Address":"8 Johnstown Road","City":"Cork","PostalCode":"","Country":"Ireland","Phone":"2967 542","Fax":"2967 3333"},{"CustomerID":"ISLAT","CompanyName":"Island Trading","ContactName":"Helen Bennett","ContactTitle":"Marketing Manager","Address":"Garden House\nCrowther Way","City":"Cowes","PostalCode":"PO31 7PJ","Country":"UK","Phone":"(198) 555-8888","Fax":""},{"CustomerID":"KOENE","CompanyName":"Königlich Essen","ContactName":"Philip Cramer","ContactTitle":"Sales Associate","Address":"Maubelstr. 90","City":"Brandenburg","PostalCode":"14776","Country":"Germany","Phone":"0555-09876","Fax":""},{"CustomerID":"LACOR","CompanyName":"La corne d'abondance","ContactName":"Daniel Tonini","ContactTitle":"Sales Representative","Address":"67, avenue de l'Europe","City":"Versailles","PostalCode":"78000","Country":"France","Phone":"30.59.84.10","Fax":"30.59.85.11"},{"CustomerID":"LAMAI","CompanyName":"La maison d'Asie","ContactName":"Annette Roulet","ContactTitle":"Sales Manager","Address":"1 rue Alsace-Lorraine","City":"Toulouse","PostalCode":"31000","Country":"France","Phone":"61.77.61.10","Fax":"61.77.61.11"},{"CustomerID":"LAUGB","CompanyName":"Laughing Bacchus Wine Cellars","ContactName":"Yoshi Tannamuri","ContactTitle":"Marketing Assistant","Address":"1900 Oak St.","City":"Vancouver","PostalCode":"V3F 2K1","Country":"Canada","Phone":"(604) 555-3392","Fax":"(604) 555-7293"},{"CustomerID":"LAZYK","CompanyName":"Lazy K Kountry Store","ContactName":"John Steel","ContactTitle":"Marketing Manager","Address":"12 Orchestra Terrace","City":"Walla Walla","PostalCode":"99362","Country":"USA","Phone":"(509) 555-7969","Fax":"(509) 555-6221"},{"CustomerID":"LEHMS","CompanyName":"Lehmanns Marktstand","ContactName":"Renate Messner","ContactTitle":"Sales Representative","Address":"Magazinweg 7","City":"Frankfurt a.M. ","PostalCode":"60528","Country":"Germany","Phone":"069-0245984","Fax":"069-0245874"},{"CustomerID":"LETSS","CompanyName":"Let's Stop N Shop","ContactName":"Jaime Yorres","ContactTitle":"Owner","Address":"87 Polk St.\nSuite 5","City":"San Francisco","PostalCode":"94117","Country":"USA","Phone":"(415) 555-5938","Fax":""},{"CustomerID":"LILAS","CompanyName":"LILA-Supermercado","ContactName":"Carlos González","ContactTitle":"Accounting Manager","Address":"Carrera 52 con Ave. Bolívar #65-98 Llano Largo","City":"Barquisimeto","PostalCode":"3508","Country":"Venezuela","Phone":"(9) 331-6954","Fax":"(9) 331-7256"},{"CustomerID":"LINOD","CompanyName":"LINO-Delicateses","ContactName":"Felipe Izquierdo","ContactTitle":"Owner","Address":"Ave. 5 de Mayo Porlamar","City":"I. de Margarita","PostalCode":"4980","Country":"Venezuela","Phone":"(8) 34-56-12","Fax":"(8) 34-93-93"},{"CustomerID":"LONEP","CompanyName":"Lonesome Pine Restaurant","ContactName":"Fran Wilson","ContactTitle":"Sales Manager","Address":"89 Chiaroscuro Rd.","City":"Portland","PostalCode":"97219","Country":"USA","Phone":"(503) 555-9573","Fax":"(503) 555-9646"},{"CustomerID":"MAGAA","CompanyName":"Magazzini Alimentari Riuniti","ContactName":"Giovanni Rovelli","ContactTitle":"Marketing Manager","Address":"Via Ludovico il Moro 22","City":"Bergamo","PostalCode":"24100","Country":"Italy","Phone":"035-640230","Fax":"035-640231"},{"CustomerID":"MAISD","CompanyName":"Maison Dewey","ContactName":"Catherine Dewey","ContactTitle":"Sales Agent","Address":"Rue Joseph-Bens 532","City":"Bruxelles","PostalCode":"B-1180","Country":"Belgium","Phone":"(02) 201 24 67","Fax":"(02) 201 24 68"},{"CustomerID":"MEREP","CompanyName":"Mère Paillarde","ContactName":"Jean Fresnière","ContactTitle":"Marketing Assistant","Address":"43 rue St. Laurent","City":"Montréal","PostalCode":"H1J 1C3","Country":"Canada","Phone":"(514) 555-8054","Fax":"(514) 555-8055"},{"CustomerID":"MORGK","CompanyName":"Morgenstern Gesundkost","ContactName":"Alexander Feuer","ContactTitle":"Marketing Assistant","Address":"Heerstr. 22","City":"Leipzig","PostalCode":"04179","Country":"Germany","Phone":"0342-023176","Fax":""},{"CustomerID":"NORTS","CompanyName":"North/South","ContactName":"Simon Crowther","ContactTitle":"Sales Associate","Address":"South House\n300 Queensbridge","City":"London","PostalCode":"SW7 1RZ","Country":"UK","Phone":"(171) 555-7733","Fax":"(171) 555-2530"},{"CustomerID":"OCEAN","CompanyName":"Océano Atlántico Ltda.","ContactName":"Yvonne Moncada","ContactTitle":"Sales Agent","Address":"Ing. Gustavo Moncada 8585\nPiso 20-A","City":"Buenos Aires","PostalCode":"1010","Country":"Argentina","Phone":"(1) 135-5333","Fax":"(1) 135-5535"},{"CustomerID":"OLDWO","CompanyName":"Old World Delicatessen","ContactName":"Rene Phillips","ContactTitle":"Sales Representative","Address":"2743 Bering St.","City":"Anchorage","PostalCode":"99508","Country":"USA","Phone":"(907) 555-7584","Fax":"(907) 555-2880"},{"CustomerID":"OTTIK","CompanyName":"Ottilies Käseladen","ContactName":"Henriette Pfalzheim","ContactTitle":"Owner","Address":"Mehrheimerstr. 369","City":"Köln","PostalCode":"50739","Country":"Germany","Phone":"0221-0644327","Fax":"0221-0765721"},{"CustomerID":"PARIS","CompanyName":"Paris spécialités","ContactName":"Marie Bertrand","ContactTitle":"Owner","Address":"265, boulevard Charonne","City":"Paris","PostalCode":"75012","Country":"France","Phone":"(1) 42.34.22.66","Fax":"(1) 42.34.22.77"},{"CustomerID":"PERIC","CompanyName":"Pericles Comidas clásicas","ContactName":"Guillermo Fernández","ContactTitle":"Sales Representative","Address":"Calle Dr. Jorge Cash 321","City":"México D.F.","PostalCode":"05033","Country":"Mexico","Phone":"(5) 552-3745","Fax":"(5) 545-3745"},{"CustomerID":"PICCO","CompanyName":"Piccolo und mehr","ContactName":"Georg Pipps","ContactTitle":"Sales Manager","Address":"Geislweg 14","City":"Salzburg","PostalCode":"5020","Country":"Austria","Phone":"6562-9722","Fax":"6562-9723"},{"CustomerID":"PRINI","CompanyName":"Princesa Isabel Vinhos","ContactName":"Isabel de Castro","ContactTitle":"Sales Representative","Address":"Estrada da saúde n. 58","City":"Lisboa","PostalCode":"1756","Country":"Portugal","Phone":"(1) 356-5634","Fax":""},{"CustomerID":"QUEDE","CompanyName":"Que Delícia","ContactName":"Bernardo Batista","ContactTitle":"Accounting Manager","Address":"Rua da Panificadora, 12","City":"Rio de Janeiro","PostalCode":"02389-673","Country":"Brazil","Phone":"(21) 555-4252","Fax":"(21) 555-4545"},{"CustomerID":"QUEEN","CompanyName":"Queen Cozinha","ContactName":"Lúcia Carvalho","ContactTitle":"Marketing Assistant","Address":"Alameda dos Canàrios, 891","City":"São Paulo","PostalCode":"05487-020","Country":"Brazil","Phone":"(11) 555-1189","Fax":""},{"CustomerID":"QUICK","CompanyName":"QUICK-Stop","ContactName":"Horst Kloss","ContactTitle":"Accounting Manager","Address":"Taucherstraße 10","City":"Cunewalde","PostalCode":"01307","Country":"Germany","Phone":"0372-035188","Fax":""},{"CustomerID":"RANCH","CompanyName":"Rancho grande","ContactName":"Sergio Gutiérrez","ContactTitle":"Sales Representative","Address":"Av. del Libertador 900","City":"Buenos Aires","PostalCode":"1010","Country":"Argentina","Phone":"(1) 123-5555","Fax":"(1) 123-5556"},{"CustomerID":"RATTC","CompanyName":"Rattlesnake Canyon Grocery","ContactName":"Paula Wilson","ContactTitle":"Assistant Sales Representative","Address":"2817 Milton Dr.","City":"Albuquerque","PostalCode":"87110","Country":"USA","Phone":"(505) 555-5939","Fax":"(505) 555-3620"},{"CustomerID":"REGGC","CompanyName":"Reggiani Caseifici","ContactName":"Maurizio Moroni","ContactTitle":"Sales Associate","Address":"Strada Provinciale 124","City":"Reggio Emilia","PostalCode":"42100","Country":"Italy","Phone":"0522-556721","Fax":"0522-556722"},{"CustomerID":"RICAR","CompanyName":"Ricardo Adocicados","ContactName":"Janete Limeira","ContactTitle":"Assistant Sales Agent","Address":"Av. Copacabana, 267","City":"Rio de Janeiro","PostalCode":"02389-890","Country":"Brazil","Phone":"(21) 555-3412","Fax":""},{"CustomerID":"RICSU","CompanyName":"Richter Supermarkt","ContactName":"Michael Holz","ContactTitle":"Sales Manager","Address":"Grenzacherweg 237","City":"Genève","PostalCode":"1203","Country":"Switzerland","Phone":"0897-034214","Fax":""},{"CustomerID":"ROMEY","CompanyName":"Romero y tomillo","ContactName":"Alejandra Camino","ContactTitle":"Accounting Manager","Address":"Gran Vía, 1","City":"Madrid","PostalCode":"28001","Country":"Spain","Phone":"(91) 745 6200","Fax":"(91) 745 6210"},{"CustomerID":"SANTG","CompanyName":"Santé Gourmet","ContactName":"Jonas Bergulfsen","ContactTitle":"Owner","Address":"Erling Skakkes gate 78","City":"Stavern","PostalCode":"4110","Country":"Norway","Phone":"07-98 92 35","Fax":"07-98 92 47"},{"CustomerID":"SAVEA","CompanyName":"Save-a-lot Markets","ContactName":"Jose Pavarotti","ContactTitle":"Sales Representative","Address":"187 Suffolk Ln.","City":"Boise","PostalCode":"83720","Country":"USA","Phone":"(208) 555-8097","Fax":""},{"CustomerID":"SEVES","CompanyName":"Seven Seas Imports","ContactName":"Hari Kumar","ContactTitle":"Sales Manager","Address":"90 Wadhurst Rd.","City":"London","PostalCode":"OX15 4NB","Country":"UK","Phone":"(171) 555-1717","Fax":"(171) 555-5646"},{"CustomerID":"SIMOB","CompanyName":"Simons bistro","ContactName":"Jytte Petersen","ContactTitle":"Owner","Address":"Vinbæltet 34","City":"København","PostalCode":"1734","Country":"Denmark","Phone":"31 12 34 56","Fax":"31 13 35 57"},{"CustomerID":"SPECD","CompanyName":"Spécialités du monde","ContactName":"Dominique Perrier","ContactTitle":"Marketing Manager","Address":"25, rue Lauriston","City":"Paris","PostalCode":"75016","Country":"France","Phone":"(1) 47.55.60.10","Fax":"(1) 47.55.60.20"},{"CustomerID":"SPLIR","CompanyName":"Split Rail Beer & Ale","ContactName":"Art Braunschweiger","ContactTitle":"Sales Manager","Address":"P.O. Box 555","City":"Lander","PostalCode":"82520","Country":"USA","Phone":"(307) 555-4680","Fax":"(307) 555-6525"},{"CustomerID":"SUPRD","CompanyName":"Suprêmes délices","ContactName":"Pascale Cartrain","ContactTitle":"Accounting Manager","Address":"Boulevard Tirou, 255","City":"Charleroi","PostalCode":"B-6000","Country":"Belgium","Phone":"(071) 23 67 22 20","Fax":"(071) 23 67 22 21"},{"CustomerID":"THEBI","CompanyName":"The Big Cheese","ContactName":"Liz Nixon","ContactTitle":"Marketing Manager","Address":"89 Jefferson Way\nSuite 2","City":"Portland","PostalCode":"97201","Country":"USA","Phone":"(503) 555-3612","Fax":""},{"CustomerID":"THECR","CompanyName":"The Cracker Box","ContactName":"Liu Wong","ContactTitle":"Marketing Assistant","Address":"55 Grizzly Peak Rd.","City":"Butte","PostalCode":"59801","Country":"USA","Phone":"(406) 555-5834","Fax":"(406) 555-8083"},{"CustomerID":"TOMSP","CompanyName":"Toms Spezialitäten","ContactName":"Karin Josephs","ContactTitle":"Marketing Manager","Address":"Luisenstr. 48","City":"Münster","PostalCode":"44087","Country":"Germany","Phone":"0251-031259","Fax":"0251-035695"},{"CustomerID":"TORTU","CompanyName":"Tortuga Restaurante","ContactName":"Miguel Angel Paolino","ContactTitle":"Owner","Address":"Avda. Azteca 123","City":"México D.F.","PostalCode":"05033","Country":"Mexico","Phone":"(5) 555-2933","Fax":""},{"CustomerID":"TRADH","CompanyName":"Tradição Hipermercados","ContactName":"Anabela Domingues","ContactTitle":"Sales Representative","Address":"Av. Inês de Castro, 414","City":"São Paulo","PostalCode":"05634-030","Country":"Brazil","Phone":"(11) 555-2167","Fax":"(11) 555-2168"},{"CustomerID":"TRAIH","CompanyName":"Trail's Head Gourmet Provisioners","ContactName":"Helvetius Nagy","ContactTitle":"Sales Associate","Address":"722 DaVinci Blvd.","City":"Kirkland","PostalCode":"98034","Country":"USA","Phone":"(206) 555-8257","Fax":"(206) 555-2174"},{"CustomerID":"VAFFE","CompanyName":"Vaffeljernet","ContactName":"Palle Ibsen","ContactTitle":"Sales Manager","Address":"Smagsløget 45","City":"Århus","PostalCode":"8200","Country":"Denmark","Phone":"86 21 32 43","Fax":"86 22 33 44"},{"CustomerID":"VICTE","CompanyName":"Victuailles en stock","ContactName":"Mary Saveley","ContactTitle":"Sales Agent","Address":"2, rue du Commerce","City":"Lyon","PostalCode":"69004","Country":"France","Phone":"78.32.54.86","Fax":"78.32.54.87"},{"CustomerID":"VINET","CompanyName":"Vins et alcools Chevalier","ContactName":"Paul Henriot","ContactTitle":"Accounting Manager","Address":"59 rue de l'Abbaye","City":"Reims","PostalCode":"51100","Country":"France","Phone":"26.47.15.10","Fax":"26.47.15.11"},{"CustomerID":"WANDK","CompanyName":"Die Wandernde Kuh","ContactName":"Rita Müller","ContactTitle":"Sales Representative","Address":"Adenauerallee 900","City":"Stuttgart","PostalCode":"70563","Country":"Germany","Phone":"0711-020361","Fax":"0711-035428"},{"CustomerID":"WARTH","CompanyName":"Wartian Herkku","ContactName":"Pirkko Koskitalo","ContactTitle":"Accounting Manager","Address":"Torikatu 38","City":"Oulu","PostalCode":"90110","Country":"Finland","Phone":"981-443655","Fax":"981-443655"},{"CustomerID":"WELLI","CompanyName":"Wellington Importadora","ContactName":"Paula Parente","ContactTitle":"Sales Manager","Address":"Rua do Mercado, 12","City":"Resende","PostalCode":"08737-363","Country":"Brazil","Phone":"(14) 555-8122","Fax":""},{"CustomerID":"WHITC","CompanyName":"White Clover Markets","ContactName":"Karl Jablonski","ContactTitle":"Owner","Address":"305 - 14th Ave. S.\nSuite 3B","City":"Seattle","PostalCode":"98128","Country":"USA","Phone":"(206) 555-4112","Fax":"(206) 555-4115"},{"CustomerID":"WILMK","CompanyName":"Wilman Kala","ContactName":"Matti Karttunen","ContactTitle":"Owner/Marketing Assistant","Address":"Keskuskatu 45","City":"Helsinki","PostalCode":"21240","Country":"Finland","Phone":"90-224 8858","Fax":"90-224 8858"},{"CustomerID":"WOLZA","CompanyName":"Wolski Zajazd","ContactName":"Zbyszek Piestrzeniewicz","ContactTitle":"Owner","Address":"ul. Filtrowa 68","City":"Warszawa","PostalCode":"01-012","Country":"Poland","Phone":"(26) 642-7012","Fax":"(26) 642-7012"}] -------------------------------------------------------------------------------- /src/Northwind.Data/DataHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Resources; 5 | using System.Text; 6 | 7 | namespace Northwind.Data 8 | { 9 | public class DataHelper 10 | { 11 | public DataHelper() 12 | { 13 | Assembly assembly = typeof(DataHelper).Assembly; 14 | 15 | using (System.IO.StreamReader reader = 16 | new System.IO.StreamReader(assembly.GetManifestResourceStream("Northwind.Data.Employees.txt"))) 17 | { 18 | Employees = Newtonsoft.Json.JsonConvert.DeserializeObject>(reader.ReadToEnd()); 19 | } 20 | 21 | 22 | using (System.IO.StreamReader reader = 23 | new System.IO.StreamReader(assembly.GetManifestResourceStream("Northwind.Data.Customers.txt"))) 24 | { 25 | Customers = Newtonsoft.Json.JsonConvert.DeserializeObject>(reader.ReadToEnd()); 26 | } 27 | 28 | 29 | using (System.IO.StreamReader reader = 30 | new System.IO.StreamReader(assembly.GetManifestResourceStream("Northwind.Data.Orders.txt"))) 31 | { 32 | Orders = Newtonsoft.Json.JsonConvert.DeserializeObject>(reader.ReadToEnd()); 33 | } 34 | 35 | using (System.IO.StreamReader reader = 36 | new System.IO.StreamReader(assembly.GetManifestResourceStream("Northwind.Data.OrderBase.txt"))) 37 | { 38 | OrderBases = Newtonsoft.Json.JsonConvert.DeserializeObject>(reader.ReadToEnd()); 39 | } 40 | 41 | } 42 | 43 | public List OrderBases; 44 | 45 | public List Employees; 46 | 47 | public List Customers; 48 | 49 | public List Orders; 50 | 51 | private static DataHelper mDefault; 52 | 53 | public static DataHelper Defalut 54 | { 55 | get 56 | { 57 | if (mDefault == null) 58 | mDefault = new DataHelper(); 59 | return mDefault; 60 | } 61 | } 62 | 63 | public Employee GetEmployee(int id) 64 | { 65 | return Employees[id]; 66 | } 67 | 68 | public Order GetOrder(int id) 69 | { 70 | return Orders[id]; 71 | } 72 | 73 | public OrderBase GetOrderBase(int id) 74 | { 75 | return OrderBases[id]; 76 | } 77 | 78 | public Customer GetCustomer(int id) 79 | { 80 | return Customers[id]; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Northwind.Data/Employee.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using MessagePack; 7 | using ProtoBuf; 8 | namespace Northwind.Data 9 | { 10 | [MessagePackObject] 11 | [ProtoContract] 12 | public class Employee 13 | { 14 | [ProtoMember(1)] 15 | [Key(0)] 16 | public int EmployeeID 17 | { 18 | get; 19 | set; 20 | } 21 | [ProtoMember(2)] 22 | [Key(1)] 23 | public string LastName 24 | { 25 | get; 26 | set; 27 | } 28 | [ProtoMember(3)] 29 | [Key(2)] 30 | public string FirstName 31 | { 32 | get; 33 | set; 34 | } 35 | [ProtoMember(4)] 36 | [Key(3)] 37 | public string Title 38 | { 39 | get; 40 | set; 41 | } 42 | [ProtoMember(5)] 43 | [Key(4)] 44 | public string TitleOfCourtesy { get; set; } 45 | [ProtoMember(6)] 46 | [Key(5)] 47 | public DateTime BirthDate { get; set; } 48 | [ProtoMember(7)] 49 | [Key(6)] 50 | public DateTime HireDate { get; set; } 51 | [ProtoMember(8)] 52 | [Key(7)] 53 | public string Address { get; set; } 54 | [ProtoMember(9)] 55 | [Key(8)] 56 | public string City { get; set; } 57 | [ProtoMember(10)] 58 | [Key(9)] 59 | public string Region { get; set; } 60 | [ProtoMember(11)] 61 | [Key(10)] 62 | public string PostalCode { get; set; } 63 | [ProtoMember(12)] 64 | [Key(11)] 65 | public string Country { get; set; } 66 | [ProtoMember(13)] 67 | [Key(12)] 68 | public string HomePhone { get; set; } 69 | [ProtoMember(14)] 70 | [Key(13)] 71 | public string Extension { get; set; } 72 | [ProtoMember(15)] 73 | [Key(14)] 74 | public string Photo { get; set; } 75 | [ProtoMember(16)] 76 | [Key(15)] 77 | public string Notes { get; set; } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Northwind.Data/Employees.txt: -------------------------------------------------------------------------------- 1 | [{"EmployeeID":1,"LastName":"Davolio","FirstName":"Nancy","Title":"Sales Representative","TitleOfCourtesy":"Ms.","BirthDate":"1968-12-08T00:00:00","HireDate":"1992-05-01T00:00:00","Address":"507 - 20th Ave. E.\nApt. 2A","City":"Seattle","Region":"WA","PostalCode":"98122","Country":"USA","HomePhone":"(206) 555-9857","Extension":"5467","Photo":null,"Notes":"Education includes a BA in psychology from Colorado State University. She also completed \"The Art of the Cold Call.\" Nancy is a member of Toastmasters International."},{"EmployeeID":2,"LastName":"Fuller","FirstName":"Andrew","Title":"Vice President, Sales","TitleOfCourtesy":"Dr.","BirthDate":"1952-02-19T00:00:00","HireDate":"1992-08-14T00:00:00","Address":"908 W. Capital Way","City":"Tacoma","Region":"WA","PostalCode":"98401","Country":"USA","HomePhone":"(206) 555-9482","Extension":"3457","Photo":null,"Notes":"Andrew received his BTS commercial and a Ph.D. in international marketing from the University of Dallas. He is fluent in French and Italian and reads German. He joined the company as a sales representative, was promoted to sales manager and was then named vice president of sales. Andrew is a member of the Sales Management Roundtable, the Seattle Chamber of Commerce, and the Pacific Rim Importers Association."},{"EmployeeID":3,"LastName":"Leverling","FirstName":"Janet","Title":"Sales Representative","TitleOfCourtesy":"Ms.","BirthDate":"1963-08-30T00:00:00","HireDate":"1992-04-01T00:00:00","Address":"722 Moss Bay Blvd.","City":"Kirkland","Region":"WA","PostalCode":"98033","Country":"USA","HomePhone":"(206) 555-3412","Extension":"3355","Photo":null,"Notes":"test"},{"EmployeeID":4,"LastName":"Peacock","FirstName":"Margaret","Title":"Sales Representative","TitleOfCourtesy":"Mrs.","BirthDate":"1958-09-19T00:00:00","HireDate":"1993-05-03T00:00:00","Address":"4110 Old Redmond Rd.","City":"Redmond","Region":"WA","PostalCode":"98052","Country":"USA","HomePhone":"(206) 555-8122","Extension":"5176","Photo":null,"Notes":"Margaret holds a BA in English literature from Concordia College and an MA from the American Institute of Culinary Arts. She was temporarily assigned to the London office before returning to her permanent post in Seattle."},{"EmployeeID":5,"LastName":"Buchanan","FirstName":"Steven","Title":"Sales Manager","TitleOfCourtesy":"Mr.","BirthDate":"1955-03-04T00:00:00","HireDate":"1993-10-17T00:00:00","Address":"14 Garrett Hill","City":"London","Region":"","PostalCode":"SW1 8JR","Country":"UK","HomePhone":"(71) 555-4848","Extension":"3453","Photo":null,"Notes":"Steven Buchanan graduated from St. Andrews University, Scotland, with a BSC degree. Upon joining the company as a sales representative, he spent 6 months in an orientation program at the Seattle office and then returned to his permanent post in London, where he was promoted to sales manager. Mr. Buchanan has completed the courses \"Successful Telemarketing\" and \"International Sales Management.\" He is fluent in French."},{"EmployeeID":6,"LastName":"Suyama","FirstName":"Michael","Title":"Sales Representative","TitleOfCourtesy":"Mr.","BirthDate":"1963-07-02T00:00:00","HireDate":"1993-10-17T00:00:00","Address":"Coventry House\nMiner Rd.","City":"London","Region":"","PostalCode":"EC2 7JR","Country":"UK","HomePhone":"(71) 555-7773","Extension":"428","Photo":null,"Notes":"Michael is a graduate of Sussex University (MA, economics) and the University of California at Los Angeles (MBA, marketing). He has also taken the courses \"Multi-Cultural Selling\" and \"Time Management for the Sales Professional.\" He is fluent in Japanese and can read and write French, Portuguese, and Spanish."},{"EmployeeID":7,"LastName":"King","FirstName":"Robert","Title":"Sales Representative","TitleOfCourtesy":"Mr.","BirthDate":"1960-05-29T00:00:00","HireDate":"1994-01-02T00:00:00","Address":"Edgeham Hollow\nWinchester Way","City":"London","Region":"","PostalCode":"RG1 9SP","Country":"UK","HomePhone":"(71) 555-5598","Extension":"465","Photo":null,"Notes":"Robert King served in the Peace Corps and traveled extensively before completing his degree in English at the University of Michigan and then joining the company. After completing a course entitled \"Selling in Europe,\" he was transferred to the London office."},{"EmployeeID":8,"LastName":"Callahan","FirstName":"Laura","Title":"Inside Sales Coordinator","TitleOfCourtesy":"Ms.","BirthDate":"1958-01-09T00:00:00","HireDate":"1994-03-05T00:00:00","Address":"4726 - 11th Ave. N.E.","City":"Seattle","Region":"WA","PostalCode":"98105","Country":"USA","HomePhone":"(206) 555-1189","Extension":"2344","Photo":null,"Notes":"Laura received a BA in psychology from the University of Washington. She has also completed a course in business French. She reads and writes French."},{"EmployeeID":9,"LastName":"Dodsworth","FirstName":"Anne","Title":"Sales Representative","TitleOfCourtesy":"Ms.","BirthDate":"1969-07-02T00:00:00","HireDate":"1994-11-15T00:00:00","Address":"7 Houndstooth Rd.","City":"London","Region":"","PostalCode":"WG2 7LT","Country":"UK","HomePhone":"(71) 555-4444","Extension":"452","Photo":null,"Notes":"Anne has a BA degree in English from St. Lawrence College. She is fluent in French and German."}] -------------------------------------------------------------------------------- /src/Northwind.Data/Northwind.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 1.2.3 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Northwind.Data/Order.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using MessagePack; 7 | using ProtoBuf; 8 | 9 | namespace Northwind.Data 10 | { 11 | [MessagePackObject] 12 | [ProtoContract] 13 | public class Order 14 | { 15 | [ProtoMember(1)] 16 | [Key(0)] 17 | public int OrderID { get; set; } 18 | [ProtoMember(2)] 19 | [Key(1)] 20 | public string CustomerID { get; set; } 21 | [ProtoMember(3)] 22 | [Key(2)] 23 | public int EmployeeID { get; set; } 24 | [ProtoMember(4)] 25 | [Key(3)] 26 | public DateTime OrderDate { get; set; } 27 | [ProtoMember(5)] 28 | [Key(4)] 29 | public DateTime RequiredDate { get; set; } 30 | [ProtoMember(6)] 31 | [Key(5)] 32 | public DateTime ShippedDate { get; set; } 33 | [ProtoMember(7)] 34 | [Key(6)] 35 | public int ShipVia { get; set; } 36 | [ProtoMember(8)] 37 | [Key(7)] 38 | public double Freight { get; set; } 39 | [ProtoMember(9)] 40 | [Key(8)] 41 | public string ShipName { get; set; } 42 | [ProtoMember(10)] 43 | [Key(9)] 44 | public string ShipAddress { get; set; } 45 | [ProtoMember(11)] 46 | [Key(10)] 47 | public string ShipCity { get; set; } 48 | [ProtoMember(12)] 49 | [Key(11)] 50 | public string ShipPostalCode { get; set; } 51 | [ProtoMember(13)] 52 | [Key(12)] 53 | public string ShipCountry { get; set; } 54 | } 55 | [MessagePackObject] 56 | [ProtoContract] 57 | public class OrderBase 58 | { 59 | [ProtoMember(1)] 60 | [Key(0)] 61 | public int OrderID { get; set; } 62 | [ProtoMember(2)] 63 | [Key(1)] 64 | public int EmployeeID { get; set; } 65 | [ProtoMember(3)] 66 | [Key(2)] 67 | public DateTime OrderDate { get; set; } 68 | [ProtoMember(4)] 69 | [Key(3)] 70 | public DateTime RequiredDate { get; set; } 71 | [ProtoMember(5)] 72 | [Key(4)] 73 | public DateTime ShippedDate { get; set; } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/PostAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | [AttributeUsage(AttributeTargets.Method)] 8 | public class PostAttribute : Attribute 9 | { 10 | public string Route { get; set; } 11 | } 12 | 13 | [AttributeUsage(AttributeTargets.Method)] 14 | public class GetAttribute : Attribute 15 | { 16 | public string Route { get; set; } 17 | } 18 | 19 | [AttributeUsage(AttributeTargets.Method)] 20 | public class DelAttribute : Attribute 21 | { 22 | public string Route { get; set; } 23 | } 24 | 25 | [AttributeUsage(AttributeTargets.Method)] 26 | public class PutAttribute : Attribute 27 | { 28 | public string Route { get; set; } 29 | } 30 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Parameter, AllowMultiple = true)] 31 | public class HeaderAttribute : Attribute 32 | { 33 | public HeaderAttribute() 34 | { 35 | 36 | } 37 | public HeaderAttribute(string name) 38 | { 39 | Name = name; 40 | } 41 | public HeaderAttribute(string name, string value) 42 | { 43 | Name = name; 44 | Value = value; 45 | } 46 | public string Name { get; set; } 47 | 48 | public string Value { get; set; } 49 | } 50 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Interface, AllowMultiple = true)] 51 | public class QueryAttribute : Attribute 52 | { 53 | public QueryAttribute() 54 | { 55 | 56 | } 57 | 58 | public QueryAttribute(string name) 59 | { 60 | Name = name; 61 | } 62 | public QueryAttribute(string name, string value) 63 | { 64 | Name = name; 65 | Value = value; 66 | } 67 | 68 | public string Name { get; set; } 69 | 70 | public string Value { get; set; } 71 | } 72 | 73 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 74 | public class RequestMaxRPS : Attribute 75 | { 76 | public RequestMaxRPS(int value) 77 | { 78 | Value = value; 79 | } 80 | 81 | public int Value { get; set; } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Request.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using BeetleX.Clients; 3 | using BeetleX.Tasks; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Diagnostics; 10 | #if NETCOREAPP2_1 11 | using BeetleX.Tracks; 12 | #endif 13 | namespace BeetleX.Http.Clients 14 | { 15 | public enum LoadedState : int 16 | { 17 | None = 1, 18 | Method = 2, 19 | Header = 4, 20 | Completed = 8 21 | } 22 | 23 | #if NETCOREAPP2_1 24 | public class Request : IApiObject 25 | #else 26 | public class Request 27 | #endif 28 | { 29 | #if NETCOREAPP2_1 30 | 31 | private CodeTrack mRequestTrack; 32 | 33 | public string Name => this.Url; 34 | 35 | public string[] Group 36 | { 37 | get 38 | { 39 | return new[] { "HTTPClient", "Request", $"{HttpHost.Host}" }; 40 | } 41 | } 42 | #endif 43 | 44 | public const string CODE_TREAK_PARENTID = "parent-id"; 45 | 46 | public const string POST = "POST"; 47 | 48 | public const string GET = "GET"; 49 | 50 | public const string DELETE = "DELETE"; 51 | 52 | public const string PUT = "PUT"; 53 | 54 | public Request() 55 | { 56 | Method = GET; 57 | this.HttpProtocol = "HTTP/1.1"; 58 | } 59 | 60 | public string Boundary { get; set; } 61 | 62 | public IBodyFormater Formater { get; set; } = new JsonFormater(); 63 | 64 | public Dictionary QuestryString { get; set; } 65 | 66 | public Header Header { get; set; } 67 | 68 | public string Url { get; set; } 69 | 70 | public string Method { get; set; } 71 | 72 | public string HttpProtocol { get; set; } 73 | 74 | public Response Response { get; set; } 75 | 76 | public Object Body { get; set; } 77 | 78 | public Type BodyType { get; set; } 79 | 80 | public int? TimeOut { get; set; } 81 | 82 | public RequestStatus Status { get; set; } 83 | 84 | public IClient Client { get; set; } 85 | 86 | internal void Execute(PipeStream stream) 87 | { 88 | try 89 | { 90 | var buffer = HttpParse.GetByteBuffer(); 91 | int offset = 0; 92 | offset += Encoding.ASCII.GetBytes(Method, 0, Method.Length, buffer, offset); 93 | buffer[offset] = HeaderTypeFactory._SPACE_BYTE; 94 | offset++; 95 | offset += Encoding.ASCII.GetBytes(Url, 0, Url.Length, buffer, offset); 96 | if (QuestryString != null && QuestryString.Count > 0) 97 | { 98 | int i = 0; 99 | foreach (var item in this.QuestryString) 100 | { 101 | string key = item.Key; 102 | string value = item.Value; 103 | if (string.IsNullOrEmpty(value)) 104 | continue; 105 | value = System.Net.WebUtility.UrlEncode(value); 106 | if (i == 0) 107 | { 108 | buffer[offset] = HeaderTypeFactory._QMARK; 109 | offset++; 110 | } 111 | else 112 | { 113 | buffer[offset] = HeaderTypeFactory._AND; 114 | offset++; 115 | } 116 | offset += Encoding.ASCII.GetBytes(key, 0, key.Length, buffer, offset); 117 | buffer[offset] = HeaderTypeFactory._EQ; 118 | offset++; 119 | offset += Encoding.ASCII.GetBytes(value, 0, value.Length, buffer, offset); 120 | i++; 121 | } 122 | } 123 | buffer[offset] = HeaderTypeFactory._SPACE_BYTE; 124 | offset++; 125 | offset += Encoding.ASCII.GetBytes(HttpProtocol, 0, HttpProtocol.Length, buffer, offset); 126 | buffer[offset] = HeaderTypeFactory._LINE_R; 127 | offset++; 128 | buffer[offset] = HeaderTypeFactory._LINE_N; 129 | offset++; 130 | stream.Write(buffer, 0, offset); 131 | 132 | Formater?.Setting(this); 133 | if (Header != null) 134 | Header.Write(stream); 135 | 136 | if (Method == POST || Method == PUT) 137 | { 138 | if (Body != null) 139 | { 140 | stream.Write(HeaderTypeFactory.CONTENT_LENGTH_BYTES, 0, 16); 141 | MemoryBlockCollection contentLength = stream.Allocate(10); 142 | stream.Write(HeaderTypeFactory.TOW_LINE_BYTES, 0, 4); 143 | int len = stream.CacheLength; 144 | Formater.Serialization(this, Body, stream); 145 | int count = stream.CacheLength - len; 146 | contentLength.Full(count.ToString().PadRight(10), stream.Encoding); 147 | } 148 | else 149 | { 150 | stream.Write(HeaderTypeFactory.NULL_CONTENT_LENGTH_BYTES, 0, HeaderTypeFactory.NULL_CONTENT_LENGTH_BYTES.Length); 151 | stream.Write(HeaderTypeFactory.LINE_BYTES, 0, 2); 152 | } 153 | } 154 | else 155 | { 156 | stream.Write(HeaderTypeFactory.LINE_BYTES, 0, 2); 157 | } 158 | } 159 | catch (Exception e) 160 | { 161 | Client?.DisConnect(); 162 | throw new HttpClientException($"http client write data error {e.Message}", e); 163 | } 164 | } 165 | 166 | public HttpHost HttpHost { get; set; } 167 | 168 | public Action GetConnection { get; set; } 169 | 170 | public static event Action Executing; 171 | 172 | public async Task Execute() 173 | { 174 | #if NETCOREAPP2_1 175 | using (mRequestTrack = CodeTrackFactory.TrackReport(this, CodeTrackLevel.Module, null)) 176 | { 177 | if (Activity.Current != null) 178 | Header[CODE_TREAK_PARENTID] = Activity.Current.Id; 179 | if (mRequestTrack.Enabled) 180 | mRequestTrack.Activity?.AddTag("tag", "BeetleX HttpClient"); 181 | #endif 182 | Executing?.Invoke(this); 183 | mTaskCompletionSource = CompletionSourceFactory.Create(TimeOut != null ? TimeOut.Value : HttpHost.Pool.TimeOut); // new AnyCompletionSource(); 184 | mTaskCompletionSource.TimeOut = OnRequestTimeout; 185 | OnExecute(); 186 | return await (Task)mTaskCompletionSource.GetTask(); 187 | #if NETCOREAPP2_1 188 | } 189 | #endif 190 | } 191 | 192 | private IAnyCompletionSource mTaskCompletionSource; 193 | 194 | private void OnRequestTimeout(IAnyCompletionSource source) 195 | { 196 | if (requestResult != null) 197 | { 198 | requestResult.TrySetException(new TimeoutException($"request imeout!")); 199 | } 200 | else 201 | { 202 | Response response = new Response(); 203 | response.Code = "408"; 204 | response.CodeMsg = "Request timeout"; 205 | response.Exception = new HttpClientException(this, HttpHost.Uri, "Request timeout"); 206 | source?.Success(response); 207 | } 208 | } 209 | 210 | private void onEventClientError(IClient c, ClientErrorArgs e) 211 | { 212 | requestResult.TrySetException(e.Error); 213 | } 214 | 215 | private void OnEventClientPacketCompleted(IClient client, object message) 216 | { 217 | requestResult.TrySetResult(message); 218 | } 219 | 220 | private TaskCompletionSource requestResult; 221 | 222 | private async Task DisConnect(IClient client) 223 | { 224 | client.DisConnect(); 225 | await Task.Delay(1000); 226 | } 227 | 228 | private async void OnExecute() 229 | { 230 | HttpClientHandler client = null; 231 | Response response; 232 | bool closeClient = false; 233 | try 234 | { 235 | object result = null; 236 | requestResult = new TaskCompletionSource(); 237 | client = await HttpHost.Pool.Pop(); 238 | Client = client.Client; 239 | AsyncTcpClient asyncClient = (AsyncTcpClient)client.Client; 240 | asyncClient.ClientError = onEventClientError; 241 | asyncClient.PacketReceive = OnEventClientPacketCompleted; 242 | GetConnection?.Invoke(asyncClient); 243 | #if NETCOREAPP2_1 244 | using (CodeTrackFactory.Track(Url, CodeTrackLevel.Function, mRequestTrack?.Activity?.Id, "HTTPClient", "Protocol", "Write")) 245 | { 246 | asyncClient.Send(this); 247 | Status = RequestStatus.SendCompleted; 248 | } 249 | #else 250 | asyncClient.Send(this); 251 | Status = RequestStatus.SendCompleted; 252 | #endif 253 | 254 | #if NETCOREAPP2_1 255 | using (CodeTrackFactory.Track(Url, CodeTrackLevel.Function, mRequestTrack?.Activity?.Id, "HTTPClient", "Protocol", "Read")) 256 | { 257 | var a = requestResult.Task; 258 | result = await a; 259 | } 260 | #else 261 | var a = requestResult.Task; 262 | result = await a; 263 | #endif 264 | if (result is Exception error) 265 | { 266 | response = new Response(); 267 | response.Exception = new HttpClientException(this, HttpHost.Uri, error.Message, error); 268 | Status = RequestStatus.Error; 269 | closeClient = true; 270 | } 271 | else 272 | { 273 | response = (Response)result; 274 | Status = RequestStatus.Received; 275 | } 276 | 277 | if (response.Exception == null) 278 | { 279 | int code = int.Parse(response.Code); 280 | if (response.Length > 0) 281 | { 282 | try 283 | { 284 | if (code >= 200 && code < 300) 285 | response.Body = this.Formater.Deserialization(response, response.Stream, this.BodyType, response.Length); 286 | else 287 | response.Body = response.Stream.ReadString(response.Length); 288 | } 289 | finally 290 | { 291 | response.Stream.ReadFree(response.Length); 292 | if (response.Chunked) 293 | response.Stream.Dispose(); 294 | response.Stream = null; 295 | } 296 | } 297 | if (!response.KeepAlive) 298 | client.Client.DisConnect(); 299 | if (code >= 400) 300 | { 301 | response.Exception = new HttpClientException(this, HttpHost.Uri, $"{Url}({response.Code}) [{response.Body}]"); 302 | response.Exception.Code = code; 303 | } 304 | Status = RequestStatus.Completed; 305 | } 306 | } 307 | catch (Exception e_) 308 | { 309 | HttpClientException clientException = new HttpClientException(this, HttpHost.Uri, e_.Message, e_); 310 | response = new Response { Exception = clientException }; 311 | Status = RequestStatus.Error; 312 | closeClient = true; 313 | } 314 | if (response.Exception != null) 315 | HttpHost.AddError(response.Exception.SocketError); 316 | else 317 | HttpHost.AddSuccess(); 318 | Response.Current = response; 319 | this.Response = response; 320 | if (client != null) 321 | { 322 | if (client.Client is AsyncTcpClient asclient) 323 | { 324 | asclient.ClientError = null; 325 | asclient.PacketReceive = null; 326 | } 327 | if (closeClient) 328 | await DisConnect(client.Client); 329 | HttpHost.Pool.Push(client); 330 | client = null; 331 | } 332 | await Task.Run(() => mTaskCompletionSource.Success(response)); 333 | } 334 | } 335 | 336 | public enum RequestStatus 337 | { 338 | None, 339 | SendCompleted, 340 | Received, 341 | Completed, 342 | Error 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Response.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace BeetleX.Http.Clients 9 | { 10 | public class Response 11 | { 12 | 13 | public Response() 14 | { 15 | Header = new Header(); 16 | Cookies = new Cookies(); 17 | mState = LoadedState.None; 18 | this.KeepAlive = true; 19 | } 20 | 21 | public HttpClientException Exception { get; set; } 22 | 23 | public Cookies Cookies { get; private set; } 24 | 25 | public string Code { get; set; } 26 | 27 | public string CodeMsg { get; set; } 28 | 29 | public bool KeepAlive { get; set; } 30 | 31 | public string HttpVersion { get; set; } 32 | 33 | public object Body { get; set; } 34 | 35 | public int Length { get; set; } 36 | 37 | public Header Header { get; private set; } 38 | 39 | public bool Chunked { get; set; } 40 | 41 | public bool Gzip { get; set; } 42 | 43 | internal PipeStream Stream { get; set; } 44 | 45 | private LoadedState mState; 46 | 47 | public LoadedState Load(PipeStream stream) 48 | { 49 | string line; 50 | if (mState == LoadedState.None) 51 | { 52 | if (stream.TryReadWith(HeaderTypeFactory.LINE_BYTES, out line)) 53 | { 54 | HttpParse.AnalyzeResponseLine(line.AsSpan(), this); 55 | mState = LoadedState.Method; 56 | } 57 | } 58 | if (mState == LoadedState.Method) 59 | { 60 | if (Header.Read(stream, Cookies)) 61 | { 62 | mState = LoadedState.Header; 63 | } 64 | } 65 | if (mState == LoadedState.Header) 66 | { 67 | if (string.Compare(Header[HeaderTypeFactory.CONNECTION], "close", true) == 0) 68 | { 69 | this.KeepAlive = false; 70 | } 71 | if (string.Compare(Header[HeaderTypeFactory.TRANSFER_ENCODING], "chunked", true) == 0) 72 | { 73 | Chunked = true; 74 | } 75 | else 76 | { 77 | string lenstr = Header[HeaderTypeFactory.CONTENT_LENGTH]; 78 | int length = 0; 79 | if (lenstr != null) 80 | int.TryParse(lenstr, out length); 81 | Length = length; 82 | } 83 | mState = LoadedState.Completed; 84 | } 85 | return mState; 86 | } 87 | 88 | private static AsyncLocal mCurrent = new AsyncLocal(); 89 | 90 | public T GetResult() 91 | { 92 | if (Exception != null) 93 | throw Exception; 94 | return (T)Body; 95 | } 96 | 97 | public static Response Current 98 | { 99 | get 100 | { 101 | return mCurrent.Value; 102 | } 103 | set 104 | { 105 | mCurrent.Value = value; 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/RouteAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace BeetleX.Http.Clients 7 | { 8 | [AttributeUsage(AttributeTargets.Method)] 9 | class RouteTemplateAttribute : Attribute 10 | { 11 | public RouteTemplateAttribute(string template) 12 | { 13 | Templete = template; 14 | } 15 | public string Templete { get; set; } 16 | 17 | public string Analysis(string parent) 18 | { 19 | if (string.IsNullOrEmpty(Templete)) 20 | return null; 21 | if (string.IsNullOrEmpty(parent)) 22 | parent = "/"; 23 | if (parent[parent.Length - 1] != '/') 24 | parent += "/"; 25 | if (!Regex.IsMatch(Templete, @"(\{[A-Za-z0-9]+\})")) 26 | { 27 | return null; 28 | 29 | } 30 | return parent + Templete; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RouteTemplateMatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | class RouteTemplateMatch 8 | { 9 | public RouteTemplateMatch(string value, int offset = 0) 10 | { 11 | mTemplate = value; 12 | mOffset = offset; 13 | Init(); 14 | } 15 | 16 | private int mOffset; 17 | 18 | private List mItems = new List(); 19 | 20 | public List Items => mItems; 21 | 22 | private string mTemplate; 23 | 24 | public string Template => mTemplate; 25 | 26 | private void Init() 27 | { 28 | MatchItem item = new MatchItem(); 29 | bool first = true; 30 | bool inmatch = false; 31 | int offset = 0; 32 | for (int i = mOffset; i < mTemplate.Length; i++) 33 | { 34 | if (mTemplate[i] == '{') 35 | { 36 | if (!first) 37 | item = new MatchItem(); 38 | inmatch = true; 39 | offset = i; 40 | } 41 | else if (mTemplate[i] == '}') 42 | { 43 | if (first) 44 | { 45 | first = false; 46 | } 47 | item.Name = mTemplate.Substring(offset + 1, i - offset - 1); 48 | mItems.Add(item); 49 | inmatch = false; 50 | } 51 | else 52 | { 53 | if (mItems.Count > 0 && !inmatch) 54 | item.Eof += mTemplate[i]; 55 | if (mItems.Count == 0 && !inmatch) 56 | item.Start += mTemplate[i]; 57 | } 58 | } 59 | } 60 | 61 | public class MatchItem 62 | { 63 | public string Start; 64 | 65 | public string Name; 66 | 67 | public string Eof; 68 | 69 | public int Match(string url, int offset, out string value) 70 | { 71 | int count = 0; 72 | value = ""; 73 | int length = url.Length; 74 | if (Start != null) 75 | { 76 | for (int k = 0; k < Start.Length; k++) 77 | { 78 | if (offset + k < length) 79 | { 80 | if (Start[k] != url[offset + k]) 81 | return -1; 82 | } 83 | else 84 | return -1; 85 | } 86 | offset = offset + Start.Length; 87 | count += Start.Length; 88 | } 89 | if (Eof != null) 90 | { 91 | for (int i = offset; i < length; i++) 92 | { 93 | if (Eof != null && url[i] == Eof[0]) 94 | { 95 | bool submatch = true; 96 | for (int k = 1; k < Eof.Length; k++) 97 | { 98 | if (url[i + k] != Eof[k]) 99 | { 100 | submatch = false; 101 | break; 102 | } 103 | } 104 | if (submatch) 105 | { 106 | value = url.Substring(offset, i - offset); 107 | count += Eof.Length; 108 | break; 109 | } 110 | } 111 | 112 | else 113 | { 114 | count++; 115 | } 116 | } 117 | } 118 | else 119 | { 120 | count = url.Length - offset; 121 | value = url.Substring(offset, count); 122 | } 123 | if (value == "") 124 | return -1; 125 | return count; 126 | } 127 | } 128 | 129 | public override string ToString() 130 | { 131 | StringBuilder sb = new StringBuilder(); 132 | foreach (var item in mItems) 133 | { 134 | sb.Append(item.Start).Append(item.Name).Append(item.Eof); 135 | } 136 | return sb.ToString(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/StringUrlRequestExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | public static class StringUrlRequestExtension 8 | { 9 | public static HttpClient GetRequest(this string url) 10 | where T : IBodyFormater, new() 11 | { 12 | return new HttpClient(url); 13 | } 14 | 15 | public static HttpBinaryClient BinaryRequest(this string url) 16 | { 17 | return new HttpBinaryClient(url); 18 | } 19 | public static HttpJsonClient JsonRequest(this string url) 20 | { 21 | return new HttpJsonClient(url); 22 | } 23 | 24 | public static HttpFormUrlClient FormUrlRequest(this string url) 25 | { 26 | return new HttpFormUrlClient(url); 27 | } 28 | 29 | public static HttpFormDataClient FormDataRequest(this string url) 30 | { 31 | return new HttpFormDataClient(url); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/UploadFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.Clients 6 | { 7 | public class UploadFile 8 | { 9 | public string Name { get; set; } 10 | 11 | public ArraySegment Data { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/WebSockets/DataFrame.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace BeetleX.Http.WebSockets 7 | { 8 | 9 | 10 | public enum DataPacketLoadStep 11 | { 12 | None, 13 | Header, 14 | Length, 15 | Mask, 16 | Completed 17 | } 18 | 19 | 20 | public class DataFrame 21 | { 22 | public DataFrame() 23 | { 24 | this.FIN = true; 25 | Type = DataPacketType.text; 26 | IsMask = false; 27 | } 28 | 29 | internal WSClient Client { get; set; } 30 | 31 | const int CHECK_B1 = 0x1; 32 | 33 | const int CHECK_B2 = 0x2; 34 | 35 | const int CHECK_B3 = 0x4; 36 | 37 | const int CHECK_B4 = 0x8; 38 | 39 | const int CHECK_B5 = 0x10; 40 | 41 | const int CHECK_B6 = 0x20; 42 | 43 | const int CHECK_B7 = 0x40; 44 | 45 | const int CHECK_B8 = 0x80; 46 | 47 | public bool FIN { get; set; } 48 | 49 | public bool RSV1 { get; set; } 50 | 51 | public bool IsBroadcast { get; set; } = false; 52 | 53 | public bool RSV2 { get; set; } 54 | 55 | public bool RSV3 { get; set; } 56 | 57 | public DataPacketType Type { get; set; } 58 | 59 | public ArraySegment? Body { get; set; } 60 | 61 | public bool IsMask { get; set; } 62 | 63 | internal byte PayloadLen { get; set; } 64 | 65 | public ulong Length { get; set; } 66 | 67 | public byte[] MaskKey { get; set; } 68 | 69 | private DataPacketLoadStep mLoadStep = DataPacketLoadStep.None; 70 | 71 | internal DataPacketLoadStep Read(PipeStream stream, WSClient client) 72 | { 73 | if (mLoadStep == DataPacketLoadStep.None) 74 | { 75 | if (stream.Length >= 2) 76 | { 77 | byte value = (byte)stream.ReadByte(); 78 | this.FIN = (value & CHECK_B8) > 0; 79 | this.RSV1 = (value & CHECK_B7) > 0; 80 | this.RSV2 = (value & CHECK_B6) > 0; 81 | this.RSV3 = (value & CHECK_B5) > 0; 82 | this.Type = (DataPacketType)(byte)(value & 0xF); 83 | value = (byte)stream.ReadByte(); 84 | this.IsMask = (value & CHECK_B8) > 0; 85 | this.PayloadLen = (byte)(value & 0x7F); 86 | mLoadStep = DataPacketLoadStep.Header; 87 | } 88 | } 89 | if (mLoadStep == DataPacketLoadStep.Header) 90 | { 91 | if (this.PayloadLen == 127) 92 | { 93 | if (stream.Length >= 8) 94 | { 95 | Length = stream.ReadUInt64(); 96 | mLoadStep = DataPacketLoadStep.Length; 97 | } 98 | } 99 | else if (this.PayloadLen == 126) 100 | { 101 | if (stream.Length >= 2) 102 | { 103 | Length = stream.ReadUInt16(); 104 | mLoadStep = DataPacketLoadStep.Length; 105 | } 106 | } 107 | else 108 | { 109 | this.Length = this.PayloadLen; 110 | mLoadStep = DataPacketLoadStep.Length; 111 | } 112 | } 113 | if (mLoadStep == DataPacketLoadStep.Length) 114 | { 115 | if (IsMask) 116 | { 117 | if (stream.Length >= 4) 118 | { 119 | this.MaskKey = new byte[4]; 120 | stream.Read(this.MaskKey, 0, 4); 121 | mLoadStep = DataPacketLoadStep.Mask; 122 | } 123 | } 124 | else 125 | { 126 | mLoadStep = DataPacketLoadStep.Mask; 127 | } 128 | } 129 | if (mLoadStep == DataPacketLoadStep.Mask) 130 | { 131 | if (this.Length > 0 && (ulong)stream.Length >= this.Length) 132 | { 133 | var len = (int)this.Length; 134 | if (this.IsMask) 135 | ReadMask(stream); 136 | // Body = this.DataPacketSerializer.FrameDeserialize(this, stream); 137 | var data = client.GetFrameDataBuffer(len); 138 | stream.Read(data, 0, len); 139 | Body = new ArraySegment(data, 0, len); 140 | mLoadStep = DataPacketLoadStep.Completed; 141 | } 142 | } 143 | return mLoadStep; 144 | } 145 | 146 | private void ReadMask(PipeStream stream) 147 | { 148 | IndexOfResult result = stream.IndexOf((int)this.Length); 149 | ulong index = 0; 150 | if (result.Start.ID == result.End.ID) 151 | { 152 | index = MarkBytes(result.Start.Data, result.StartPostion, result.EndPostion, index); 153 | } 154 | else 155 | { 156 | index = MarkBytes(result.Start.Data, result.StartPostion, result.Start.Length - 1, index); 157 | IMemoryBlock next = result.Start.NextMemory; 158 | while (next != null && index < this.Length) 159 | { 160 | if (next.ID == result.End.ID) 161 | { 162 | index = MarkBytes(next.Data, 0, result.EndPostion, index); 163 | break; 164 | } 165 | else 166 | { 167 | index = MarkBytes(next.Data, 0, next.Length - 1, index); 168 | } 169 | next = next.NextMemory; 170 | } 171 | } 172 | } 173 | 174 | private ulong MarkBytes(Span bytes, int start, int end, ulong index) 175 | { 176 | for (int i = start; i <= end; i++) 177 | { 178 | bytes[i] = (byte)(bytes[i] ^ MaskKey[index % 4]); 179 | index++; 180 | if (index >= this.Length) 181 | break; 182 | } 183 | return index; 184 | } 185 | 186 | internal void Write(PipeStream stream) 187 | { 188 | try 189 | { 190 | byte[] header = new byte[2]; 191 | if (FIN) 192 | header[0] |= CHECK_B8; 193 | if (RSV1) 194 | header[0] |= CHECK_B7; 195 | if (RSV2) 196 | header[0] |= CHECK_B6; 197 | if (RSV3) 198 | header[0] |= CHECK_B5; 199 | header[0] |= (byte)Type; 200 | if (Body != null) 201 | { 202 | ArraySegment data = Body.Value; 203 | try 204 | { 205 | if (MaskKey == null || MaskKey.Length != 4) 206 | this.IsMask = false; 207 | else 208 | this.IsMask = true; 209 | if (this.IsMask) 210 | { 211 | header[1] |= CHECK_B8; 212 | int offset = data.Offset; 213 | for (int i = offset; i < data.Count; i++) 214 | { 215 | data.Array[i] = (byte)(data.Array[i] ^ MaskKey[(i - offset) % 4]); 216 | } 217 | } 218 | int len = data.Count; 219 | if (len > 125 && len <= UInt16.MaxValue) 220 | { 221 | header[1] |= (byte)126; 222 | stream.Write(header, 0, 2); 223 | stream.Write((UInt16)len); 224 | } 225 | else if (len > UInt16.MaxValue) 226 | { 227 | header[1] |= (byte)127; 228 | stream.Write(header, 0, 2); 229 | stream.Write((ulong)len); 230 | } 231 | else 232 | { 233 | header[1] |= (byte)data.Count; 234 | stream.Write(header, 0, 2); 235 | } 236 | if (IsMask) 237 | stream.Write(MaskKey, 0, 4); 238 | stream.Write(data.Array, data.Offset, data.Count); 239 | } 240 | finally 241 | { 242 | Client?.FreeFrameDataBuffer(data.Array); 243 | 244 | } 245 | } 246 | else 247 | { 248 | stream.Write(header, 0, 2); 249 | } 250 | } 251 | finally 252 | { 253 | //this.DataPacketSerializer = null; 254 | //this.Body = null; 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/WebSockets/DataPackeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.WebSockets 6 | { 7 | public enum DataPacketType : byte 8 | { 9 | continuation = 0x0, 10 | text = 0x1, 11 | binary = 0x2, 12 | non_control3 = 0x3, 13 | non_control4 = 0x4, 14 | non_control5 = 0x5, 15 | non_control6 = 0x6, 16 | non_control7 = 0x7, 17 | connectionClose = 0x8, 18 | ping = 0x9, 19 | pong = 0xA, 20 | controlB = 0xB, 21 | controlE = 0xE, 22 | controlF = 0xF 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WebSockets/JsonClient.cs: -------------------------------------------------------------------------------- 1 | #if NETCOREAPP2_1 2 | using BeetleX.Tracks; 3 | #endif 4 | using Newtonsoft.Json.Linq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | namespace BeetleX.Http.WebSockets 10 | { 11 | public class JsonClient : WSClient 12 | { 13 | public JsonClient(string host) : base(host) { } 14 | 15 | public JsonClient(Uri host) : base(host) { } 16 | 17 | public virtual void Send(object data) 18 | { 19 | #if NETCOREAPP2_1 20 | using (CodeTrackFactory.Track("Send", CodeTrackLevel.Function, null, "Websocket", "JsonClient")) 21 | { 22 | #endif 23 | string text = Newtonsoft.Json.JsonConvert.SerializeObject(data); 24 | DataFrame df = new DataFrame(); 25 | var buffer = Encoding.UTF8.GetBytes(text); 26 | df.Body = new ArraySegment(buffer, 0, buffer.Length); 27 | base.SendFrame(df); 28 | #if NETCOREAPP2_1 29 | } 30 | #endif 31 | } 32 | 33 | 34 | public virtual async Task Receive() 35 | { 36 | #if NETCOREAPP2_1 37 | using (CodeTrackFactory.Track("Receive", CodeTrackLevel.Function, null, "Websocket", "JsonClient")) 38 | { 39 | #endif 40 | var data = await ReceiveFrame(); 41 | if (data.Type != DataPacketType.text) 42 | throw new BXException("Data type is not json text"); 43 | if (data.Body == null) 44 | return new JObject(); 45 | var body = data.Body.Value; 46 | string result = Encoding.UTF8.GetString(body.Array, body.Offset, body.Count); 47 | return (JToken)Newtonsoft.Json.JsonConvert.DeserializeObject(result); 48 | #if NETCOREAPP2_1 49 | } 50 | #endif 51 | } 52 | 53 | public async Task ReceiveFrom(object data) 54 | { 55 | #if NETCOREAPP2_1 56 | using (CodeTrackFactory.Track("Request", CodeTrackLevel.Function, null, "Websocket", "JsonClient")) 57 | { 58 | #endif 59 | Send(data); 60 | return await Receive(); 61 | #if NETCOREAPP2_1 62 | } 63 | #endif 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/WebSockets/Response.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Buffers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using BeetleX.Http.Clients; 6 | 7 | namespace BeetleX.Http.WebSockets 8 | { 9 | public class Response 10 | { 11 | 12 | public Dictionary Headers { get; private set; } = new Dictionary(); 13 | 14 | public int? Code { get; set; } 15 | 16 | public string Message { get; set; } 17 | 18 | public string HttpVersion { get; set; } 19 | 20 | public bool Read(PipeStream stream) 21 | { 22 | while(stream.TryReadLine(out string line)) 23 | { 24 | if (string.IsNullOrEmpty(line)) 25 | return true; 26 | if(Code==null) 27 | { 28 | var result = HttpParse.AnalyzeResponseLine(line.AsSpan()); 29 | Code = result.Item2; 30 | HttpVersion = result.Item1; 31 | Message = result.Item3; 32 | } 33 | else 34 | { 35 | var header = HttpParse.AnalyzeHeader(line.AsSpan()); 36 | Headers[header.Item1] = header.Item2; 37 | } 38 | } 39 | return false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WebSockets/TextClient.cs: -------------------------------------------------------------------------------- 1 | #if NETCOREAPP2_1 2 | using BeetleX.Tracks; 3 | #endif 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace BeetleX.Http.WebSockets 10 | { 11 | public class TextClient : WSClient 12 | { 13 | public TextClient(string host) : base(host) { } 14 | 15 | public TextClient(Uri host) : base(host) { } 16 | 17 | public virtual void Send(string text) 18 | { 19 | #if NETCOREAPP2_1 20 | using (CodeTrackFactory.Track("Send", CodeTrackLevel.Function, null, "Websocket", "TextClient")) 21 | { 22 | #endif 23 | DataFrame dataFrame = new DataFrame(); 24 | byte[] data = Encoding.UTF8.GetBytes(text); 25 | dataFrame.Body = new ArraySegment(data, 0, data.Length); 26 | SendFrame(dataFrame); 27 | #if NETCOREAPP2_1 28 | } 29 | #endif 30 | } 31 | 32 | public virtual async Task Receive() 33 | { 34 | #if NETCOREAPP2_1 35 | using (CodeTrackFactory.Track("Receive", CodeTrackLevel.Function, null, "Websocket", "TextClient")) 36 | { 37 | #endif 38 | var data = await ReceiveFrame(); 39 | if (data.Type != DataPacketType.text) 40 | throw new BXException("Data type is not text"); 41 | if (data.Body == null) 42 | return null; 43 | var body = data.Body.Value; 44 | string result = Encoding.UTF8.GetString(body.Array, body.Offset, body.Count); 45 | return result; 46 | #if NETCOREAPP2_1 47 | } 48 | #endif 49 | } 50 | 51 | public async Task ReceiveFrom(string text) 52 | { 53 | #if NETCOREAPP2_1 54 | using (CodeTrackFactory.Track("Request", CodeTrackLevel.Function, null, "Websocket", "TextClient")) 55 | { 56 | #endif 57 | Send(text); 58 | return await Receive(); 59 | #if NETCOREAPP2_1 60 | } 61 | #endif 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/WebSockets/WSClient.cs: -------------------------------------------------------------------------------- 1 | using BeetleX.Clients; 2 | using BeetleX.Tasks; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Security; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Net; 10 | #if NETCOREAPP2_1 11 | using BeetleX.Tracks; 12 | #endif 13 | 14 | namespace BeetleX.Http.WebSockets 15 | { 16 | public class WSClient : IDisposable 17 | { 18 | 19 | public WSClient(string host) : this(new Uri(host)) { } 20 | 21 | public WSClient(Uri host) 22 | { 23 | Host = host; 24 | byte[] key = new byte[16]; 25 | new Random().NextBytes(key); 26 | _SecWebSocketKey = Convert.ToBase64String(key); 27 | this.Origin = Host.OriginalString; 28 | this.SSLAuthenticateName = this.Origin; 29 | } 30 | 31 | public Response Response { get; internal set; } 32 | 33 | public byte[] MaskKey { get; set; } 34 | 35 | private string _SecWebSocketKey; 36 | 37 | private AsyncTcpClient mNetClient; 38 | 39 | private long mSends = 0; 40 | 41 | private long mReceives = 0; 42 | 43 | public int TimeOut { get; set; } = 10 * 1000; 44 | 45 | private Dictionary mProperties = new Dictionary(); 46 | 47 | private System.Collections.Concurrent.ConcurrentQueue mDataFrames = new System.Collections.Concurrent.ConcurrentQueue(); 48 | 49 | private bool OnWSConnected = false; 50 | 51 | private void OnPacketCompleted(IClient client, object message) 52 | { 53 | if (message is DataFrame dataFrame) 54 | { 55 | OnReceiveMessage(dataFrame); 56 | } 57 | else 58 | { 59 | OnConnectResponse(null, message as Response); 60 | } 61 | } 62 | 63 | 64 | public object Token { get; set; } 65 | 66 | public object this[string name] 67 | { 68 | get 69 | { 70 | mProperties.TryGetValue(name, out object result); 71 | return result; 72 | } 73 | set 74 | { 75 | mProperties[name] = value; 76 | } 77 | } 78 | 79 | 80 | public AsyncTcpClient Client => mNetClient; 81 | 82 | public DateTime PingPongTime { get; set; } 83 | 84 | public event System.EventHandler DataReceive; 85 | 86 | public void Ping() 87 | { 88 | DataFrame pong = new DataFrame(); 89 | pong.Type = DataPacketType.ping; 90 | SendFrame(pong); 91 | } 92 | 93 | public virtual byte[] GetFrameDataBuffer(int length) 94 | { 95 | return new byte[length]; 96 | } 97 | 98 | public virtual void FreeFrameDataBuffer(byte[] data) 99 | { 100 | 101 | } 102 | 103 | internal void FrameWrited(DataFrame frame) 104 | { 105 | OnDataFrameWrited(frame); 106 | } 107 | 108 | 109 | 110 | protected virtual void OnDataFrameWrited(DataFrame frame) 111 | { 112 | 113 | } 114 | 115 | protected virtual void OnDataReceive(WSReceiveArgs e) 116 | { 117 | try 118 | { 119 | DataReceive?.Invoke(this, e); 120 | } 121 | catch (Exception e_) 122 | { 123 | try 124 | { 125 | e.Error = new BXException($"ws client receive error {e_.Message}", e_); 126 | DataReceive?.Invoke(this, e); 127 | } 128 | catch { } 129 | } 130 | } 131 | 132 | protected virtual void OnReceiveMessage(DataFrame message) 133 | { 134 | System.Threading.Interlocked.Increment(ref mReceives); 135 | if (message.Type == DataPacketType.connectionClose) 136 | { 137 | Dispose(); 138 | OnClientError(mNetClient, new ClientErrorArgs { Error = new BXException("ws connection close!"), Message = "ws connection close" }); 139 | return; 140 | } 141 | if (message.Type == DataPacketType.ping || message.Type == DataPacketType.pong) 142 | { 143 | PingPongTime = DateTime.Now; 144 | if (message.Type == DataPacketType.ping) 145 | { 146 | DataFrame pong = new DataFrame(); 147 | pong.Type = DataPacketType.pong; 148 | SendFrame(pong); 149 | } 150 | return; 151 | } 152 | else 153 | { 154 | OnDataReceive(message); 155 | } 156 | 157 | } 158 | 159 | private object mLockReceive = new object(); 160 | 161 | protected virtual void OnDataReceive(DataFrame data) 162 | { 163 | if (DataReceive != null) 164 | { 165 | WSReceiveArgs e = new WSReceiveArgs(); 166 | e.Client = this; 167 | e.Frame = data; 168 | DataReceive(this, e); 169 | } 170 | else 171 | { 172 | lock (mLockReceive) 173 | { 174 | if (mReceiveCompletionSource != null) 175 | { 176 | var result = mReceiveCompletionSource; 177 | mReceiveCompletionSource = null; 178 | Task.Run(() => result.Success(data)); 179 | } 180 | else 181 | { 182 | mDataFrames.Enqueue(data); 183 | } 184 | } 185 | } 186 | } 187 | 188 | private IAnyCompletionSource mReceiveCompletionSource; 189 | 190 | private void OnReceiveTimeOut(IAnyCompletionSource source) 191 | { 192 | if (mReceiveCompletionSource != null) 193 | { 194 | var completed = mReceiveCompletionSource; 195 | mReceiveCompletionSource = null; 196 | Task.Run(() => 197 | { 198 | completed?.Error(new BXException("Websocket receive time out!")); 199 | }); 200 | return; 201 | } 202 | } 203 | 204 | public Task ReceiveFrame() 205 | { 206 | Connect(); 207 | lock (mLockReceive) 208 | { 209 | if (mDataFrames.TryDequeue(out object data)) 210 | { 211 | if (data is Exception error) 212 | throw error; 213 | return Task.FromResult((DataFrame)data); 214 | } 215 | else 216 | { 217 | mReceiveCompletionSource = CompletionSourceFactory.Create(TimeOut); 218 | mReceiveCompletionSource.TimeOut = OnReceiveTimeOut; 219 | return (Task)mReceiveCompletionSource.GetTask(); 220 | } 221 | } 222 | 223 | } 224 | private void OnClientError(IClient c, ClientErrorArgs e) 225 | { 226 | if (OnWSConnected) 227 | { 228 | if (e.Error is BXException) 229 | { 230 | OnWSConnected = false; 231 | } 232 | lock (mLockReceive) 233 | { 234 | if (mReceiveCompletionSource != null) 235 | { 236 | var completed = mReceiveCompletionSource; 237 | mReceiveCompletionSource = null; 238 | Task.Run(() => 239 | { 240 | completed.Error(e.Error); 241 | }); 242 | return; 243 | } 244 | } 245 | 246 | if (DataReceive != null) 247 | { 248 | try 249 | { 250 | WSReceiveArgs wse = new WSReceiveArgs(); 251 | wse.Client = this; 252 | wse.Error = e.Error; 253 | DataReceive?.Invoke(this, wse); 254 | } 255 | catch { } 256 | 257 | } 258 | } 259 | else 260 | { 261 | OnConnectResponse(e.Error, null); 262 | } 263 | 264 | } 265 | 266 | private TaskCompletionSource mWScompletionSource; 267 | 268 | 269 | 270 | private void OnWriteConnect() 271 | { 272 | var stream = mNetClient.Stream.ToPipeStream(); 273 | stream.WriteLine($"{Method} {Path} HTTP/1.1"); 274 | stream.WriteLine($"Host: {Host.Host}"); 275 | stream.WriteLine($"Upgrade: websocket"); 276 | stream.WriteLine($"Connection: Upgrade"); 277 | foreach (var item in Headers) 278 | { 279 | stream.WriteLine($"{item.Key}: {item.Value}"); 280 | } 281 | stream.WriteLine($"Origin: {Origin}"); 282 | stream.WriteLine($"Sec-WebSocket-Key: {_SecWebSocketKey}"); 283 | stream.WriteLine($"Sec-WebSocket-Version: {SecWebSocketVersion}"); 284 | stream.WriteLine(""); 285 | mNetClient.Stream.Flush(); 286 | } 287 | 288 | public EndPoint LocalEndPoint { get; set; } 289 | 290 | private object mLockConnect = new object(); 291 | 292 | public bool IsConnected => OnWSConnected && mNetClient != null && mNetClient.IsConnected; 293 | 294 | public void Connect() 295 | { 296 | if (IsConnected) 297 | { 298 | return; 299 | } 300 | lock (mLockConnect) 301 | { 302 | if (IsConnected) 303 | { 304 | return; 305 | } 306 | #if NETCOREAPP2_1 307 | using (CodeTrackFactory.Track($"Connect {Host}", CodeTrackLevel.Function, null, "Websocket", "Client")) 308 | { 309 | #endif 310 | mWScompletionSource = new TaskCompletionSource(); 311 | if (mNetClient == null) 312 | { 313 | string protocol = Host.Scheme.ToLower(); 314 | if (!(protocol == "ws" || protocol == "wss")) 315 | { 316 | OnConnectResponse(new BXException("protocol error! host must [ws|wss]//host:port"), null); 317 | mWScompletionSource.Task.Wait(); 318 | } 319 | WSPacket wSPacket = new WSPacket 320 | { 321 | WSClient = this 322 | }; 323 | if (Host.Scheme.ToLower() == "wss") 324 | { 325 | mNetClient = SocketFactory.CreateSslClient(wSPacket, Host.Host, Host.Port, SSLAuthenticateName); 326 | mNetClient.CertificateValidationCallback = CertificateValidationCallback; 327 | } 328 | else 329 | { 330 | mNetClient = SocketFactory.CreateClient(wSPacket, Host.Host, Host.Port); 331 | } 332 | mNetClient.LocalEndPoint = this.LocalEndPoint; 333 | mNetClient.LittleEndian = false; 334 | mNetClient.PacketReceive = OnPacketCompleted; 335 | mNetClient.ClientError = OnClientError; 336 | } 337 | mDataFrames = new System.Collections.Concurrent.ConcurrentQueue(); 338 | bool isNew; 339 | if (mNetClient.Connect(out isNew)) 340 | { 341 | OnWriteConnect(); 342 | } 343 | else 344 | { 345 | OnConnectResponse(mNetClient.LastError, null); 346 | } 347 | mWScompletionSource.Task.Wait(10000); 348 | if (!OnWSConnected) 349 | throw new TimeoutException($"Connect {Host} websocket server timeout!"); 350 | #if NETCOREAPP2_1 351 | } 352 | #endif 353 | } 354 | } 355 | 356 | public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } 357 | 358 | protected virtual void OnConnectResponse(Exception exception, Response response) 359 | { 360 | Response = response; 361 | Task.Run(() => 362 | { 363 | if (exception != null) 364 | { 365 | OnWSConnected = false; 366 | mWScompletionSource?.TrySetException(exception); 367 | } 368 | else 369 | { 370 | if (response.Code == 101) 371 | { 372 | OnWSConnected = true; 373 | mWScompletionSource?.TrySetResult(true); 374 | 375 | } 376 | else 377 | { 378 | OnWSConnected = false; 379 | mWScompletionSource?.TrySetException(new BXException($"ws connect error {response.Code} {response.Message}")); 380 | 381 | } 382 | } 383 | }); 384 | } 385 | 386 | public void SendFrame(DataFrame data) 387 | { 388 | Connect(); 389 | data.Client = this; 390 | data.MaskKey = this.MaskKey; 391 | mNetClient.Send(data); 392 | System.Threading.Interlocked.Increment(ref mSends); 393 | } 394 | 395 | public void Dispose() 396 | { 397 | OnWSConnected = false; 398 | mNetClient.DisConnect(); 399 | mNetClient = null; 400 | } 401 | 402 | public Dictionary Headers { get; private set; } = new Dictionary(); 403 | 404 | public string SecWebSocketProtocol { get; set; } = "websocket, beetlex"; 405 | 406 | public int SecWebSocketVersion { get; set; } = 13; 407 | 408 | public string Method { get; private set; } = "GET"; 409 | 410 | public string Path { get; set; } = "/"; 411 | 412 | public string SSLAuthenticateName { get; set; } 413 | 414 | public string Origin { get; set; } 415 | 416 | public Uri Host { get; private set; } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/WebSockets/WSPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using BeetleX.Clients; 6 | 7 | namespace BeetleX.Http.WebSockets 8 | { 9 | public class WSPacket : BeetleX.Clients.IClientPacket 10 | { 11 | public EventClientPacketCompleted Completed { get; set; } 12 | 13 | public IClientPacket Clone() 14 | { 15 | WSPacket result = new WSPacket(); 16 | result.WSClient = this.WSClient; 17 | return result; 18 | } 19 | 20 | public Response Response { get; set; } = new Response(); 21 | 22 | private bool OnWSConnected = false; 23 | 24 | private DataFrame mReceiveFrame; 25 | 26 | public WSClient WSClient { get; set; } 27 | 28 | public void Decode(IClient client, Stream stream) 29 | { 30 | try 31 | { 32 | if (!OnWSConnected) 33 | { 34 | if (Response.Read(stream.ToPipeStream())) 35 | { 36 | Completed?.Invoke(client, Response); 37 | OnWSConnected = true; 38 | } 39 | } 40 | else 41 | { 42 | var pipestream = stream.ToPipeStream(); 43 | while (pipestream.Length > 0) 44 | { 45 | if (mReceiveFrame == null) 46 | mReceiveFrame = new DataFrame(); 47 | if (mReceiveFrame.Read(pipestream, WSClient) == DataPacketLoadStep.Completed) 48 | { 49 | Completed?.Invoke(client, mReceiveFrame); 50 | mReceiveFrame = null; 51 | } 52 | else 53 | { 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | catch (Exception e_) 60 | { 61 | throw new BXException("ws protocol decode error", e_); 62 | } 63 | 64 | } 65 | 66 | public void Dispose() 67 | { 68 | WSClient = null; 69 | } 70 | 71 | public void Encode(object data, IClient client, Stream stream) 72 | { 73 | try 74 | { 75 | ((DataFrame)data).Write(stream.ToPipeStream()); 76 | } 77 | catch (Exception e_) 78 | { 79 | throw new BXException("ws protocol encode error", e_); 80 | } 81 | finally 82 | { 83 | WSClient.FrameWrited((DataFrame)data); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/WebSockets/WSReceiveArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace BeetleX.Http.WebSockets 6 | { 7 | public class WSReceiveArgs:System.EventArgs 8 | { 9 | public WSClient Client { get; internal set; } 10 | 11 | public DataFrame Frame { get; internal set; } 12 | 13 | public Exception Error { get; internal set; } 14 | } 15 | } 16 | --------------------------------------------------------------------------------