init: tcp connect latency and jitter tester
This commit is contained in:
commit
37c8aaab80
5 changed files with 178 additions and 0 deletions
14
.github/workflows/ci.yml
vendored
Normal file
14
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- run: cmake -B build
|
||||
- run: cmake --build build --config Release
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
build/
|
||||
.claude/
|
||||
*.obj
|
||||
*.exe
|
||||
*.pdb
|
||||
*.ilk
|
||||
11
CMakeLists.txt
Normal file
11
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(pingpp CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_executable(pingpp src/main.cpp)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(pingpp ws2_32)
|
||||
endif()
|
||||
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# pingpp
|
||||
|
||||
меряет задержку и джиттер до хостов через обычный TCP-connect, без ICMP и прав администратора. удобно быстро глянуть какая VPN-нода бодрее.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
loop 5 попыток
|
||||
pingpp->>Host: TCP connect
|
||||
Host-->>pingpp: SYN-ACK
|
||||
Note over pingpp: засекли время
|
||||
end
|
||||
pingpp->>pingpp: avg / min / max / jitter
|
||||
```
|
||||
|
||||
## сборка
|
||||
|
||||
```
|
||||
cmake -B build
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
## юзать
|
||||
|
||||
```
|
||||
build\Release\pingpp.exe 1.1.1.1:443 vpn.example.com:51820
|
||||
```
|
||||
121
src/main.cpp
Normal file
121
src/main.cpp
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#define NOMINMAX
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
|
||||
struct Target {
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
};
|
||||
|
||||
static bool parse_target(const std::string& s, Target& t) {
|
||||
size_t colon = s.rfind(':');
|
||||
if (colon == std::string::npos) return false;
|
||||
t.host = s.substr(0, colon);
|
||||
try {
|
||||
t.port = static_cast<uint16_t>(std::stoi(s.substr(colon + 1)));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool tcp_connect_once(const Target& t, double& ms) {
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo* res = nullptr;
|
||||
std::string portstr = std::to_string(t.port);
|
||||
if (getaddrinfo(t.host.c_str(), portstr.c_str(), &hints, &res) != 0 || !res) return false;
|
||||
|
||||
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (s == INVALID_SOCKET) { freeaddrinfo(res); return false; }
|
||||
|
||||
u_long nonblock = 1;
|
||||
ioctlsocket(s, FIONBIO, &nonblock);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
int rc = connect(s, res->ai_addr, static_cast<int>(res->ai_addrlen));
|
||||
bool ok = false;
|
||||
|
||||
if (rc == 0) {
|
||||
ok = true;
|
||||
} else if (WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(s, &writefds);
|
||||
timeval tv{};
|
||||
tv.tv_sec = 2;
|
||||
tv.tv_usec = 0;
|
||||
if (select(0, nullptr, &writefds, nullptr, &tv) > 0) {
|
||||
int err = 0;
|
||||
int errlen = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &errlen);
|
||||
ok = (err == 0);
|
||||
}
|
||||
}
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
ms = std::chrono::duration<double, std::milli>(end - start).count();
|
||||
|
||||
closesocket(s);
|
||||
freeaddrinfo(res);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "usage: pingpp <host:port> [host:port ...]" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
WSADATA wsa;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
|
||||
std::cerr << "WSAStartup failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int attempts = 5;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
Target t;
|
||||
if (!parse_target(argv[i], t)) {
|
||||
std::cout << argv[i] << ": bad format, use host:port" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<double> times;
|
||||
for (int a = 0; a < attempts; ++a) {
|
||||
double ms;
|
||||
if (tcp_connect_once(t, ms)) times.push_back(ms);
|
||||
}
|
||||
|
||||
std::cout << t.host << ":" << t.port << " -> ";
|
||||
if (times.empty()) {
|
||||
std::cout << "no response" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
double sum = 0, mn = times[0], mx = times[0];
|
||||
for (double v : times) { sum += v; mn = std::min(mn, v); mx = std::max(mx, v); }
|
||||
double avg = sum / times.size();
|
||||
|
||||
double var = 0;
|
||||
for (double v : times) var += (v - avg) * (v - avg);
|
||||
double jitter = std::sqrt(var / times.size());
|
||||
|
||||
std::cout << times.size() << "/" << attempts << " ok, avg " << avg << " ms, "
|
||||
<< "min " << mn << " ms, max " << mx << " ms, jitter " << jitter << " ms" << std::endl;
|
||||
}
|
||||
|
||||
WSACleanup();
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue