├── .editorconfig ├── .clang-format ├── src ├── CMakeLists.txt ├── main.cc └── sion.h ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── .gitattributes ├── .gitignore └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cc,h}] 2 | indent_style = space 3 | indent_size = 4 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | Language: Cpp 5 | DerivePointerAlignment: false 6 | PointerAlignment: Left 7 | BreakBeforeBraces: Allman 8 | ColumnLimit: 140 -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMake 最低版本号要求 2 | cmake_minimum_required (VERSION 3.5) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | aux_source_directory(. DIR_SRCS) 6 | 7 | # 8 | 9 | if(DEFINED VCPKG_PATH) 10 | message("enable ssl") 11 | set(CMAKE_TOOLCHAIN_FILE ${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake) 12 | set(VCPKG_TARGET_TRIPLET ${VCPKG_TARGET_TRIPLET}) 13 | set(VCPKG_MANIFEST_MODE OFF) 14 | message(CMAKE_TOOLCHAIN_FILE: ${CMAKE_TOOLCHAIN_FILE}) 15 | project (sion) 16 | else() 17 | message("disable ssl") 18 | project (sion) 19 | endif() 20 | 21 | add_executable(sion ${DIR_SRCS}) 22 | if(NOT WIN32) 23 | target_link_libraries(sion PRIVATE pthread) 24 | endif() 25 | 26 | 27 | if(DEFINED VCPKG_PATH) 28 | target_link_libraries(sion PRIVATE OpenSSL::SSL OpenSSL::Crypto) 29 | find_package(OpenSSL REQUIRED) 30 | else() 31 | add_definitions(-DSION_DISABLE_SSL) 32 | endif() 33 | 34 | 35 | set(CMAKE_BUILD_TYPE "Debug") 36 | 37 | enable_testing() 38 | add_test (test_run sion -test) 39 | set_tests_properties (test_run 40 | PROPERTIES PASS_REGULAR_EXPRESSION "TestPassed;All ok") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zanllp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: ["README.md"] 7 | pull_request: 8 | branches: [ master ] 9 | paths-ignore: ["README.md"] 10 | 11 | jobs: 12 | job: 13 | runs-on: ${{ matrix.config.os }} 14 | strategy: 15 | matrix: 16 | config: 17 | - os: ubuntu-latest 18 | vcpkg_triplet: x64-linux-release 19 | - os: macos-latest 20 | vcpkg_triplet: x64-osx-release 21 | - os: windows-latest 22 | vcpkg_triplet: x64-windows-release 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: vcpkg build 26 | uses: johnwason/vcpkg-action@v4 27 | id: vcpkg 28 | with: 29 | pkgs: openssl 30 | triplet: ${{ matrix.config.vcpkg_triplet }} 31 | cache-key: ${{ matrix.config.os }} 32 | revision: master 33 | token: ${{ github.token }} 34 | - name: Install dependencies and generate project files 35 | run: | 36 | cmake -S src -B bin -DVCPKG_PATH=${{ github.workspace }}/vcpkg -DVCPKG_TARGET_TRIPLET=${{ matrix.config.vcpkg_triplet }} 37 | - name: Build 38 | run: | 39 | cmake --build bin 40 | - name: Test 41 | working-directory: ${{github.workspace}}/bin 42 | run: | 43 | ctest --verbose 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | 2 | #include "assert.h" 3 | #include "sion.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | sion::Async async_thread_pool; 10 | 11 | const sion::String url = "https://github.com/zanllp/sion"; 12 | 13 | void PrintFuncCalled(const char* str) { std::cout << "\033[32m------" << str << "-------\033[0m " << std::endl; } 14 | 15 | void FetchHeader() 16 | { 17 | 18 | PrintFuncCalled("FetchHeader"); 19 | auto resp = sion::Fetch(url); 20 | std::cout << resp.StrBody().Trim().substr(0, 100) << "..." << std::endl << "cache-control: " << resp.GetHeader().Get("cache-control"); 21 | for (auto&& i : resp.GetHeader().Data()) 22 | { 23 | std::cout << "k:" << i.first << "\tv:" << i.second.substr(0, 20) << std::endl; 24 | } 25 | assert(resp.Code() == "200"); 26 | } 27 | 28 | void FetchChunkedHtml() 29 | { 30 | PrintFuncCalled("FetchChunkedHtml"); 31 | auto resp = sion::Fetch(url); 32 | std::cout << resp.StrBody().Trim().substr(0, 100) << "..." << std::endl; 33 | assert(resp.Code() == "200"); 34 | { 35 | auto resp = sion::Fetch(url, sion::Method::Delete); 36 | assert(resp.Code() != "200"); 37 | } 38 | } 39 | 40 | void DownloadChunkedFile() 41 | { 42 | PrintFuncCalled("DownloadChunkedFile"); 43 | auto resp = sion::Fetch("http://www.httpwatch.com/httpgallery/chunked/chunkedimage.aspx"); 44 | std::ofstream file("chunked.jpeg", std::ios::binary); 45 | file.write(resp.Body().data(), resp.Body().size()); 46 | assert(resp.Code() == "200" && resp.Body().size() != 0); 47 | } 48 | 49 | void PostRequest() 50 | { 51 | PrintFuncCalled("PostRequest"); 52 | auto resp = sion::Request() 53 | .SetBody(R"({ "hello": "world" })") 54 | .SetHeader("Content-type", "application/json") 55 | .SetUrl("http://www.httpbin.org/post") 56 | .SetHttpMethod("POST") 57 | .Send(); 58 | std::cout << resp.StrBody() << std::endl; 59 | assert(resp.Code() == "200"); 60 | } 61 | 62 | void AsyncGetAvailableResponse() 63 | { 64 | PrintFuncCalled("AsyncGetAvailableResponse"); 65 | const int num = 10; 66 | for (size_t i = 0; i < num; i++) 67 | { 68 | async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 69 | } 70 | int i = 0; 71 | while (i <= num) 72 | { 73 | std::this_thread::sleep_for(std::chrono::seconds(1)); 74 | auto async_resps = async_thread_pool.GetAvailableResponse(); 75 | if (async_resps.size()) 76 | { 77 | i += async_resps.size(); 78 | std::cout << "AsyncGetAvailableResponse got resp size:" << async_resps.size() << std::endl; 79 | } 80 | } 81 | } 82 | 83 | void AsyncAwait() 84 | { 85 | PrintFuncCalled("AsyncAwait"); 86 | { 87 | auto id = async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 88 | auto pkg = async_thread_pool.Await(id); 89 | std::cout << "AsyncAwait " << pkg.resp.GetHeader().Data().size() << pkg.err_msg << std::endl; 90 | assert(pkg.resp.Code() == "200"); 91 | } 92 | { 93 | try 94 | { 95 | auto id = async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 96 | auto pkg = async_thread_pool.Await(id, 10); 97 | assert(pkg.err_msg.find("timeout") != -1); 98 | } 99 | catch (const std::exception& e) 100 | { 101 | std::cerr << e.what() << '\n'; 102 | } 103 | } 104 | } 105 | 106 | void AsyncCallback() 107 | { 108 | PrintFuncCalled("AsyncCallback"); 109 | async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }, 110 | [](sion::AsyncResponse async_resp) { 111 | std::cout << "AsyncCallback " << async_resp.resp.Status() << std::endl; 112 | assert(async_resp.resp.Code() == "200"); 113 | }); 114 | } 115 | 116 | void RequestWithProxy() 117 | { 118 | PrintFuncCalled("RequestWithProxy"); 119 | std::smatch m; 120 | std::regex url_parse(R"(^http://([\w.]+):(\d+)$)"); 121 | std::string proxy_env(std::getenv("http_proxy")); 122 | std::regex_match(proxy_env, m, url_parse); 123 | int port = std::stoi(m[2]); 124 | std::string ip = m[1]; 125 | sion::HttpProxy proxy{port, ip}; // 请求代理服务器,只支持http 126 | // 如果身份验证自己加个header https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Proxy-Authorization 127 | auto resp = sion::Request().SetProxy(proxy).SetUrl(url).SetHttpMethod("GET").Send(); 128 | std::cout << "server: " << resp.GetHeader().Get("server") << std::endl; 129 | std::cout << resp.StrBody() << std::endl; 130 | } 131 | 132 | sion::Payload::Binary LoadFile(sion::String path, sion::String type) 133 | { 134 | std::fstream file(path, std::ios_base::in); 135 | assert(file.good()); 136 | sion::Payload::Binary bin; 137 | bin.file_name = path; 138 | bin.type = type; 139 | const int size = 1000; 140 | std::array buf; 141 | while (file.good()) 142 | { 143 | file.read(buf.data(), size - 1); 144 | bin.data.insert(bin.data.end(), buf.begin(), buf.begin() + file.gcount()); 145 | } 146 | return bin; 147 | } 148 | 149 | void PostBinaryData() 150 | { 151 | PrintFuncCalled("PostBinaryData - Formdata"); 152 | auto file = LoadFile("chunked.jpeg", "image/jpeg"); 153 | { 154 | 155 | sion::Payload::FormData form; 156 | form.Append("helo", "world"); 157 | form.Append("hello2", "world"); 158 | form.Append("file", file); 159 | form.Append("agumi", "script runtime"); 160 | form.Remove("agumi"); 161 | auto req = sion::Request().SetUrl("http://www.httpbin.org/post").SetBody(form).SetHttpMethod("POST").Send(); 162 | // std::cout << "post binary data with FormData " << req.Code() << req.StrBody(); 163 | assert(req.Code() == "200"); 164 | } 165 | 166 | PrintFuncCalled("PostBinaryData - Binary"); 167 | { 168 | auto req = sion::Request() 169 | .SetUrl("http://www.httpbin.org/post") 170 | .SetBody(file) 171 | .SetHttpMethod("POST") 172 | .Send(); 173 | // std::cout << "post binary data with Binary " << req.Code() << req.StrBody(); 174 | assert(req.Code() == "200"); 175 | } 176 | } 177 | int main() 178 | { 179 | async_thread_pool.Start(); 180 | // RequestWithProxy(); 181 | FetchHeader(); 182 | FetchChunkedHtml(); 183 | DownloadChunkedFile(); 184 | PostRequest(); 185 | AsyncAwait(); 186 | AsyncCallback(); 187 | AsyncGetAvailableResponse(); 188 | PostBinaryData(); 189 | std::cout << "TestPassed;All ok" << std::endl; 190 | return 1; 191 | } 192 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | .vscode 6 | bin 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | **/chunked.jpeg 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015/2017 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # Visual Studio 2017 auto generated files 35 | Generated\ Files/ 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # Benchmark Results 51 | BenchmarkDotNet.Artifacts/ 52 | 53 | # .NET Core 54 | project.lock.json 55 | project.fragment.lock.json 56 | artifacts/ 57 | **/Properties/launchSettings.json 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_i.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *.log 83 | *.vspscc 84 | *.vssscc 85 | .builds 86 | *.pidb 87 | *.svclog 88 | *.scc 89 | 90 | # Chutzpah Test files 91 | _Chutzpah* 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opendb 98 | *.opensdf 99 | *.sdf 100 | *.cachefile 101 | *.VC.db 102 | *.VC.VC.opendb 103 | 104 | # Visual Studio profiler 105 | *.psess 106 | *.vsp 107 | *.vspx 108 | *.sap 109 | 110 | # Visual Studio Trace Files 111 | *.e2e 112 | 113 | # TFS 2012 Local Workspace 114 | $tf/ 115 | 116 | # Guidance Automation Toolkit 117 | *.gpState 118 | 119 | # ReSharper is a .NET coding add-in 120 | _ReSharper*/ 121 | *.[Rr]e[Ss]harper 122 | *.DotSettings.user 123 | 124 | # JustCode is a .NET coding add-in 125 | .JustCode 126 | 127 | # TeamCity is a build add-in 128 | _TeamCity* 129 | 130 | # DotCover is a Code Coverage Tool 131 | *.dotCover 132 | 133 | # AxoCover is a Code Coverage Tool 134 | .axoCover/* 135 | !.axoCover/settings.json 136 | 137 | # Visual Studio code coverage results 138 | *.coverage 139 | *.coveragexml 140 | 141 | # NCrunch 142 | _NCrunch_* 143 | .*crunch*.local.xml 144 | nCrunchTemp_* 145 | 146 | # MightyMoose 147 | *.mm.* 148 | AutoTest.Net/ 149 | 150 | # Web workbench (sass) 151 | .sass-cache/ 152 | 153 | # Installshield output folder 154 | [Ee]xpress/ 155 | 156 | # DocProject is a documentation generator add-in 157 | DocProject/buildhelp/ 158 | DocProject/Help/*.HxT 159 | DocProject/Help/*.HxC 160 | DocProject/Help/*.hhc 161 | DocProject/Help/*.hhk 162 | DocProject/Help/*.hhp 163 | DocProject/Help/Html2 164 | DocProject/Help/html 165 | 166 | # Click-Once directory 167 | publish/ 168 | 169 | # Publish Web Output 170 | *.[Pp]ublish.xml 171 | *.azurePubxml 172 | # Note: Comment the next line if you want to checkin your web deploy settings, 173 | # but database connection strings (with potential passwords) will be unencrypted 174 | *.pubxml 175 | *.publishproj 176 | 177 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 178 | # checkin your Azure Web App publish settings, but sensitive information contained 179 | # in these scripts will be unencrypted 180 | PublishScripts/ 181 | 182 | # NuGet Packages 183 | *.nupkg 184 | # The packages folder can be ignored because of Package Restore 185 | **/[Pp]ackages/* 186 | # except build/, which is used as an MSBuild target. 187 | !**/[Pp]ackages/build/ 188 | # Uncomment if necessary however generally it will be regenerated when needed 189 | #!**/[Pp]ackages/repositories.config 190 | # NuGet v3's project.json files produces more ignorable files 191 | *.nuget.props 192 | *.nuget.targets 193 | 194 | # Microsoft Azure Build Output 195 | csx/ 196 | *.build.csdef 197 | 198 | # Microsoft Azure Emulator 199 | ecf/ 200 | rcf/ 201 | 202 | # Windows Store app package directories and files 203 | AppPackages/ 204 | BundleArtifacts/ 205 | Package.StoreAssociation.xml 206 | _pkginfo.txt 207 | *.appx 208 | 209 | # Visual Studio cache files 210 | # files ending in .cache can be ignored 211 | *.[Cc]ache 212 | # but keep track of directories ending in .cache 213 | !*.[Cc]ache/ 214 | 215 | # Others 216 | ClientBin/ 217 | ~$* 218 | *~ 219 | *.dbmdl 220 | *.dbproj.schemaview 221 | *.jfm 222 | *.pfx 223 | *.publishsettings 224 | orleans.codegen.cs 225 | 226 | # Including strong name files can present a security risk 227 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 228 | #*.snk 229 | 230 | # Since there are multiple workflows, uncomment next line to ignore bower_components 231 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 232 | #bower_components/ 233 | 234 | # RIA/Silverlight projects 235 | Generated_Code/ 236 | 237 | # Backup & report files from converting an old project file 238 | # to a newer Visual Studio version. Backup files are not needed, 239 | # because we have git ;-) 240 | _UpgradeReport_Files/ 241 | Backup*/ 242 | UpgradeLog*.XML 243 | UpgradeLog*.htm 244 | ServiceFabricBackup/ 245 | *.rptproj.bak 246 | 247 | # SQL Server files 248 | *.mdf 249 | *.ldf 250 | *.ndf 251 | 252 | # Business Intelligence projects 253 | *.rdl.data 254 | *.bim.layout 255 | *.bim_*.settings 256 | *.rptproj.rsuser 257 | 258 | # Microsoft Fakes 259 | FakesAssemblies/ 260 | 261 | # GhostDoc plugin setting file 262 | *.GhostDoc.xml 263 | 264 | # Node.js Tools for Visual Studio 265 | .ntvs_analysis.dat 266 | node_modules/ 267 | 268 | # Visual Studio 6 build log 269 | *.plg 270 | 271 | # Visual Studio 6 workspace options file 272 | *.opt 273 | 274 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 275 | *.vbw 276 | 277 | # Visual Studio LightSwitch build output 278 | **/*.HTMLClient/GeneratedArtifacts 279 | **/*.DesktopClient/GeneratedArtifacts 280 | **/*.DesktopClient/ModelManifest.xml 281 | **/*.Server/GeneratedArtifacts 282 | **/*.Server/ModelManifest.xml 283 | _Pvt_Extensions 284 | 285 | # Paket dependency manager 286 | .paket/paket.exe 287 | paket-files/ 288 | 289 | # FAKE - F# Make 290 | .fake/ 291 | 292 | # JetBrains Rider 293 | .idea/ 294 | *.sln.iml 295 | 296 | # CodeRush 297 | .cr/ 298 | 299 | # Python Tools for Visual Studio (PTVS) 300 | __pycache__/ 301 | *.pyc 302 | 303 | # Cake - Uncomment if you are using it 304 | # tools/** 305 | # !tools/packages.config 306 | 307 | # Tabs Studio 308 | *.tss 309 | 310 | # Telerik's JustMock configuration file 311 | *.jmconfig 312 | 313 | # BizTalk build output 314 | *.btp.cs 315 | *.btm.cs 316 | *.odx.cs 317 | *.xsd.cs 318 | 319 | # OpenCover UI analysis results 320 | OpenCover/ 321 | 322 | # Azure Stream Analytics local run output 323 | ASALocalRun/ 324 | 325 | # MSBuild Binary and Structured Log 326 | *.binlog 327 | 328 | # NVidia Nsight GPU debugger configuration file 329 | *.nvuser 330 | 331 | # MFractors (Xamarin productivity tool) working folder 332 | .mfractor/ 333 | *.png 334 | *.jpeg 335 | *.b 336 | *.gif 337 | /Sion/Sion/chunked.html 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sion 2 | [![ci](https://github.com/zanllp/sion/actions/workflows/ci.yml/badge.svg)](https://github.com/zanllp/sion/actions/workflows/ci.yml) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style)](https://makeapullrequest.com) 3 | 4 | 5 | 直接复制[sion.h](src/sion.h)到自己的项目下`include` 6 | 7 | Include [sion.h](src/sion.h) directly 8 | ## GET 9 | ```cpp 10 | auto resp = sion::Fetch(url); 11 | std::cout << resp.StrBody() << std::endl; 12 | std::cout << resp.GetHeader().Get("content-type") << std::endl; 13 | ``` 14 | ## POST 15 | ```CPP 16 | auto resp = sion::Request() 17 | .SetBody(R"({ "hello": "world" })") 18 | .SetHeader("Content-type", "application/json") 19 | .SetUrl("http://www.httpbin.org/post") 20 | .SetHttpMethod("POST") 21 | .Send(); 22 | std::cout << resp.StrBody() << std::endl; 23 | ``` 24 | > see [main.cc](https://github.com/zanllp/sion/blob/master/src/main.cc) for more example。更多例子参考[main.cc](https://github.com/zanllp/sion/blob/master/src/main.cc) 25 | 26 | # 特性/feature 27 | * Sion是一个轻量级简单易用的c++请求库 28 | * 仅单个头文件,自带std::string的扩展 29 | * 跨平台,支持linux, win, mac... 30 | * 有着良好的异步支持,可以选择以自己喜欢的方式发送异步请求, callback, await, 事件循环, etc. 31 | * 支持文本及二进制的响应体 32 | * 支持分块(chunked)的传输编码 33 | * 支持FormData&单独的二进制载荷请求 34 | * 支持http代理 35 | * 支持http,https请求。_https需要安装openssl(推荐使用[vcpkg](https://github.com/microsoft/vcpkg)),如果不需要可以使用 #define SION_DISABLE_SSL 关闭_。如果使用了代理,即使是没启用ssl,也可以请求https链接。 36 | # 用法 37 | 38 | 39 | 40 | ## 异步请求 41 | sion在此版本后对异步增加了巨大的支持,相对于之前的版本依旧保持了同步api+异步库的方式,尽可能保持简单轻量,尽管性能不是很理想但性能从来就不是sion的最优先考虑的东西,但性能也不算太差 [粗略的性能参考](#粗略的性能参考)。 42 | 43 | sion的异步支持依赖于sion内置的Async线程池,函数可参考[Async](#Async)。 44 | 45 | ### 先启动线程池线程池 46 | ```cpp 47 | sion::Async async_thread_pool; 48 | 49 | async_thread_pool.Start(); // 启动线程池,创建子线程,子线程接受任务开始工作 50 | ``` 51 | 52 | ### 添加请求到线程池,并接受响应 53 | sion::Async处理异步有3种方式 54 | 1. 使用回调 55 | 56 | ```cpp 57 | async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }, 58 | [](sion::AsyncResponse async_resp) { std::cout << "AsyncCallback " << async_resp.resp.Status() << std::endl; }); 59 | ``` 60 | 61 | 2. 使用await 62 | 63 | 在无回调时提交任务到线程池会返回给你一个id,通过这个id我们可以使用await在当前线程上等待请求完成。 64 | 65 | ```cpp 66 | auto id = async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 67 | // 在Run后的这段时间里当前线程可以先去干其他的活,等待需要使用响应时再使用await获取响应。 68 | auto pkg = async_thread_pool.Await(id); 69 | std::cout << "AsyncAwait " << pkg.resp.GetHeader().Data().size() << pkg.err_msg << std::endl; 70 | // 你可以给await添加超时时间,如果超时会抛出AsyncAwaitTimeout 71 | try 72 | { 73 | auto id = async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 74 | auto pkg = async_thread_pool.Await(id, 10); 75 | } 76 | catch (const std::exception& e) 77 | { 78 | std::cerr << e.what() << '\n'; 79 | } 80 | ``` 81 | 82 | 3. 使用GetAvailableResponse 83 | 84 | 85 | 这种方式是通过不断获取调取此函数来获取想要的响应,主要还是方便与事件循环集成。 86 | ```cpp 87 | const int num = 100; 88 | for (size_t i = 0; i < num; i++) 89 | { 90 | async_thread_pool.Run([=] { return sion::Request().SetUrl(url).SetHttpMethod(sion::Method::Get); }); 91 | } 92 | int i = 0; 93 | while (i <= num) 94 | { 95 | std::this_thread::sleep_for(std::chrono::seconds(1)); 96 | auto async_resps = async_thread_pool.GetAvailableResponse(); 97 | if (async_resps.size()) 98 | { 99 | i += async_resps.size(); 100 | std::cout << "AsyncGetAvailableResponse got resp size:" << async_resps.size() << std::endl; 101 | } 102 | } 103 | ``` 104 | ### AsyncResponse 105 | 上面几种方式都会返回AsyncResponse,通过这个可以获取异步请求的响应体,id,以及错误信息 106 | 107 | ## 更多用法 108 | 包含普通请求,代理,二进制数据发送,FormData,Async等 109 | [查看参考](./src/main.cc) 110 | # 类&函数定义 111 | ## Fetch 112 | ~~~cpp 113 | // 静态请求方法 114 | Response Fetch(String url, Method method = Get, Header header = Header(), String body = ""); 115 | ~~~ 116 | 117 | ## Response 118 | 该类用来处理请求响应 119 | ```cpp 120 | // 返回接收到的响应体 121 | const vector& Body(); 122 | // 将接收到的响应体转成字符串,如果你能确保接收到的数据是字符串,可以直接使用这个 123 | String StrBody(); 124 | String Code(); 125 | String Status(); 126 | int ContentLength(); 127 | const Header& GetHeader();// 获取响应头 参考 [Header](#Header) 128 | ``` 129 | 130 | ## Request 131 | 该类用来处理发送请求 132 | ~~~cpp 133 | //支持链式设置属性 134 | Request& SetHttpMethod(String other) ; 135 | Request& SetUrl(String url); 136 | Request& SetBody(String body); // 设置响应体,字符串 137 | Request& SetBody(Payload::Binary body); // 设置响应体,二进制文件 138 | Request& SetBody(Payload::FormData body); // 设置响应体,表单数据 139 | Request& SetHeader(Header header); 140 | Request& SetHeader(String k, String v); 141 | Request& SetProxy(HttpProxy proxy); // 设置代理,仅在http可用 142 | Response Send(); // 发送请求 143 | ~~~ 144 | 145 | ## Header 146 | 响应头和请求头 147 | ```cpp 148 | // 添加一个键值对到头中 149 | void Add(String k, String v); 150 | 151 | // 获取头的键所对应的所有值 152 | vector GetAll(String key); 153 | 154 | // 获取头的键所对应的值,以最后一个为准 155 | String Get(String key); 156 | 157 | //删除目标键值对,返回一个bool指示操作是否成功 158 | bool Remove(String key); 159 | 160 | // 删除所有的键值对 161 | void RemoveAll(String key) 162 | 163 | // 返回数据本体 164 | const vector>& Data(); 165 | ``` 166 | ## Async 167 | 该类依赖处理sion的异步请求,内置了个小型的线程池,提供了callback,await,事件循环三种方式让你来处理异步的请求。线程池会在析构时退出启动的所有线程。 168 | 169 | ```cpp 170 | // 设置线程池的线程数量,即能够同时请求或者是运行请求回调的数量, 如果进行比较重的回调可以设置成std::thread::hardware_concurrency(),以减少上下文切换开销 171 | Async& SetThreadNum(int num); 172 | 173 | // 设置线程池是否以阻塞当前线程的方式运行 174 | Async& SetBlock(bool wait); 175 | 176 | // 设置await是否在接收到包含错误消息的响应时抛异常 177 | Async& SetThrowIfHasErrMsg(bool op); 178 | 179 | // 启动线程池 180 | void Start(); 181 | 182 | // 添加一个请求到线程池的任务队列, 返回一个请求的id,id可用于await或者是查找对应的请求 183 | int Run(std::function fn); 184 | 185 | // 添加一个请求和回调到线程池的任务队列,在请求完成后会在请求的线程执行回调 186 | void Run(std::function fn, std::function cb); 187 | 188 | //在当前线程等待直到目标请求可用。id:Async::Run返回的id,timeout_ms 超时时间单位毫秒,默认永不超时 189 | AsyncResponse Await(int id, int timeout_ms); 190 | 191 | // 获得当前可用的响应,如果你添加一个请求到线程池的任务队列,且没有回调或者使用await取获取,那么可用通过这种方式获取。适用场合是一般是在事件循环里面 192 | vector GetAvailableResponse(); 193 | ``` 194 | ## Payload 195 | Payload名称空间包含2种请求体可用的数据 196 | ```cpp 197 | namespace Payload 198 | { 199 | struct Binary 200 | { 201 | std::vector data; // 数据本体 202 | String file_name; // 文件名,在提交formdata如果写了,则会带上文件名的信息 203 | String type; // 文件mime类型,如果写了在直接使用二进制时会添加到正文的Content-Type,在formdata则是添加到子部分的 204 | }; 205 | 206 | class FormData 207 | { 208 | void Append(String name, Binary value); // 添加一个新的二进制值 209 | void Append(String name, String value); // 添加一个新的字符串键 210 | bool Remove(String key); // 移除一项,返回是否成功 211 | const std::vector>>& Data() const; // 返回数据本体 212 | }; 213 | } 214 | ``` 215 | 216 | ## String 217 | 该类继承std::string,用法基本一致,拓展了几个函数 218 | 219 | ~~~cpp 220 | 221 | //使用字符串分割 222 | //flag 分割标志,返回的字符串向量会剔除,flag不要用char,会重载不明确 223 | //num 分割次数,默认0即分割到结束,例num=1,返回开头到flag,flag到结束size=2的字符串向量 224 | //skip_empty 跳过空字符串,即不压入length==0的字符串 225 | std::vector Split(String flag, int num = -1, bool skip_empty = true); 226 | 227 | //清除前后的字符 228 | //target 需要清除的字符默认空格空格姬换行符 229 | String Trim(String target = " \n\r"); 230 | 231 | //包含字母 232 | bool HasLetter(); 233 | 234 | // 转成小写,返回一个新的字符串 235 | String ToLowerCase(); 236 | 237 | // 转成大写,返回一个新的字符串 238 | String ToUpperCase(); 239 | 240 | //返回搜索到的所有位置 241 | //flag 定位标志 242 | //num 搜索数量,默认直到结束 243 | std::vector FindAll(String flag, int num = -1); 244 | 245 | //字符串替换,会修改原有的字符串,而不是返回新的 246 | String& Replace(String old_str, String new_str); 247 | ~~~ 248 | 249 | Proxy 250 | ```cpp 251 | struct HttpProxy 252 | { 253 | int port; // 目标端口 254 | String host; // 目标主机,ip 255 | }; 256 | ``` 257 | 258 | # 粗略的性能参考 259 | 12小时请求270万次。占用内存10m,cpu负载2%左右。cpu是AMD Ryzen 7 PRO 4750U 260 | ![PF9Q_G L~B_EFDKL2 X9 XL](https://user-images.githubusercontent.com/25872019/153871447-3e4be3e9-3b86-43a7-8ea6-85696c785dca.jpg) 261 | -------------------------------------------------------------------------------- /src/sion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #ifdef _WIN32 14 | #include 15 | #include 16 | #include 17 | #pragma comment(lib, "ws2_32.lib") 18 | #else 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #endif // WIN32 25 | #ifndef SION_DISABLE_SSL 26 | #include 27 | #include 28 | #include 29 | #endif // !SION_DISABLE_SSL 30 | namespace sion 31 | { 32 | class Request; 33 | class Response; 34 | 35 | class Error : public std::exception 36 | { 37 | std::string msg; 38 | 39 | public: 40 | Error() {} 41 | Error(std::string msg) : msg(msg) {} 42 | ~Error() {} 43 | const char* what() const noexcept { return msg.c_str(); } 44 | }; 45 | 46 | class AsyncAwaitTimeout : public Error 47 | { 48 | public: 49 | AsyncAwaitTimeout(std::string msg = "await timeout") : Error(msg) {} 50 | }; 51 | 52 | class PeerConnectionClose : public Error 53 | { 54 | public: 55 | PeerConnectionClose(std::string msg = "对方关闭了连接") : Error(msg) {} 56 | }; 57 | 58 | class String : public std::string 59 | { 60 | public: 61 | String(){}; 62 | ~String(){}; 63 | template String(T&& arg) : std::string(std::forward(arg)) {} 64 | 65 | String(int arg) : std::string(std::to_string(arg)) {} 66 | 67 | String(unsigned long arg) : std::string(std::to_string(arg)) {} 68 | 69 | String(double arg) : std::string(std::to_string(arg)) {} 70 | 71 | String(bool arg) { (*this) = arg ? "true" : "false"; } 72 | 73 | String(char arg) 74 | { 75 | (*this) = " "; 76 | (*this)[0] = arg; 77 | } 78 | 79 | // 使用字符串分割 80 | // flag 分割标志,返回的字符串向量会剔除,flag不要用char,会重载不明确 81 | // num 分割次数,默认-1即分割到结束,例num=1,返回开头到flag,flag到结束size=2的字符串向量 82 | // skip_empty 跳过空字符串,即不压入length==0的字符串 83 | std::vector Split(String flag, int num = -1, bool skip_empty = true) const 84 | { 85 | std::vector data_set; 86 | auto push_data = [&](String line) { 87 | if (line.length() != 0 || !skip_empty) 88 | { 89 | data_set.push_back(line); 90 | } 91 | }; 92 | auto pos = FindAll(flag, num); 93 | if (pos.size() == 0) 94 | { 95 | return std::vector({*this}); 96 | } 97 | for (auto i = 0; i < static_cast(pos.size()) + 1; i++) 98 | { 99 | if (data_set.size() == num && static_cast(pos.size()) > num && num != -1) 100 | { // 满足数量直接截到结束 101 | push_data(substr(pos[data_set.size()] + flag.size())); 102 | break; 103 | } 104 | if (i == 0) 105 | { // 第一个数的位置不是0的话补上 106 | push_data(substr(0, pos[0])); 107 | } 108 | else if (i != pos.size()) 109 | { 110 | int left = pos[i - 1] + static_cast(flag.length()); 111 | int right = pos[i] - left; 112 | push_data(substr(left, right)); 113 | } 114 | else 115 | { // 最后一个标志到结束 116 | push_data(substr(*(--pos.end()) + flag.size())); 117 | } 118 | } 119 | return data_set; 120 | } 121 | // 清除前后的字符 122 | // target 需要清除的字符默认空格 123 | String Trim(String empty_set = " \n\r") const 124 | { 125 | int len = static_cast(length()); 126 | int left = 0; 127 | while (left < len && IncludeSym(empty_set, (*this)[left])) 128 | { 129 | left++; 130 | } 131 | if (left >= len) 132 | { 133 | return *this; 134 | } 135 | int right = len - 1; 136 | while (right > 0 && IncludeSym(empty_set, (*this)[right])) 137 | { 138 | right--; 139 | } 140 | return substr(left, right - left + 1); 141 | } 142 | 143 | String ToLowerCase() 144 | { 145 | String s = *this; 146 | std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); 147 | return s; 148 | } 149 | 150 | String ToUpperCase() 151 | { 152 | String s = *this; 153 | std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::toupper(c); }); 154 | return s; 155 | } 156 | 157 | static bool IncludeSym(String syms, char sym) 158 | { 159 | for (auto i : syms) 160 | { 161 | if (i == sym) 162 | { 163 | return true; 164 | } 165 | } 166 | return false; 167 | } 168 | 169 | // 包含字母 170 | bool HasLetter() 171 | { 172 | for (auto& x : *this) 173 | { 174 | if ((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z')) 175 | { 176 | return true; 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | // 返回搜索到的所有位置 183 | // flag 定位标志 184 | // num 搜索数量,默认直到结束 185 | std::vector FindAll(String flag, int num = -1) const 186 | { 187 | std::vector result; 188 | auto pos = find(flag); 189 | auto flag_offset = flag.length() == 0 ? 1 : flag.length(); 190 | while (pos != -1 && result.size() != num) 191 | { 192 | result.push_back(static_cast(pos)); 193 | pos = find(flag, *(--result.end()) + flag_offset); 194 | } 195 | return result; 196 | } 197 | 198 | // 字符串替换 199 | // oldStr 被替换的字符串 200 | // newStr 新换上的字符串 201 | // count 替换次数,默认1,大于0时替换到足够次数或找不到旧字符串为止,小于0时替换到结束 202 | String& Replace(String old_str, String new_str, int count = 1) 203 | { 204 | if (count == 0) 205 | { 206 | return *this; 207 | } 208 | auto pos = find(old_str); 209 | if (pos == std::string::npos) 210 | { 211 | return *this; 212 | } 213 | replace(pos, old_str.length(), new_str); 214 | return Replace(old_str, new_str, count < 0 ? -1 : count - 1); 215 | } 216 | }; 217 | 218 | template void Throw(String msg = "") { throw ExceptionType(msg); } 219 | 220 | template 221 | void check( 222 | bool condition, String msg = "", std::function recycle = [] {}) 223 | { 224 | if (!condition) 225 | { 226 | recycle(); 227 | Throw(msg); 228 | } 229 | } 230 | using Socket = int; 231 | static String GetIpByHost(String hostname) 232 | { 233 | addrinfo hints, *res; 234 | in_addr addr; 235 | int err; 236 | memset(&hints, 0, sizeof(addrinfo)); 237 | hints.ai_socktype = SOCK_STREAM; 238 | hints.ai_family = AF_INET; // ipv4 239 | if ((err = getaddrinfo(hostname.c_str(), NULL, &hints, &res)) != 0) 240 | { 241 | #if _WIN32 242 | auto str_err = gai_strerrorA(err); 243 | #else 244 | auto str_err = gai_strerror(err); 245 | #endif // _WIN32 246 | 247 | Throw("错误" + std::to_string(err) + String(str_err)); 248 | } 249 | addr.s_addr = ((sockaddr_in*)(res->ai_addr))->sin_addr.s_addr; 250 | char str[INET_ADDRSTRLEN]; 251 | inet_ntop(AF_INET, &addr, str, sizeof(str)); 252 | freeaddrinfo(res); 253 | return str; 254 | } 255 | 256 | static Socket GetSocket() 257 | { 258 | #ifdef _WIN32 259 | // 初始化。,WSA windows异步套接字 260 | WSADATA inet_WsaData; // 261 | const auto result = WSAStartup(MAKEWORD(2, 0), &inet_WsaData); // socket2.0版本 262 | switch (result) 263 | { 264 | case WSASYSNOTREADY: 265 | WSACleanup(); 266 | Throw("底层网络子系统尚未准备好进行网络通信"); 267 | break; 268 | case WSAVERNOTSUPPORTED: 269 | WSACleanup(); 270 | Throw("此特定 Windows 套接字实现不提供请求的 Windows 套接字支持版本"); 271 | break; 272 | case WSAEINPROGRESS: 273 | WSACleanup(); 274 | Throw("正在进行阻塞的 Windows Sockets 1.1 操作"); 275 | break; 276 | case WSAEPROCLIM: 277 | WSACleanup(); 278 | Throw("已达到 Windows 套接字实现支持的任务数限制"); 279 | break; 280 | } 281 | 282 | if (LOBYTE(inet_WsaData.wVersion) != 2 || HIBYTE(inet_WsaData.wVersion) != 0) 283 | { // 高位字节指明副版本、低位字节指明主版本 284 | WSACleanup(); 285 | Throw("wsa初始化错误"); 286 | } 287 | #endif 288 | auto tcp_socket = socket(AF_INET, SOCK_STREAM, 0); // ipv4,tcp,tcp或udp该参数可为0 289 | return tcp_socket; 290 | } 291 | 292 | const String crlf = "\r\n"; 293 | const String crlf_crlf = "\r\n\r\n"; 294 | static void PushStr2Vec(const String& str, std::vector& vec) { vec.insert(vec.end(), str.begin(), str.end()); } 295 | 296 | namespace Payload 297 | { 298 | struct Binary 299 | { 300 | std::vector data; 301 | String file_name; 302 | String type; 303 | }; 304 | 305 | class FormData 306 | { 307 | std::vector>> data_; 308 | String boundary_; 309 | 310 | public: 311 | FormData() { boundary_ = "----SionBoundary" + std::to_string(std::uintptr_t(this)); } 312 | ~FormData() {} 313 | void Append(String name, Binary value) 314 | { 315 | std::vector res; 316 | String filename = value.file_name.size() ? "; filename=\"" + value.file_name + "\"" : ""; 317 | String str = "--" + boundary_ + crlf; 318 | str += "Content-Disposition: form-data; name=\"" + name + "\""; 319 | str += filename; 320 | str += crlf; 321 | if (value.type != "") 322 | { 323 | str += "Content-type: " + value.type; 324 | str += crlf; 325 | } 326 | str += crlf; 327 | PushStr2Vec(str, res); 328 | res.insert(res.end(), value.data.begin(), value.data.end()); 329 | PushStr2Vec(crlf, res); 330 | data_.push_back({name, res}); 331 | } 332 | void Append(String name, String value) 333 | { 334 | std::vector res; 335 | String str = "--" + boundary_ + crlf; 336 | str += "Content-Disposition: form-data; name=\"" + name + "\"" + crlf; 337 | str += crlf; 338 | str += value; 339 | str += crlf; 340 | PushStr2Vec(str, res); 341 | data_.push_back({name, res}); 342 | } 343 | bool Remove(String key) 344 | { 345 | for (size_t i = 0; i < data_.size(); i++) 346 | { 347 | if (data_[i].first == key) 348 | { 349 | data_.erase(data_.begin() + i); 350 | return true; 351 | } 352 | } 353 | return false; 354 | } 355 | const std::vector>>& Data() const { return data_; } 356 | String GetContentType() { return "multipart/form-data; boundary=" + boundary_; } 357 | std::vector Serialize() 358 | { 359 | std::vector res; 360 | for (auto& i : data_) 361 | { 362 | res.insert(res.end(), i.second.begin(), i.second.end()); 363 | } 364 | res.push_back('-'); 365 | res.push_back('-'); 366 | PushStr2Vec(boundary_, res); 367 | res.push_back('-'); 368 | res.push_back('-'); 369 | return res; 370 | } 371 | }; 372 | } // namespace Payload 373 | 374 | class Header 375 | { 376 | public: 377 | using RawT = typename std::vector>; 378 | Header(){}; 379 | ~Header(){}; 380 | const RawT& Data() const { return data; } 381 | bool Remove(String key) 382 | { 383 | for (size_t i = 0; i < data.size(); i++) 384 | { 385 | if (data[i].first == key) 386 | { 387 | data.erase(data.begin() + i); 388 | return true; 389 | } 390 | } 391 | return false; 392 | } 393 | void RemoveAll(String key) 394 | { 395 | while (Remove(key)) 396 | { 397 | } 398 | } 399 | Header(const RawT& other) { data = other; } 400 | // 添加一个键值对到头中 401 | void Add(String k, String v) { data.push_back({k, v}); } 402 | // 获取头的键所对应的所有值 403 | std::vector GetAll(String key) const 404 | { 405 | key = key.ToLowerCase(); 406 | std::vector res; 407 | for (auto& i : data) 408 | { 409 | if (i.first == key) 410 | { 411 | res.push_back(i.second); 412 | } 413 | } 414 | return res; 415 | } 416 | // 获取头的键所对应的值,以最后一个为准 417 | String Get(String key) const 418 | { 419 | key = key.ToLowerCase(); 420 | for (int i = static_cast(data.size()) - 1; i >= 0; i--) 421 | { 422 | if (data[i].first == key) 423 | { 424 | return data[i].second; 425 | } 426 | } 427 | return ""; 428 | } 429 | 430 | private: 431 | RawT data; 432 | }; 433 | 434 | enum class Method 435 | { 436 | Get, 437 | Post, 438 | Put, 439 | Delete 440 | }; 441 | 442 | class Response 443 | { 444 | friend Request; 445 | bool is_chunked_ = false; // 是否分块编码 446 | int content_length_ = 0; // 正文长度 447 | std::vector source_; // 源响应报文 448 | String protocol_version_; 449 | String code_; 450 | String status_; 451 | std::vector body_; // 响应体 452 | Header response_header_; // 响应头 453 | public: 454 | Response(){}; 455 | ~Response(){}; 456 | 457 | Response(std::vector source) 458 | { 459 | source_ = source; 460 | ParseHeader(); 461 | ParseBody(); 462 | } 463 | 464 | const std::vector& Body() const { return body_; } 465 | const String Code() const { return code_; }; 466 | const String Status() const { return status_; }; 467 | const int ContentLength() const { return content_length_; }; 468 | String StrBody() { return body_.size() == 0 ? "" : std::string(body_.data(), 0, body_.size()); } 469 | const Header& GetHeader() const { return response_header_; }; 470 | 471 | private: 472 | size_t resp_body_start_pos_ = -1; 473 | 474 | String Sourse2Str() { return source_.size() == 0 ? "" : std::string(source_.data(), 0, source_.size()); } 475 | 476 | bool CanParseHeader() 477 | { 478 | auto buf_str = Sourse2Str(); 479 | if (resp_body_start_pos_ == -1 && buf_str.find(crlf_crlf) != -1) 480 | { 481 | resp_body_start_pos_ = buf_str.find(crlf_crlf) + 4; 482 | } 483 | return resp_body_start_pos_ != std::string::npos; 484 | } 485 | 486 | void ParseHeader() 487 | { 488 | String buf_str = Sourse2Str(); 489 | auto header_str = buf_str.substr(0, resp_body_start_pos_); 490 | auto data = String(header_str).Split(crlf); 491 | if (data.size() == 0) 492 | { 493 | return; 494 | } 495 | // 第一行 496 | auto first_line = data[0].Split(" ", 2); 497 | check(first_line.size() == 3, "解析错误\n" + buf_str); 498 | protocol_version_ = first_line[0].Trim(); 499 | code_ = first_line[1].Trim(); 500 | status_ = first_line[2].Trim(); 501 | data.erase(data.begin()); 502 | // 头 503 | for (auto& x : data) 504 | { 505 | auto pair = x.Split(":", 1); 506 | if (pair.size() == 2) 507 | { 508 | response_header_.Add(pair[0].Trim().ToLowerCase(), pair[1].Trim()); 509 | } 510 | } 511 | String content_len = response_header_.Get("content-length"); 512 | content_length_ = content_len != "" ? stoi(content_len) : content_length_; 513 | is_chunked_ = response_header_.Get("Transfer-Encoding") == "chunked"; 514 | body_.insert(body_.end(), source_.begin() + resp_body_start_pos_, source_.end()); 515 | } 516 | 517 | void ParseBody() 518 | { 519 | const auto& sc = body_; 520 | if (sc.size() == 0 || !is_chunked_) 521 | { 522 | return; 523 | } 524 | std::vector pure_source_char; 525 | // 获取下一个\r\n的位置 526 | int crlf_pos = 0; 527 | auto get_next_crlf = [&](int leap) { 528 | for (int i = crlf_pos + leap; i < static_cast(sc.size() - 1); i++) 529 | { 530 | if (sc[i] == '\r' && sc[i + 1] == '\n') 531 | { 532 | crlf_pos = i; 533 | return i; 534 | } 535 | } 536 | return -1; 537 | }; 538 | int left = -2; // 这里-2是因为第一个数量是400\r\n这样的,而其它的是\r\n400\r\n。所以要对第一次进行补偿 539 | int right = get_next_crlf(0); 540 | while (left != -1 && right != -1) 541 | { 542 | auto count = std::string(sc.begin() + 2 + left, sc.begin() + right); // 每个分块开头写的数量 543 | auto count_num = stoi(count, nullptr, 16); // 那数量是16进制 544 | if (count_num == 0) // 最后一个 0\r\n\r\n,退出 545 | { 546 | break; 547 | } 548 | auto chunked_start = sc.begin() + right + 2; // 每个分块正文的开始位置 549 | pure_source_char.insert(pure_source_char.end(), chunked_start, chunked_start + count_num); 550 | left = get_next_crlf(count_num + 2); // 更新位置lpv 551 | right = get_next_crlf(1); 552 | } 553 | body_ = pure_source_char; 554 | content_length_ = static_cast(pure_source_char.size()); 555 | } 556 | }; 557 | 558 | struct HttpProxy 559 | { 560 | int port{}; 561 | String host; 562 | }; 563 | 564 | class Request 565 | { 566 | std::vector source_; 567 | String method_; 568 | String path_; 569 | String protocol_; 570 | String ip_; 571 | String url_; 572 | String host_; 573 | std::vector request_body_; 574 | String protocol_version_ = "HTTP/1.1"; 575 | Header request_header_; 576 | HttpProxy proxy_; 577 | bool enable_proxy_ = false; 578 | int port_ = 80; 579 | 580 | public: 581 | Request(){}; 582 | ~Request(){}; 583 | 584 | Request& SetHttpMethod(sion::Method method) 585 | { 586 | switch (method) 587 | { 588 | case Method::Get: 589 | method_ = "GET"; 590 | break; 591 | case Method::Post: 592 | method_ = "POST"; 593 | break; 594 | case Method::Put: 595 | method_ = "PUT"; 596 | break; 597 | case Method::Delete: 598 | method_ = "DELETE"; 599 | break; 600 | } 601 | return *this; 602 | } 603 | 604 | Request& SetHttpMethod(String other) 605 | { 606 | method_ = other; 607 | return *this; 608 | } 609 | 610 | Request& SetUrl(String url) 611 | { 612 | url_ = url; 613 | return *this; 614 | } 615 | 616 | Request& SetBody(String body) 617 | { 618 | request_body_.clear(); 619 | PushStr2Vec(body, request_body_); 620 | return *this; 621 | } 622 | 623 | Request& SetBody(Payload::Binary body) 624 | { 625 | request_body_.clear(); 626 | request_body_.insert(request_body_.begin(), body.data.begin(), body.data.end()); 627 | if (body.type != "") 628 | { 629 | request_header_.RemoveAll("Content-Type"); 630 | request_header_.Add("Content-Type", body.type); 631 | } 632 | return *this; 633 | } 634 | 635 | Request& SetBody(Payload::FormData body) 636 | { 637 | request_body_ = body.Serialize(); 638 | request_header_.RemoveAll("Content-Type"); 639 | request_header_.Add("Content-Type", body.GetContentType()); 640 | return *this; 641 | } 642 | 643 | Request& SetHeader(Header header) 644 | { 645 | request_header_ = header; 646 | return *this; 647 | } 648 | 649 | Request& SetHeader(String k, String v) 650 | { 651 | request_header_.Add(k, v); 652 | return *this; 653 | } 654 | 655 | Request& SetProxy(HttpProxy proxy) 656 | { 657 | enable_proxy_ = true; 658 | proxy_ = proxy; 659 | return *this; 660 | } 661 | 662 | Response Send(sion::Method method, String url) 663 | { 664 | SetHttpMethod(method); 665 | return Send(url); 666 | } 667 | 668 | Response Send() { return Send(url_); } 669 | #ifndef SION_DISABLE_SSL 670 | private: 671 | Response SendBySSL(Socket socket) 672 | { 673 | SSL_library_init(); 674 | auto method = TLS_method(); 675 | auto ssl_ctx = SSL_CTX_new(method); 676 | auto ssl = SSL_new(ssl_ctx); 677 | check(ssl != nullptr && ssl_ctx != nullptr, "openssl初始化异常"); 678 | SSL_set_fd(ssl, socket); 679 | SSL_connect(ssl); 680 | SSL_write(ssl, source_.data(), int(source_.size())); 681 | auto resp = ReadResponse(socket, ssl); 682 | SSL_shutdown(ssl); 683 | SSL_free(ssl); 684 | SSL_CTX_free(ssl_ctx); 685 | return resp; 686 | } 687 | 688 | #endif 689 | public: 690 | Response Send(String url) 691 | { 692 | check(method_.length(), "请求方法未定义"); 693 | std::smatch m; 694 | #ifdef SION_DISABLE_SSL 695 | bool enable_ssl = false; 696 | #else 697 | bool enable_ssl = true; 698 | #endif 699 | if (enable_ssl || enable_proxy_) 700 | { 701 | std::regex url_parse(R"(^(http|https)://([\w.-]*):?(\d*)(/?.*)$)"); 702 | regex_match(url, m, url_parse); 703 | check(m.size() == 5, "url格式不对或者是用了除http(s)外的协议"); 704 | protocol_ = m[1]; 705 | port_ = m[3].length() == 0 ? (protocol_ == "http" ? 80 : 443) : stoi(m[3]); 706 | } 707 | else 708 | { 709 | std::regex url_parse(R"(^(http)://([\w.-]*):?(\d*)(/?.*)$)"); 710 | regex_match(url, m, url_parse); 711 | check(m.size() == 5, "url格式不对或者是用了除http外的协议"); 712 | protocol_ = m[1]; 713 | port_ = m[3].length() == 0 ? 80 : stoi(m[3]); 714 | } 715 | host_ = m[2]; 716 | path_ = m[4].length() == 0 ? "/" : m[4].str(); 717 | Socket socket = GetSocket(); 718 | try 719 | { 720 | Connection(socket, host_); 721 | BuildRequestString(); 722 | if (protocol_ == "http" || enable_proxy_) 723 | { 724 | send(socket, source_.data(), static_cast(source_.size()), 0); 725 | return ReadResponse(socket); 726 | } 727 | #ifndef SION_DISABLE_SSL 728 | else // if (protocol_ == "https") 729 | { 730 | return SendBySSL(socket); 731 | } 732 | #endif 733 | } 734 | catch (const std::exception& e) 735 | { 736 | #ifdef _WIN32 737 | WSACleanup(); 738 | #endif 739 | Throw(e.what()); 740 | } 741 | throw Error("unreachable code"); 742 | } 743 | 744 | private: 745 | void BuildRequestString() 746 | { 747 | request_header_.Add("Host", host_); 748 | request_header_.Add("Content-Length", std::to_string(request_body_.size())); 749 | auto request_target = enable_proxy_ ? url_ : path_; 750 | String source_str = method_ + " " + request_target + " " + protocol_version_ + crlf; 751 | for (auto& x : request_header_.Data()) 752 | { 753 | source_str += x.first + ": " + x.second + crlf; 754 | } 755 | source_str += crlf; 756 | PushStr2Vec(source_str, source_); 757 | source_.insert(source_.end(), request_body_.begin(), request_body_.end()); 758 | } 759 | 760 | void Connection(Socket socket, String host) 761 | { 762 | in_addr sa{}; 763 | ip_ = host.HasLetter() ? GetIpByHost(host) : host; 764 | auto target_ip = enable_proxy_ ? (proxy_.host.HasLetter() ? GetIpByHost(proxy_.host) : proxy_.host) : ip_; 765 | check((inet_pton(AF_INET, target_ip.c_str(), &sa) != -1), "地址转换错误"); 766 | sockaddr_in saddr{}; 767 | saddr.sin_family = AF_INET; 768 | saddr.sin_port = htons(enable_proxy_ ? proxy_.port : port_); 769 | saddr.sin_addr = sa; 770 | if (::connect(socket, (sockaddr*)&saddr, sizeof(saddr)) != 0) 771 | { 772 | String err = "连接失败:\n"; 773 | err += "Host:" + host_ + "\n"; 774 | err += "Ip:" + ip_ + "\n"; 775 | if (enable_proxy_) 776 | { 777 | err += "Proxy IP:" + target_ip + "\n"; 778 | err += "Proxy Host:" + proxy_.host + "\n"; 779 | } 780 | #ifdef _WIN32 781 | err += "错误码:" + std::to_string(WSAGetLastError()); 782 | #else 783 | err += String("error str:") + strerror(errno); 784 | #endif 785 | Throw(err); 786 | } 787 | } 788 | #ifndef SION_DISABLE_SSL 789 | Response ReadResponse(Socket socket, SSL* ssl = nullptr) 790 | #else 791 | Response ReadResponse(Socket socket) 792 | #endif 793 | { 794 | const int buf_size = 2048; 795 | std::array buf{0}; 796 | auto Read = [&]() { 797 | buf.fill(0); 798 | int status = 0; 799 | if (protocol_ == "http" || enable_proxy_) 800 | { 801 | status = recv(socket, buf.data(), buf_size - 1, 0); 802 | } 803 | #ifndef SION_DISABLE_SSL 804 | else if (protocol_ == "https") 805 | { 806 | status = SSL_read(ssl, buf.data(), buf_size - 1); 807 | } 808 | #endif 809 | check(status != 0); 810 | check(status > 0, "网络异常,Socket错误码:" + std::to_string(status)); 811 | return status; 812 | }; 813 | Response resp; 814 | auto read_count = 0; 815 | // 读取解析头部信息 816 | while (true) 817 | { 818 | read_count = Read(); 819 | if (read_count > 0) 820 | { 821 | resp.source_.insert(resp.source_.end(), buf.data(), buf.data() + read_count); 822 | } 823 | if (resp.CanParseHeader()) 824 | { 825 | break; 826 | } 827 | } 828 | 829 | resp.ParseHeader(); 830 | // 检查是否接收完 831 | auto check_end = [&] { 832 | const auto& body = resp.Body(); 833 | if (resp.is_chunked_) 834 | { 835 | if (body.size() < 7) 836 | { 837 | return false; 838 | } 839 | auto chunked_end_offset = body.size() - 4; 840 | auto chunked_end_iter = body.begin() + chunked_end_offset; 841 | auto chunked_end = std::string(chunked_end_iter, chunked_end_iter + 4); 842 | if (chunked_end != crlf_crlf) 843 | { 844 | return false; 845 | } 846 | auto chunked_start_offset = chunked_end_offset - 1; 847 | // 有些不是\r\n0\r\n\r\n 而是\r\n000000\r\n\r\n 848 | for (auto& i = chunked_start_offset; i >= 2; i--) 849 | { 850 | auto r = body[i]; 851 | if (r != '0') 852 | { 853 | break; 854 | } 855 | if (body[i - 1] == '\n' && body[i - 2] == '\r') 856 | { 857 | return true; 858 | } 859 | } 860 | } 861 | else 862 | { 863 | return body.size() == resp.content_length_; 864 | } 865 | return false; 866 | }; 867 | // 循环读取接收 868 | while (!check_end()) 869 | { 870 | read_count = Read(); 871 | resp.body_.insert(resp.body_.end(), buf.begin(), buf.begin() + read_count); 872 | } 873 | #ifdef _WIN32 874 | closesocket(socket); 875 | WSACleanup(); 876 | #else 877 | close(socket); 878 | #endif 879 | resp.ParseBody(); 880 | return resp; 881 | } 882 | }; 883 | 884 | static Response Fetch(String url, Method method = Method::Get, Header header = Header(), String body = "") 885 | { 886 | return Request().SetUrl(url).SetHttpMethod(method).SetHeader(header).SetBody(body).Send(); 887 | } 888 | enum AsyncResponseReceiveMode 889 | { 890 | callback, 891 | queue, 892 | future 893 | }; 894 | 895 | struct AsyncResponse 896 | { 897 | Response resp; 898 | int id{}; 899 | String err_msg; 900 | }; 901 | 902 | struct AsyncPackage 903 | { 904 | Request request; 905 | std::function callback; 906 | int id{}; 907 | AsyncResponseReceiveMode received_mode{}; 908 | }; 909 | 910 | class Async 911 | { 912 | int thread_num_ = 6; 913 | std::queue queue_; 914 | std::mutex m_; 915 | std::condition_variable cv_; 916 | std::condition_variable waiting_resp_cv_; 917 | std::map threads_; 918 | bool running_ = false; 919 | bool throw_if_has_err_msg = false; 920 | std::atomic_bool stopped_ = false; 921 | std::atomic_int incr_id = 0; 922 | bool is_block_ = false; 923 | std::mutex waiting_resp_queue_mutex_; 924 | std::vector waiting_handle_response_; 925 | 926 | public: 927 | ~Async() 928 | { 929 | stopped_ = true; 930 | cv_.notify_all(); 931 | std::unique_lock lk(m_); 932 | auto& ts = threads_; 933 | cv_.wait(lk, [&] { return ts.empty(); }); // 等待所有线程退出 934 | } 935 | Async& SetThreadNum(int num) 936 | { 937 | thread_num_ = num; 938 | return *this; 939 | } 940 | Async& SetBlock(bool wait) 941 | { 942 | is_block_ = wait; 943 | return *this; 944 | } 945 | Async& SetThrowIfHasErrMsg(bool op) 946 | { 947 | throw_if_has_err_msg = op; 948 | return *this; 949 | } 950 | 951 | AsyncResponse GetTargetResp(int id) 952 | { 953 | auto& queue = waiting_handle_response_; 954 | for (size_t i = 0; i < queue.size(); i++) 955 | { 956 | auto& target = queue[i]; 957 | if (target.id == id) 958 | { 959 | auto r = queue[i]; 960 | if (throw_if_has_err_msg && r.err_msg.size()) 961 | { 962 | Throw(r.err_msg); 963 | } 964 | queue.erase(queue.begin() + i); 965 | return r; 966 | } 967 | } 968 | throw Error("unreachable code"); 969 | } 970 | 971 | AsyncResponse Await(int id, int timeout_ms = 0) 972 | { 973 | std::unique_lock lk(waiting_resp_queue_mutex_); 974 | auto& queue = waiting_handle_response_; 975 | auto check = [&] { 976 | for (auto& i : queue) 977 | { 978 | if (i.id == id) 979 | { 980 | return true; 981 | } 982 | } 983 | return false; 984 | }; 985 | if (check()) 986 | { 987 | return GetTargetResp(id); 988 | } 989 | if (timeout_ms != 0) 990 | { 991 | waiting_resp_cv_.wait_for(lk, std::chrono::milliseconds(timeout_ms), check); 992 | if (!check()) 993 | { 994 | throw AsyncAwaitTimeout(); 995 | } 996 | } 997 | else 998 | { 999 | waiting_resp_cv_.wait(lk, check); 1000 | } 1001 | return GetTargetResp(id); 1002 | } 1003 | 1004 | void Start() 1005 | { 1006 | check(!running_, "一个线程池实例只能start一次"); 1007 | for (int i = 0; i < thread_num_; i++) 1008 | { 1009 | threads_[i] = std::thread([&, i] { AsyncLoop(i); }); 1010 | } 1011 | running_ = true; 1012 | if (is_block_) 1013 | { 1014 | for (auto& i : threads_) 1015 | { 1016 | i.second.join(); 1017 | } 1018 | } 1019 | else 1020 | { 1021 | for (auto& i : threads_) 1022 | { 1023 | i.second.detach(); 1024 | } 1025 | } 1026 | } 1027 | 1028 | int Run(std::function fn) 1029 | { 1030 | auto id = ++incr_id; 1031 | { 1032 | std::lock_guard lock(m_); 1033 | AsyncPackage pkg; 1034 | pkg.request = fn(); 1035 | pkg.id = id; 1036 | pkg.received_mode = AsyncResponseReceiveMode::queue; 1037 | queue_.push(pkg); 1038 | } 1039 | cv_.notify_one(); 1040 | return id; 1041 | } 1042 | 1043 | void Run(std::function fn, std::function cb) 1044 | { 1045 | { 1046 | std::lock_guard lock(m_); 1047 | AsyncPackage pkg; 1048 | pkg.callback = cb; 1049 | pkg.request = fn(); 1050 | pkg.received_mode = AsyncResponseReceiveMode::callback; 1051 | queue_.push(pkg); 1052 | } 1053 | cv_.notify_one(); 1054 | } 1055 | 1056 | std::vector GetAvailableResponse() 1057 | { 1058 | std::lock_guard m(waiting_resp_queue_mutex_); 1059 | auto available_resp_queue = waiting_handle_response_; 1060 | waiting_handle_response_ = {}; 1061 | return available_resp_queue; 1062 | } 1063 | 1064 | private: 1065 | void AsyncLoop(int id) 1066 | { 1067 | AsyncPackage pkg; 1068 | while (!stopped_.load()) 1069 | { 1070 | std::unique_lock lk(m_); 1071 | cv_.wait(lk, [&] { return !queue_.empty() || stopped_.load(); }); 1072 | if (stopped_.load()) 1073 | { 1074 | break; 1075 | } 1076 | pkg = queue_.front(); 1077 | queue_.pop(); 1078 | lk.unlock(); // 提前解锁,不等析构再解,防止notify_one拉起后又马上阻塞 1079 | cv_.notify_one(); 1080 | Response resp; 1081 | String err_msg; 1082 | if (stopped_.load()) 1083 | { 1084 | break; 1085 | } 1086 | try 1087 | { 1088 | resp = pkg.request.Send(); 1089 | } 1090 | catch (const std::exception& e) 1091 | { 1092 | err_msg = e.what(); 1093 | } 1094 | if (stopped_.load()) 1095 | { 1096 | break; 1097 | } 1098 | AsyncResponse resp_pkg; 1099 | resp_pkg.id = pkg.id; 1100 | resp_pkg.resp = resp; 1101 | resp_pkg.err_msg = err_msg; 1102 | if (pkg.received_mode == AsyncResponseReceiveMode::queue) 1103 | { 1104 | std::lock_guard m(waiting_resp_queue_mutex_); 1105 | waiting_handle_response_.push_back(resp_pkg); 1106 | waiting_resp_cv_.notify_all(); 1107 | } 1108 | else if (pkg.received_mode == AsyncResponseReceiveMode::callback) 1109 | { 1110 | try 1111 | { 1112 | pkg.callback(resp_pkg); 1113 | } 1114 | catch (const std::exception& e) 1115 | { 1116 | std::cerr << e.what() << '\n'; 1117 | } 1118 | } 1119 | } 1120 | { 1121 | std::lock_guard m(m_); 1122 | threads_.erase(id); 1123 | cv_.notify_one(); 1124 | } 1125 | } 1126 | }; 1127 | 1128 | } // namespace sion 1129 | --------------------------------------------------------------------------------