Una biblioteca HTTP/HTTPS multiplataforma de encabezado de archivo único C++11.
Es extremadamente fácil de configurar. ¡Simplemente incluya el archivo httplib.h en su código!
Importante
Esta biblioteca utiliza E/S de socket de "bloqueo". Si está buscando una biblioteca con E/S de socket "sin bloqueo", esta no es la que desea.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "ruta/a/httplib.h"// HTTPhttplib::Server svr;// HTTPShttplib::SSLServer svr; svr.Get("/hola", [](const httplib::Solicitud &, httplib::Respuesta &res) { res.set_content("¡Hola mundo!", "texto/plain"); }); svr.listen("0.0.0.0", 8080);
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "ruta/a/httplib.h"// HTTPhttplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");// HTTPShttplib::Client cli(" https://cpp-httplib-server.yhirose.repl.co");auto res = cli.Get("/hi"); res->estado; res->cuerpo;
La compatibilidad con SSL está disponible con CPPHTTPLIB_OPENSSL_SUPPORT
. libssl
y libcrypto
deben estar vinculados.
Nota
Actualmente, cpp-httplib solo admite la versión 3.0 o posterior. Por favor consulte esta página para obtener más información.
Consejo
Para macOS: cpp-httplib ahora puede usar certificados del sistema con CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
. CoreFoundation
y Security
deben estar vinculados con -framework
.
#define CPPHTTPLIB_OPENSSL_SUPPORT#include "path/to/httplib.h"// Serverhttplib::SSLServer svr("./cert.pem", "./key.pem");// Clienthttplib::Client cli("https: //localhost:1234"); // esquema + hosthttplib::SSLClient cli("localhost:1234"); // hosthttplib::SSLClient cli("localhost", 1234); // host, puerto// Utilice su paquete CAcli.set_ca_cert_path("./ca-bundle.crt");// Deshabilite la verificación de certificadoscli.enable_server_certificate_verification(false);// Deshabilite la verificación de hostcli.enable_server_host_verification(false);
Nota
Cuando se utiliza SSL, parece imposible evitar SIGPIPE en todos los casos, ya que en algunos sistemas operativos, SIGPIPE sólo se puede suprimir por mensaje, pero no hay forma de hacer que la biblioteca OpenSSL lo haga para sus comunicaciones internas. Si su programa necesita evitar ser terminado en SIGPIPE, la única forma completamente general podría ser configurar un manejador de señales para que SIGPIPE lo maneje o lo ignore usted mismo.
#incluye <httplib.h>int principal(vacío) { usando el espacio de nombres httplib; servidor svr; svr.Get("/hola", [](const Solicitud y solicitud, Respuesta y resolución) { res.set_content("¡Hola mundo!", "texto/plain"); }); // Compara la ruta de la solicitud con una expresión regular // y extrae sus capturas svr.Get(R"(/numbers/(d+))", [&](const Request& req, Response& res) { números automáticos = req.matches[1]; res.set_content(números, "texto/sin formato"); }); // Captura el segundo segmento de la ruta de solicitud como parámetro de ruta "id" svr.Get("/users/:id", [&](const Request& req, Response& res) { auto user_id = req.path_params.at("id"); res.set_content(user_id, "texto/sin formato"); }); // Extrae valores de encabezados HTTP y parámetros de consulta de URL svr.Get("/body-header-param", [](const Request& req, Response& res) { if (req.has_header("Content-Length")) { auto val = req.get_header_value("Content-Length" ); } if (req.has_param("clave")) { auto val = req.get_param_value("clave"); } res.set_content(req.body, "texto/sin formato"); }); svr.Get("/stop", [&](const Solicitud y solicitud, Respuesta y resolución) { svr.stop(); }); svr.listen("localhost", 1234); }
También se admiten los métodos Post
, Put
, Delete
y Options
.
int puerto = svr.bind_to_any_port("0.0.0.0"); svr.listen_after_bind();
// Montar / en ./www directorioauto ret = svr.set_mount_point("/", "./www");if (!ret) { // El directorio base especificado no existe...}// Montar / público a ./www directorioret = svr.set_mount_point("/public", "./www");// Montar /public a ./www1 y ./www2 directoriosret = svr.set_mount_point("/public", "./www1"); // 1er orden de búsqueda = svr.set_mount_point("/public", "./www2"); // Segundo orden de búsqueda// Eliminar montaje /ret = svr.remove_mount_point("/");// Eliminar montaje /publicret = svr.remove_mount_point("/public");
// Extensión de archivo definida por el usuario y mapeo de tipo MIMEssvr.set_file_extension_and_mimetype_mapping("cc", "text/xc"); svr.set_file_extension_and_mimetype_mapping("cpp", "text/xc"); svr.set_file_extension_and_mimetype_mapping("hh", "texto/xh");
Las siguientes son asignaciones integradas:
Extensión | Tipo MIME | Extensión | Tipo MIME |
---|---|---|---|
CSS | texto/css | MPGA | audio/mpeg |
csv | texto/csv | webá | audio/webm |
TXT | texto/sin formato | wav | audio/onda |
vtt | texto/vtt | otf | fuente/otf |
html, htm | texto/html | ttf | fuente/ttf |
apng | imagen/apng | guau | fuente/woff |
avif | imagen/avif | woff2 | fuente/woff2 |
bmp | imagen/bmp | 7z | aplicación/x-7z-comprimido |
gif | imagen/gif | átomo | aplicación/átomo+xml |
png | imagen/png | solicitud/pdf | |
svg | imagen/svg+xml | mjs, js | aplicación/javascript |
webp | imagen/webp | json | aplicación/json |
ico | imagen/icono x | rss | aplicación/rss+xml |
tif | imagen/tif | alquitrán | aplicación/x-tar |
pelea | imagen/tif | xhtml, xht | aplicación/xhtml+xml |
jpeg, jpg | imagen/jpeg | xslt | aplicación/xslt+xml |
mp4 | vídeo/mp4 | XML | aplicación/xml |
MPEG | vídeo/mpeg | gz | aplicación/gzip |
webm | vídeo/webm | cremallera | aplicación/zip |
mp3 | audio/mp3 | estaba | aplicación/wasm |
Advertencia
Estos métodos de servidor de archivos estáticos no son seguros para subprocesos.
// Se llama al controlador justo antes de que se envíe la respuesta a clientesvr.set_file_request_handler([](const Solicitud &req, Respuesta &res) { ... });
svr.set_logger([](const auto& req, const auto& res) { your_logger(req, res); });
svr.set_error_handler([](const auto& req, auto& res) { auto fmt = "<p>Estado de error: <span style='color:red;'>%d</span></p>"; char buf [BUFSIZ]; snprintf(buf, tamaño de(buf), fmt, res.status); res.set_content(buf, "texto/html"); });
Se llama al controlador de excepciones si un controlador de enrutamiento de usuario arroja un error.
svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) { auto fmt = "<h1>Error 500</h1><p>%s</p>"; char buf[BUFSIZ] ; intente { std::rethrow_exception(ep); } catch (std::excepción &e) { snprintf(buf, sizeof(buf), fmt, e.what()); } catch (...) { // Consulte la siguiente NOTA snprintf(buf, sizeof(buf), fmt, "Unknown Exception"); } res.set_content(buf, "texto/html"); res.status = Código de estado::InternalServerError_500; });
Precaución
Si no proporciona el bloque catch (...)
para un puntero de excepción reiniciado, una excepción no detectada terminará provocando la caída del servidor. ¡Ten cuidado!
svr.set_pre_routing_handler([](const auto& req, auto& res) { if (req.path == "/hola") { res.set_content("mundo", "texto/html"); return Servidor::HandlerResponse::Manejado; } return Servidor::HandlerResponse::Unhandled; });
svr.set_post_routing_handler([](const auto& req, auto& res) { res.set_header("ADDITIONAL_HEADER", "valor"); });
svr.Post("/multipart", [&](const auto& req, auto& res) { auto size = req.files.size(); auto ret = req.has_file("name1"); const auto& file = req. get_file_value("nombre1"); // archivo.nombre de archivo; // archivo.content_type; // archivo.content;});
svr.Post("/content_receiver", [&](const Request &req, Response &res, const ContentReader &content_reader) { if (req.is_multipart_form_data()) { // NOTA: `content_reader` se bloquea hasta que se leen todos los campos de datos del formulario Archivos MultipartFormDataItems; lector de contenido ( [&](const MultipartFormData &archivo) { archivos.push_back(archivo); devolver verdadero; }, [&](const char *datos, tamaño_t longitud_datos) { archivos.back().content.append(datos, longitud_datos); devolver verdadero; }); } demás { std::cuerpo de cadena; content_reader([&](const char *datos, tamaño_t longitud_datos) { body.append(datos, longitud_datos); devolver verdadero; }); } });
tamaño constante_t DATA_CHUNK_SIZE = 4; svr.Get("/stream", [&](const Solicitud &req, Respuesta &res) { datos automáticos = new std::string("abcdefg"); res.set_content_provider( datos->tamaño(), // Longitud del contenido "texto/plain", // Tipo de contenido [&, datos](tamaño_t desplazamiento, tamaño_t longitud, DataSink &sink) { const auto &d = *datos; fregadero.write(&d[desplazamiento], std::min(longitud, DATA_CHUNK_SIZE)); devolver verdadero; // devuelve 'falso' si deseas cancelar el proceso. }, [datos](bool éxito) { eliminar datos; }); });
Sin longitud del contenido:
svr.Get("/stream", [&](const Solicitud &req, Respuesta &res) { res.set_content_provider( "texto/plain", // tipo de contenido [&](size_t offset, DataSink &sink) { if (/* todavía hay datos */) { std::vector<char> datos; // preparar datos... fregadero.write(data.data(), data.size()); } demás { fregadero.hecho(); // No más datos } devuelve verdadero; // devuelve 'falso' si deseas cancelar el proceso. }); });
svr.Get("/fragmentado", [&](const Solicitud y solicitud, Respuesta y resolución) { res.set_chunked_content_provider( "texto/sin formato", [](tamaño_t desplazamiento, DataSink y sumidero) { fregadero.write("123", 3); fregadero.write("345", 3); fregadero.write("789", 3); fregadero.hecho(); // No más datos return true; // devuelve 'falso' si deseas cancelar el proceso. } ); });
Con remolque:
svr.Get("/fragmentado", [&](const Solicitud y solicitud, Respuesta y resolución) { res.set_header("Tráiler", "Dummy1, Dummy2"); res.set_chunked_content_provider( "texto/sin formato", [](tamaño_t desplazamiento, DataSink y sumidero) { fregadero.write("123", 3); fregadero.write("345", 3); fregadero.write("789", 3); fregadero.done_with_trailer({ {"Dummy1", "DummyVal1"}, {"Dummy2", "DummyVal2"} }); devolver verdadero; } ); });
svr.Get("/content", [&](const Solicitud &req, Respuesta &res) { res.set_file_content("./path/to/conent.html"); }); svr.Get("/content", [&](const Solicitud &req, Respuesta &res) { res.set_file_content("./ruta/al/conent", "texto/html"); });
De forma predeterminada, el servidor envía una respuesta 100 Continue
para un encabezado Expect: 100-continue
.
// Enviar una respuesta 'Error de expectativa 417'.svr.set_expect_100_continue_handler([](const Solicitud &req, Respuesta &res) { return StatusCode::ExpectationFailed_417; });
// Enviar un estado final sin leer el mensaje body.svr.set_expect_100_continue_handler([](const Request &req, Response &res) { return res.status = StatusCode::Unauthorized_401; });
svr.set_keep_alive_max_count(2); // El valor predeterminado es 5svr.set_keep_alive_timeout(10); // El valor predeterminado es 5
svr.set_read_timeout(5, 0); // 5 segundossvr.set_write_timeout(5, 0); // 5 segundossvr.set_idle_interval(0, 100000); // 100 milisegundos
svr.set_payload_max_length(1024 * 1024 * 512); // 512MB
Nota
Cuando el tipo de contenido del cuerpo de la solicitud es 'www-form-urlencoded', la longitud real de la carga útil no debe exceder CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH
.
Consulte el ejemplo de servidor y el ejemplo de cliente.
ThreadPool
se utiliza como cola de tareas predeterminada y el número de subprocesos predeterminado es 8, o std::thread::hardware_concurrency()
. Puedes cambiarlo con CPPHTTPLIB_THREAD_POOL_COUNT
.
Si desea configurar el número de subprocesos en tiempo de ejecución, no existe una manera conveniente... Pero aquí se explica cómo.
svr.new_task_queue = [] { devolver nuevo ThreadPool(12); };
También puede proporcionar un parámetro opcional para limitar el número máximo de solicitudes pendientes, es decir, solicitudes accept()
editadas por el oyente pero que aún esperan ser atendidas por los subprocesos de trabajo.
svr.new_task_queue = [] { return new ThreadPool(/*num_threads=*/12, /*max_queued_requests=*/18); };
El límite predeterminado es 0 (ilimitado). Una vez que se alcanza el límite, el oyente cerrará la conexión del cliente.
Puede proporcionar su propia implementación de grupo de subprocesos según sus necesidades.
clase YourThreadPoolTaskQueue: pública TaskQueue {público: YourThreadPoolTaskQueue (tamaño_t n) { pool_.start_with_thread_count(n); } virtual bool enqueue(std::function<void()> fn) override { /* Devuelve verdadero si la tarea realmente estaba en cola, o falso * si la persona que llama debe desconectar la conexión correspondiente. */ return pool_.enqueue(fn); } apagado virtual vacío () anular { pool_.shutdown_gracefully(); }privado: Grupo YourThreadPool_; }; svr.new_task_queue = [] { devolver nuevo YourThreadPoolTaskQueue(12); };
#include <httplib.h>#include <iostream>int principal(vacío) { httplib::Cliente cli("localhost", 1234); if (auto res = cli.Get("/hi")) { if (res->status == StatusCode::OK_200) { std::cout << res->cuerpo << std::endl; } } else { auto err = res.error(); std::cout << "Error HTTP: " << httplib::to_string(err) << std::endl; } }
Consejo
¡Ahora se admite el constructor con cadena de puerto de host de esquema!
httplib::Cliente cli("localhost"); httplib::Cliente cli("localhost:8080"); httplib::Cliente cli("http://localhost"); httplib::Cliente cli("http://localhost:8080"); httplib::Cliente cli("https://localhost"); httplib::SSLClient cli("localhost");
Aquí está la lista de errores de Result::error()
.
Error de enumeración { Éxito = 0, Desconocido, Conexión, vincular dirección IP, Leer, Escribir, ExcederRedirectCount, Cancelado, conexión SSL, Certificados de carga SSL, SSLServerVerificación, Caracteres de límite multiparte no admitidos, Compresión, tiempo de espera de conexión, };
httplib::Encabezados encabezados = { { "Aceptar-Codificación", "gzip, deflate" } };auto res = cli.Get("/hola", encabezados);
o
auto res = cli.Get("/hi", {{"Accept-Encoding", "gzip, deflate"}});
o
cli.set_default_headers({ { "Aceptar-Codificación", "gzip, deflate" } });auto res = cli.Get("/hola");
res = cli.Post("/post", "texto", "texto/plain"); res = cli.Post("/persona", "nombre=john1¬e=coder", "aplicación/x-www-form-urlencoded");
httplib::Params params; params.emplace("nombre", "juan"); params.emplace("nota", "codificador");auto res = cli.Post("/post", params);
o
httplib::Params parámetros{ { "nombre", "juan" }, { "nota", "codificador" } };auto res = cli.Post("/post", params);
httplib::MultipartFormDataItems elementos = { { "texto1", "texto predeterminado", "", "" }, { "texto2", "aωb", "", "" }, { "archivo1", "hnennlnlnon", "hola.txt", "texto/sin formato" }, { "archivo2", "{n "mundo", verdaderon}n", "mundo.json", "aplicación/json" }, { "archivo3", "", "", "aplicación/flujo de octeto" }, };auto res = cli.Post("/multipart", elementos);
res = cli.Put("/resource/foo", "texto", "texto/plain");
res = cli.Delete("/recurso/foo");
res = cli.Options("*"); res = cli.Options("/resource/foo");
cli.set_connection_timeout(0, 300000); // 300 milisegundoscli.set_read_timeout(5, 0); // 5 segundoscli.set_write_timeout(5, 0); // 5 segundos
std::cuerpo de cadena;auto res = cli.Get("/datos grandes", [&](const char *datos, tamaño_t longitud_datos) { body.append(datos, longitud_datos); devolver verdadero; });
std::cuerpo de cadena;auto res = cli.Get( "/stream", Encabezados(), [&](Respuesta constante &respuesta) { EXPECT_EQ(StatusCode::OK_200, respuesta.status); devolver verdadero; // devuelve 'falso' si desea cancelar la solicitud. }, [&](const char *datos, tamaño_t longitud_datos) { body.append(datos, longitud_datos); devolver verdadero; // devuelve 'falso' si desea cancelar la solicitud. });
std::string body = ...;auto res = cli.Post( "/stream", body.size(), [](tamaño_t desplazamiento, tamaño_t longitud, DataSink y sumidero) { fregadero.write(body.data() + desplazamiento, longitud); devolver verdadero; // devuelve 'falso' si desea cancelar la solicitud. }, "texto/sin formato");
auto res = cli.Post( "/transmisión", [](tamaño_t desplazamiento, DataSink y sumidero) { fregadero.os << "datos fragmentados 1"; fregadero.os << "datos fragmentados 2"; fregadero.os << "datos fragmentados 3"; fregadero.hecho(); devolver verdadero; // devuelve 'falso' si desea cancelar la solicitud. }, "texto/sin formato");
httplib::Client cli(url, puerto);// imprime: 0/000 bytes => 50% completoauto res = cli.Get("/", [](uint64_t len, uint64_t total) { printf("%lld / %lld bytes => %d%% completon", len, total, (int)(len*100/total)); devolver verdadero; // devuelve 'falso' si deseas cancelar la solicitud.} );
// Autenticación básicacli.set_basic_auth("usuario", "contraseña");// Autenticación implícitacli.set_digest_auth("usuario", "contraseña");// Autenticación de token de portadorcli.set_bearer_token_auth("token");
Nota
Se requiere OpenSSL para la autenticación implícita.
cli.set_proxy("host", puerto);// Autenticación básicacli.set_proxy_basic_auth("usuario", "contraseña");// Autenticación implícitacli.set_proxy_digest_auth("usuario", "contraseña");// Autenticación de token de portadorcli.set_proxy_bearer_token_auth ("aprobar");
Nota
Se requiere OpenSSL para la autenticación implícita.
httplib::Client cli("httpbin.org");auto res = cli.Get("/range/32", { httplib::make_range_header({{1, 10}}) // 'Rango: bytes=1- 10'});// res->status debería ser 206.// res->body debería ser "bcdefghijk".
httplib::make_range_header({{1, 10}, {20, -1}}) // 'Rango: bytes=1-10, 20-'httplib::make_range_header({{100, 199}, {500, 599 }}) // 'Rango: bytes=100-199, 500-599'httplib::make_range_header({{0, 0}, {-1, 1}}) // 'Rango: bytes=0-0, -1'
httplib::Cliente cli("localhost", 1234); cli.Get("/hola"); // con "Conexión: cerrar"cli.set_keep_alive(true); cli.Get("/mundo"); cli.set_keep_alive (falso); cli.Get("/última solicitud"); // con "Conexión: cerrar"
httplib::Cliente cli("yahoo.com");auto res = cli.Get("/"); res->estado; // 301cli.set_follow_location(verdadero); res = cli.Get("/"); res->estado; // 200
Nota
Esta función aún no está disponible en Windows.
cli.set_interface("eth0"); // Nombre de la interfaz, dirección IP o nombre de host
El servidor puede aplicar compresión a los siguientes contenidos de tipo MIME:
todos los tipos de texto excepto texto/flujo de eventos
imagen/svg+xml
aplicación/javascript
aplicación/json
aplicación/xml
aplicación/xhtml+xml
La compresión 'gzip' está disponible con CPPHTTPLIB_ZLIB_SUPPORT
. libz
debe estar vinculado.
La compresión Brotli está disponible con CPPHTTPLIB_BROTLI_SUPPORT
. Las bibliotecas necesarias deben estar vinculadas. Consulte https://github.com/google/brotli para obtener más detalles.
cli.set_compress(verdadero); res = cli.Post("/resource/foo", "...", "texto/plain");
cli.set_decompress(falso); res = cli.Get("/resource/foo", {{"Accept-Encoding", "gzip, deflate, br"}}); res->cuerpo; // datos comprimidos
poll
en lugar de select
La llamada al sistema select
se utiliza de forma predeterminada, ya que es más compatible. Si desea permitir que cpp-httplib utilice poll
, puede hacerlo con CPPHTTPLIB_USE_POLL
.
La compatibilidad con Unix Domain Socket está disponible en Linux y macOS.
// Servidorhttplib::Servidor svr("./mi-socket.sock"); svr.set_address_family(AF_UNIX).listen("./my-socket.sock", 80);// Clienthttplib::Client cli("./my-socket.sock"); cli.set_address_family(AF_UNIX);
"my-socket.sock" puede ser una ruta relativa o absoluta. Su aplicación debe tener los permisos adecuados para la ruta. También puede utilizar una dirección de socket abstracta en Linux. Para utilizar una dirección de socket abstracta, anteponga un byte nulo ('x00') a la ruta.
$ ./split.py -husage: split.py [-h] [-e EXTENSION] [-o OUT]Este script divide httplib.h en partes .h y .cc. Argumentos opcionales: -h, --help show este mensaje de ayuda y salga -e EXTENSION, --extension EXTENSION extensión del archivo de implementación (predeterminado: cc) -o OUT, --out OUT dónde escribir los archivos (predeterminado: out)$ ./split.pyEscrito/httplib.h y fuera/httplib.cc
Dockerfile para servidor HTTP estático está disponible. El número de puerto de este servidor HTTP es 80 y sirve archivos estáticos desde el directorio /html
en el contenedor.
> Docker Build -t cpp-httplib-server ....> Docker Run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server Sirviendo HTTP en 0.0.0.0 puerto 80... 192.168.65.1 - - [31/ago/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31/ago/2024 :21:34:26 +0000] "OBTENER / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."192.168.65.1 - - [31/ago/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 " -" "Mozilla/5.0..."
Desde Docker Hub
> ejecución de ventana acoplable --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server ...> ejecución acoplable --init --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server Sirviendo HTTP en 0.0.0.0 puerto 80... 192.168.65.1 - - [31/ago/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1"192.168.65.1 - - [31/ago/2024 :21:34:26 +0000] "OBTENER / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..."192.168.65.1 - - [31/ago/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 " -" "Mozilla/5.0..."
g++ 4.8 y versiones anteriores no pueden compilar esta biblioteca ya que <regex>
en las versiones no funcionan.
Incluya httplib.h
antes de Windows.h
o incluya Windows.h
definiendo WIN32_LEAN_AND_MEAN
de antemano.
#incluye <httplib.h>#incluye <Windows.h>
#definir WIN32_LEAN_AND_MEAN#incluir <Windows.h>#incluir <httplib.h>
Nota
cpp-httplib admite oficialmente solo la última versión de Visual Studio. Puede que funcione con versiones anteriores de Visual Studio, pero ya no puedo verificarlo. Las solicitudes de extracción siempre son bienvenidas para las versiones anteriores de Visual Studio a menos que rompan la conformidad con C++11.
Nota
Windows 8 o inferior, Visual Studio 2013 o inferior, y Cygwin y MSYS2, incluido MinGW, no son compatibles ni probados.
Licencia MIT (© 2024 Yuji Hirose)
¡Estas personas hicieron grandes contribuciones para pulir esta biblioteca a un nivel totalmente diferente desde un simple juguete!