mirror of
https://github.com/henrywhitaker3/Speedtest-Tracker.git
synced 2025-12-21 13:23:04 +01:00
553 lines
17 KiB
C++
553 lines
17 KiB
C++
//
|
|
// Created by Francesco Laurita on 5/29/16.
|
|
//
|
|
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include "SpeedTest.h"
|
|
#include "MD5Util.h"
|
|
#include <netdb.h>
|
|
|
|
SpeedTest::SpeedTest(float minServerVersion):
|
|
mLatency(0),
|
|
mUploadSpeed(0),
|
|
mDownloadSpeed(0) {
|
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
|
mIpInfo = IPInfo();
|
|
mServerList = std::vector<ServerInfo>();
|
|
mMinSupportedServer = minServerVersion;
|
|
}
|
|
|
|
SpeedTest::~SpeedTest() {
|
|
curl_global_cleanup();
|
|
mServerList.clear();
|
|
}
|
|
|
|
bool SpeedTest::ipInfo(IPInfo& info) {
|
|
|
|
if (!mIpInfo.ip_address.empty()){
|
|
info = mIpInfo;
|
|
return true;
|
|
}
|
|
|
|
std::stringstream oss;
|
|
auto code = httpGet(SPEED_TEST_IP_INFO_API_URL, oss);
|
|
if (code == CURLE_OK){
|
|
auto values = SpeedTest::parseQueryString(oss.str());
|
|
mIpInfo = IPInfo();
|
|
mIpInfo.ip_address = values["ip_address"];
|
|
mIpInfo.isp = values["isp"];
|
|
mIpInfo.lat = std::stof(values["lat"]);
|
|
mIpInfo.lon = std::stof(values["lon"]);
|
|
values.clear();
|
|
oss.clear();
|
|
info = mIpInfo;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const std::vector<ServerInfo>& SpeedTest::serverList() {
|
|
if (!mServerList.empty())
|
|
return mServerList;
|
|
|
|
int http_code = 0;
|
|
if (fetchServers(SPEED_TEST_SERVER_LIST_URL, mServerList, http_code) && http_code == 200){
|
|
return mServerList;
|
|
}
|
|
return mServerList;
|
|
}
|
|
|
|
|
|
const ServerInfo SpeedTest::bestServer(const int sample_size, std::function<void(bool)> cb) {
|
|
auto best = findBestServerWithin(serverList(), mLatency, sample_size, cb);
|
|
SpeedTestClient client = SpeedTestClient(best);
|
|
testLatency(client, SPEED_TEST_LATENCY_SAMPLE_SIZE, mLatency);
|
|
client.close();
|
|
return best;
|
|
}
|
|
|
|
bool SpeedTest::setServer(ServerInfo& server){
|
|
SpeedTestClient client = SpeedTestClient(server);
|
|
if (client.connect() && client.version() >= mMinSupportedServer){
|
|
if (!testLatency(client, SPEED_TEST_LATENCY_SAMPLE_SIZE, mLatency)){
|
|
return false;
|
|
}
|
|
} else {
|
|
client.close();
|
|
return false;
|
|
}
|
|
client.close();
|
|
return true;
|
|
|
|
}
|
|
|
|
bool SpeedTest::downloadSpeed(const ServerInfo &server, const TestConfig &config, double& result, std::function<void(bool)> cb) {
|
|
opFn pfunc = &SpeedTestClient::download;
|
|
mDownloadSpeed = execute(server, config, pfunc, cb);
|
|
result = mDownloadSpeed;
|
|
return true;
|
|
}
|
|
|
|
bool SpeedTest::uploadSpeed(const ServerInfo &server, const TestConfig &config, double& result, std::function<void(bool)> cb) {
|
|
opFn pfunc = &SpeedTestClient::upload;
|
|
mUploadSpeed = execute(server, config, pfunc, cb);
|
|
result = mUploadSpeed;
|
|
return true;
|
|
}
|
|
|
|
const long &SpeedTest::latency() {
|
|
return mLatency;
|
|
}
|
|
|
|
bool SpeedTest::jitter(const ServerInfo &server, long& result, const int sample) {
|
|
auto client = SpeedTestClient(server);
|
|
double current_jitter = 0;
|
|
long previous_ms = LONG_MAX;
|
|
if (client.connect()){
|
|
for (int i = 0; i < sample; i++){
|
|
long ms = 0;
|
|
if (client.ping(ms)){
|
|
if (previous_ms == LONG_MAX) {
|
|
previous_ms = ms;
|
|
} else {
|
|
current_jitter += std::abs(previous_ms - ms);
|
|
}
|
|
}
|
|
}
|
|
client.close();
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
result = (long) std::floor(current_jitter / sample);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool SpeedTest::share(const ServerInfo& server, std::string& image_url) {
|
|
std::stringstream hash;
|
|
hash << std::setprecision(0) << std::fixed << mLatency
|
|
<< "-" << std::setprecision(2) << std::fixed << (mUploadSpeed * 1000)
|
|
<< "-" << std::setprecision(2) << std::fixed << (mDownloadSpeed * 1000)
|
|
<< "-" << SPEED_TEST_API_KEY;
|
|
std::string hex_digest = MD5Util::hexDigest(hash.str());
|
|
std::stringstream post_data;
|
|
post_data << "download=" << std::setprecision(2) << std::fixed << (mDownloadSpeed * 1000) << "&";
|
|
post_data << "ping=" << std::setprecision(0) << std::fixed << mLatency << "&";
|
|
post_data << "upload=" << std::setprecision(2) << std::fixed << (mUploadSpeed * 1000) << "&";
|
|
post_data << "pingselect=1&";
|
|
post_data << "recommendedserverid=" << server.id << "&";
|
|
post_data << "accuracy=1&";
|
|
post_data << "serverid=" << server.id << "&";
|
|
post_data << "hash=";
|
|
post_data << hex_digest;
|
|
|
|
std::stringstream result;
|
|
CURL *c = curl_easy_init();
|
|
curl_easy_setopt(c, CURLOPT_REFERER, SPEED_TEST_API_REFERER);
|
|
auto cres = SpeedTest::httpPost(SPEED_TEST_API_URL, post_data.str(), result, c);
|
|
long http_code = 0;
|
|
image_url.clear();
|
|
if (cres == CURLE_OK){
|
|
curl_easy_getinfo(c, CURLINFO_HTTP_CODE, &http_code);
|
|
if (http_code == 200 && !result.str().empty()){
|
|
auto data = SpeedTest::parseQueryString(result.str());
|
|
if (data.count("resultid") == 1){
|
|
image_url = "http://www.speedtest.net/result/" + data["resultid"] + ".png";
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
curl_easy_cleanup(c);
|
|
return !image_url.empty();
|
|
}
|
|
|
|
// private
|
|
|
|
double SpeedTest::execute(const ServerInfo &server, const TestConfig &config, const opFn &pfunc, std::function<void(bool)> cb) {
|
|
std::vector<std::thread> workers;
|
|
double overall_speed = 0;
|
|
std::mutex mtx;
|
|
for (int i = 0; i < config.concurrency; i++) {
|
|
workers.push_back(std::thread([&server, &overall_speed, &pfunc, &config, &mtx, cb](){
|
|
long start_size = config.start_size;
|
|
long max_size = config.max_size;
|
|
long incr_size = config.incr_size;
|
|
long curr_size = start_size;
|
|
|
|
auto spClient = SpeedTestClient(server);
|
|
|
|
if (spClient.connect()) {
|
|
long total_size = 0;
|
|
long total_time = 0;
|
|
auto start = std::chrono::steady_clock::now();
|
|
std::vector<double> partial_results;
|
|
while (curr_size < max_size){
|
|
long op_time = 0;
|
|
if ((spClient.*pfunc)(curr_size, config.buff_size, op_time)) {
|
|
total_size += curr_size;
|
|
total_time += op_time;
|
|
double metric = (curr_size * 8) / (static_cast<double>(op_time) / 1000);
|
|
partial_results.push_back(metric);
|
|
if (cb)
|
|
cb(true);
|
|
} else {
|
|
if (cb)
|
|
cb(false);
|
|
}
|
|
curr_size += incr_size;
|
|
auto stop = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count() > config.min_test_time_ms)
|
|
break;
|
|
}
|
|
|
|
spClient.close();
|
|
std::sort(partial_results.begin(), partial_results.end());
|
|
|
|
size_t skip = 0;
|
|
size_t drop = 0;
|
|
if (partial_results.size() >= 10){
|
|
skip = partial_results.size() / 4;
|
|
drop = 2;
|
|
}
|
|
|
|
size_t iter = 0;
|
|
double real_sum = 0;
|
|
for (auto it = partial_results.begin() + skip; it != partial_results.end() - drop; ++it ){
|
|
iter++;
|
|
real_sum += (*it);
|
|
}
|
|
mtx.lock();
|
|
overall_speed += (real_sum / iter);
|
|
mtx.unlock();
|
|
} else {
|
|
if (cb)
|
|
cb(false);
|
|
}
|
|
}));
|
|
|
|
}
|
|
for (auto &t : workers){
|
|
t.join();
|
|
}
|
|
|
|
workers.clear();
|
|
|
|
return overall_speed / 1000 / 1000;
|
|
}
|
|
|
|
template<typename T>
|
|
T SpeedTest::deg2rad(T n) {
|
|
return (n * M_PI / 180);
|
|
}
|
|
|
|
template<typename T>
|
|
T SpeedTest::harversine(std::pair<T, T> n1, std::pair<T, T> n2) {
|
|
T lat1r = deg2rad(n1.first);
|
|
T lon1r = deg2rad(n1.second);
|
|
T lat2r = deg2rad(n2.first);
|
|
T lon2r = deg2rad(n2.second);
|
|
T u = std::sin((lat2r - lat1r) / 2);
|
|
T v = std::sin((lon2r - lon1r) / 2);
|
|
return 2.0 * EARTH_RADIUS_KM * std::asin(std::sqrt(u * u + std::cos(lat1r) * std::cos(lat2r) * v * v));
|
|
}
|
|
|
|
CURLcode SpeedTest::httpGet(const std::string &url, std::stringstream &ss, CURL *handler, long timeout) {
|
|
|
|
CURLcode code(CURLE_FAILED_INIT);
|
|
CURL* curl = SpeedTest::curl_setup(handler);
|
|
|
|
|
|
if (curl){
|
|
if (CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_FILE, &ss))
|
|
&& CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout))
|
|
&& CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_URL, url.c_str()))) {
|
|
code = curl_easy_perform(curl);
|
|
}
|
|
if (handler == nullptr)
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
CURLcode SpeedTest::httpPost(const std::string &url, const std::string &postdata, std::stringstream &os, void *handler, long timeout) {
|
|
|
|
CURLcode code(CURLE_FAILED_INIT);
|
|
CURL* curl = SpeedTest::curl_setup(handler);
|
|
|
|
if (curl){
|
|
if (CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_FILE, &os))
|
|
&& CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout))
|
|
&& CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_URL, url.c_str()))
|
|
&& CURLE_OK == (code = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata.c_str()))) {
|
|
code = curl_easy_perform(curl);
|
|
}
|
|
if (handler == nullptr)
|
|
curl_easy_cleanup(curl);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
CURL *SpeedTest::curl_setup(CURL *handler) {
|
|
CURL* curl = handler == nullptr ? curl_easy_init() : handler;
|
|
if (curl){
|
|
if (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &writeFunc) == CURLE_OK
|
|
&& curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L) == CURLE_OK
|
|
&& curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L) == CURLE_OK
|
|
&& curl_easy_setopt(curl, CURLOPT_USERAGENT, SPEED_TEST_USER_AGENT) == CURLE_OK){
|
|
return curl;
|
|
} else {
|
|
curl_easy_cleanup(handler);
|
|
return nullptr;
|
|
}
|
|
}
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
size_t SpeedTest::writeFunc(void *buf, size_t size, size_t nmemb, void *userp) {
|
|
|
|
if (userp){
|
|
std::stringstream &os = *static_cast<std::stringstream *>(userp);
|
|
std::streamsize len = size * nmemb;
|
|
if(os.write(static_cast<char*>(buf), len))
|
|
return static_cast<size_t>(len);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::map<std::string, std::string> SpeedTest::parseQueryString(const std::string &query) {
|
|
auto map = std::map<std::string, std::string>();
|
|
auto pairs = splitString(query, '&');
|
|
for (auto &p : pairs){
|
|
auto kv = splitString(p, '=');
|
|
if (kv.size() == 2){
|
|
map[kv[0]] = kv[1];
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
std::vector<std::string> SpeedTest::splitString(const std::string &instr, const char separator) {
|
|
if (instr.empty())
|
|
return std::vector<std::string>();
|
|
|
|
std::vector<std::string> tokens;
|
|
std::size_t start = 0, end = 0;
|
|
while ((end = instr.find(separator, start)) != std::string::npos) {
|
|
std::string temp = instr.substr(start, end - start);
|
|
if (!temp.empty())
|
|
tokens.push_back(temp);
|
|
start = end + 1;
|
|
}
|
|
std::string temp = instr.substr(start);
|
|
if (!temp.empty())
|
|
tokens.push_back(temp);
|
|
return tokens;
|
|
|
|
}
|
|
|
|
ServerInfo SpeedTest::processServerXMLNode(xmlTextReaderPtr reader) {
|
|
|
|
auto name = xmlTextReaderConstName(reader);
|
|
auto nodeName = std::string((char*)name);
|
|
|
|
if (!name || nodeName != "server"){
|
|
return ServerInfo();
|
|
}
|
|
|
|
if (xmlTextReaderAttributeCount(reader) > 0){
|
|
auto info = ServerInfo();
|
|
auto server_url = xmlTextReaderGetAttribute(reader, BAD_CAST "url");
|
|
auto server_lat = xmlTextReaderGetAttribute(reader, BAD_CAST "lat");
|
|
auto server_lon = xmlTextReaderGetAttribute(reader, BAD_CAST "lon");
|
|
auto server_name = xmlTextReaderGetAttribute(reader, BAD_CAST "name");
|
|
auto server_county = xmlTextReaderGetAttribute(reader, BAD_CAST "country");
|
|
auto server_cc = xmlTextReaderGetAttribute(reader, BAD_CAST "cc");
|
|
auto server_host = xmlTextReaderGetAttribute(reader, BAD_CAST "host");
|
|
auto server_id = xmlTextReaderGetAttribute(reader, BAD_CAST "id");
|
|
auto server_sponsor = xmlTextReaderGetAttribute(reader, BAD_CAST "sponsor");
|
|
|
|
if (server_name)
|
|
info.name.append((char*)server_name);
|
|
|
|
if (server_url)
|
|
info.url.append((char*)server_url);
|
|
|
|
if (server_county)
|
|
info.country.append((char*)server_county);
|
|
|
|
if (server_cc)
|
|
info.country_code.append((char*)server_cc);
|
|
|
|
if (server_host)
|
|
info.host.append((char*)server_host);
|
|
|
|
if (server_sponsor)
|
|
info.sponsor.append((char*)server_sponsor);
|
|
|
|
if (server_id)
|
|
info.id = std::atoi((char*)server_id);
|
|
|
|
if (server_lat)
|
|
info.lat = std::stof((char*)server_lat);
|
|
|
|
if (server_lon)
|
|
info.lon = std::stof((char*)server_lon);
|
|
|
|
xmlFree(server_url);
|
|
xmlFree(server_lat);
|
|
xmlFree(server_lon);
|
|
xmlFree(server_name);
|
|
xmlFree(server_county);
|
|
xmlFree(server_cc);
|
|
xmlFree(server_host);
|
|
xmlFree(server_id);
|
|
xmlFree(server_sponsor);
|
|
return info;
|
|
}
|
|
|
|
return ServerInfo();
|
|
}
|
|
|
|
bool SpeedTest::fetchServers(const std::string& url, std::vector<ServerInfo>& target, int &http_code) {
|
|
std::stringstream oss;
|
|
target.clear();
|
|
|
|
auto isHttpSchema = url.find_first_of("http") == 0;
|
|
|
|
CURL* curl = curl_easy_init();
|
|
auto cres = httpGet(url, oss, curl, 20);
|
|
|
|
if (cres != CURLE_OK)
|
|
return false;
|
|
|
|
if (isHttpSchema) {
|
|
int req_status;
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &req_status);
|
|
http_code = req_status;
|
|
|
|
if (http_code != 200){
|
|
curl_easy_cleanup(curl);
|
|
return false;
|
|
}
|
|
} else {
|
|
http_code = 200;
|
|
}
|
|
|
|
size_t len = oss.str().length();
|
|
auto *xmlbuff = (char*)calloc(len + 1, sizeof(char));
|
|
if (!xmlbuff){
|
|
std::cerr << "Unable to calloc" << std::endl;
|
|
curl_easy_cleanup(curl);
|
|
return false;
|
|
}
|
|
memcpy(xmlbuff, oss.str().c_str(), len);
|
|
oss.str("");
|
|
|
|
xmlTextReaderPtr reader = xmlReaderForMemory(xmlbuff, static_cast<int>(len), nullptr, nullptr, 0);
|
|
|
|
if (reader != nullptr) {
|
|
IPInfo ipInfo;
|
|
if (!SpeedTest::ipInfo(ipInfo)){
|
|
curl_easy_cleanup(curl);
|
|
free(xmlbuff);
|
|
xmlFreeTextReader(reader);
|
|
std::cerr << "OOPS!" <<std::endl;
|
|
return false;
|
|
}
|
|
auto ret = xmlTextReaderRead(reader);
|
|
while (ret == 1) {
|
|
ServerInfo info = processServerXMLNode(reader);
|
|
if (!info.url.empty()){
|
|
info.distance = harversine(std::make_pair(ipInfo.lat, ipInfo.lon), std::make_pair(info.lat, info.lon));
|
|
target.push_back(info);
|
|
}
|
|
ret = xmlTextReaderRead(reader);
|
|
}
|
|
xmlFreeTextReader(reader);
|
|
if (ret != 0) {
|
|
curl_easy_cleanup(curl);
|
|
free(xmlbuff);
|
|
std::cerr << "Failed to parse" << std::endl;
|
|
return false;
|
|
}
|
|
} else {
|
|
std::cerr << "Unable to initialize xml parser" << std::endl;
|
|
curl_easy_cleanup(curl);
|
|
free(xmlbuff);
|
|
return false;
|
|
}
|
|
|
|
curl_easy_cleanup(curl);
|
|
free(xmlbuff);
|
|
xmlCleanupParser();
|
|
std::sort(target.begin(), target.end(), [](const ServerInfo &a, const ServerInfo &b) -> bool {
|
|
return a.distance < b.distance;
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const ServerInfo SpeedTest::findBestServerWithin(const std::vector<ServerInfo> &serverList, long &latency,
|
|
const int sample_size, std::function<void(bool)> cb) {
|
|
int i = sample_size;
|
|
ServerInfo bestServer = serverList[0];
|
|
|
|
latency = INT_MAX;
|
|
|
|
for (auto &server : serverList){
|
|
auto client = SpeedTestClient(server);
|
|
|
|
if (!client.connect()){
|
|
if (cb)
|
|
cb(false);
|
|
continue;
|
|
}
|
|
|
|
if (client.version() < mMinSupportedServer){
|
|
client.close();
|
|
continue;
|
|
}
|
|
|
|
long current_latency = LONG_MAX;
|
|
if (testLatency(client, 20, current_latency)){
|
|
if (current_latency < latency){
|
|
latency = current_latency;
|
|
bestServer = server;
|
|
}
|
|
}
|
|
client.close();
|
|
if (cb)
|
|
cb(true);
|
|
|
|
if (i-- < 0){
|
|
break;
|
|
}
|
|
|
|
}
|
|
return bestServer;
|
|
}
|
|
|
|
bool SpeedTest::testLatency(SpeedTestClient &client, const int sample_size, long &latency) {
|
|
if (!client.connect()){
|
|
return false;
|
|
}
|
|
latency = INT_MAX;
|
|
long temp_latency = 0;
|
|
for (int i = 0; i < sample_size; i++){
|
|
if (client.ping(temp_latency)){
|
|
if (temp_latency < latency){
|
|
latency = temp_latency;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|