【C++/uwebsockets】C++实现websockets(一) 您所在的位置:网站首页 websocket文件流 【C++/uwebsockets】C++实现websockets(一)

【C++/uwebsockets】C++实现websockets(一)

#【C++/uwebsockets】C++实现websockets(一)| 来源: 网络整理| 查看: 265

翻译并删改自µWebSockets官方文档

  µWebSockets是符合标准的WebSockets(和HTTP)的安全实现。 它具有内置的发布/订阅支持,URL路由,TLS 1.3,SNI,IPv6,permessage-deflate,并且经过了实战测试,是最受欢迎的实现之一,每天有成千上万的最终用户使用。 目前,它在许多受欢迎的比特币交易所中的交易额高达数十亿美元,并且每天都在实际应用中有出色表现。

兼容ws标准

  与其他“发布/订阅代理”不同,µWS不采用或推送任何特定的应用程序协议,而仅在原始的标准WebSocket上运行。 只需要一个符合标准的Web浏览器和一些符合标准的JavaScript即可与其通信。 不需要或不强制使用特定的客户端库。

性能

  该实现是仅含头文件的C++ 17,跨平台且编译后大小仅几kB。 依赖于µSockets。性能方面,使用SSL的μWS性能明显优于最快的Golang的非SSL实现。

易用性

  该项目的另一个目标是极简,简约和优雅。 在设计方面,它遵循类似ExpressJS的接口,通过将回调函数绑定到不同的URL,可以在几行代码中轻松构建完整的REST / WebSocket服务。

  库本身可以高效处理心跳超时(heartbeat timeouts),背压处理(backpressure handling),ping/pong等其他棘手问题。

  μWS为单线程、异步实现,可以将其作为单个线程进行扩展。实现只有一个线程,并且不是线程安全的。 但是,如果确实需要,可以通过异步委托(async delegates)进行多线程。

编译

  µWebSockets完全由头文件实现,无需编译,可跨平台运行。 但是,它依赖于需要编译的µSockets。

推荐使用conan安装依赖库

使用手册 uWS::App & uWS::SSLApp

  uWS::SSLApp(构造函数采用包含SSL参数cert、key)和uWS::App 返回相同类型的对象,下文统称为“App”。

  应用程序遵循建造者模式,成员函数返回该应用程序,以便链式调用。

App.get, post, put, [...] and any routes

  请求方法入参为 URL匹配模式和Lambda表达式与。请求方法很多,但最常见的可能是get&post。 它们具有相同的签名,让我们看一个示例:

uWS::App().get("/hello", [](auto *res, auto *req) { res->end("Hello World!"); });

  req(请求),uWS::HttpRequest *在函数返回时被释放。换句话说,请求是堆栈分配的,因此不要将其放在pocket。

  res(响应),uWS :: HttpResponse 将处于活动状态并且可访问,直到发出.onAborted回调或通过res.end/res.tryEnd响应了请求为止。

  换句话说,可以立即响应请求并返回,或者将lambda附加到res(可能保存捕获的数据),稍后在其他异步回调中响应。

In other words, you either respond to the request immediately and return, or you attach lambdas to the res (which may hold captured data), and respond later on in some other async callback.

  在资源中捕获的数据遵循RAII且仅可移动,因此可以正确移入例如可用于std::string缓冲区的缓冲区,例如,用于缓冲upp流POST数据。 非常酷,使用仅移动捕获来检查可变的lambda。

Data that you capture in a res follows RAII and is move-only so you can properly move-in for instance std::string buffers that you may use to, for instance, buffer upp streaming POST data. It's pretty cool, check out mutable lambdas with move-only captures.

  any路由将匹配任何方法。

模式匹配

  路由是按照order of specificity进行匹配,而不是按照您注册它们的顺序进行匹配:

高优先级 - 静态路由,如 "/hello/this/is/static"。 中优先级 - 参数路由,如 "/candy/:kind",:kind的值通过 req.getParameter(0)被取出。 低优先级 - 通配路由,如 "/hello/*"。

  换言之,路由越具体,匹配顺序越早。因此,可以定义广泛的通配URL路由,然后基于此“刻画”出更具体的行为。

  当且仅当另外两个路由具有相同的特定性时,那些匹配"any"HTTP方法的“any”路由都将以比特定HTTP方法(如GET)的路由低的优先级进行匹配。

"Any" routes, those who match any HTTP method, will match with lower priority than routes which specify their specific HTTP method (such as GET) if and only if the two routes otherwise are equally specific.

中间件

  一个非常普遍的问题是如何实现类似中间件的功能。 我们不支持将中间件作为路由器本身内置的东西。部分原因是路由无法将数据传递到其他路由,部分原因是HttpRequest对象是堆栈分配的,并且仅在一次回调中有效,但最重要的是,可以通过使用简单的高阶函数和函数式编程轻松实现与中间件相同的功能链。中间件并不是服务器库本身必须内置的东西,它实际上只是一个常规功能。 通过将功能传递给其他功能,可通过非常优雅和有效的方式构建行为链。

流式数据

  不要调用res.end来保证streaming data发送,因为backpressure可能会飙升。 相反,应该使用res.tryEnd来部分地流式传输大量数据, 并与res.onWritable和res.onAborted回调函数结合使用。

The App.ws route

  WebSocket 路由注册方式类似,但不完全等同。

  每个websocket路由都具有与Http相同的匹配模式,但是除了一个单独的回调外,还有一整套回调,这是一个示例:

uWS::App().ws("/*", { /* Settings */ .compression = uWS::SHARED_COMPRESSOR, .maxPayloadLength = 16 * 1024, .idleTimeout = 10, /* Handlers */ .upgrade = [](auto *res, auto *req, auto *context) { /* You may read from req only here, and COPY whatever you need into your PerSocketData. * See UpgradeSync and UpgradeAsync examples. */ }, .open = [](auto *ws) { }, .message = [](auto *ws, std::string_view message, uWS::OpCode opCode) { ws->send(message, opCode); }, .drain = [](auto *ws) { /* Check getBufferedAmount here */ }, .ping = [](auto *ws) { }, .pong = [](auto *ws) { }, .close = [](auto *ws, int code, std::string_view message) { } });

  WebSocket路由通过指定一个数据类型来保留每个WebSocket用户数据。很多人倾向于通过将指针和用户数据放在std::map中来附加应该属于websocket的用户数据。不要那样做!

使用WebSocket.getUserData() 特性

  应该使用提供的用户数据功能来存储和附加任何基于套接字的用户数据。如果使用户数据持有指向WebSocket的指针并将其连接到WebSocket打开处理程序中,则可以从用户数据转到WebSocket。WebSocket有效时,用户数据存储器有效。

  如果要创建更复杂的内容,可以让用户数据持有指向某个动态分配的内存块的指针,并存储WebSocket是否仍然有效的布尔值。 天空是这里的极限(Sky is the limit here),永远不需要任何std::map。

WebSockets从打开到关闭均有效

  确保所有给定的WebSocket指针从打开事件(获得WebSocket的地方)一直存在,直到调用close事件为止。 用户数据存储器也是如此。 一个打开的事件将始终只在一个关闭的事件中结束,它们是一对一的关系,无论如何都将始终保持平衡。使用它们来驱动您的RAII数据类型,可以将它们视为构造函数和析构函数。

  消息事件永远不会在打开/关闭之外发出。 调用WebSocket.close或WebSocket.end将立即调用关闭处理程序。

Backpressure in websockets

  与Http类似,诸如ws.send(...)之类的方法可能导致Backpressure。 发送之前需检查ws.getBufferedAmount(),并在发送更多数据之前检查ws.send的返回值。 WebSocket没有.onWritable,而是使用websocket路由处理程序的.drain处理程序。

  在.drain事件中,您应该检查ws.getBufferedAmount(),它可能已经耗尽,甚至溢出了。.drain事件最有可能已耗尽,但不确定已耗尽。

Ping/pongs "heartbeats"

  该库将根据指定的idleTimeout自动向用户发送ping命令。 如果您将idleTimeout设置为120秒,则ping会在此超时之前几秒钟消失,除非客户端最近向服务器发送了一些消息。如果客户端响应ping,套接字将保持打开状态。当客户端无法及时响应时,套接字将被强制关闭,并且将触发close事件。断开连接后,将释放所有资源,包括对主题的订阅和任何backpressure。 如果需要,您可以轻松地使浏览器使用JavaScript重新连接。

Backpressure

  在WebSocket上发送会产生backpressure。WebSocket::send返回BACKPRESSURE,SUCCESS或DROPPED的枚举。 当send返回BACKPRESSURE时,这意味着您应该停止发送数据,直到耗尽事件触发并且WebSocket::getBufferedAmount()返回合理数量的字节为止。 但是,如果在创建WebSocketContext时指定了maxBackpressure,则将自动强制执行此限制。 这意味着将取消尝试发送将导致过多背压的消息,并且发送将返回DROPPED。 这意味着邮件已删除,不会放入队列中。 当使用pub/sub作为慢速接收器时,maxBackpressure是必不可少的设置,否则可能会建立很多背压。 通过设置maxBackpressure,该库将自动为您管理一个强制每个套接字的最大允许背压。

线程

  该库是单线程的。绝对不能混合线程。从线程1的应用程序创建的套接字不能以任何方式从线程2使用。 Loop:defer是整个库中唯一可以线程安全使用并且可以在任何线程中使用的函数。Loop::defer接受一个函数(例如带有数据的lambda)并推迟执行该函数,直到指定的循环线程准备在正确的线程上以单线程方式执行该函数为止。 因此,如果需要在某个主题下发布消息,或在其他线程的套接字上发送,则可以,但是这需要一些间接性。 应该以拥有尽可能独立的应用程序和线程为目标。

设置

Compression (permessage-deflate) has three main modes; uWS::DISABLED, uWS::SHARED_COMPRESSOR and any of the uWS::DEDICATED_COMPRESSOR_xKB. Disabled and shared options require no memory, while dedicated compressor requires the amount of memory you selected. For instance, uWS::DEDICATED_COMPRESSOR_4KB adds an overhead of 4KB per WebSocket while uWS::DEDICATED_COMPRESSOR_256KB adds - you guessed it - 256KB!

Compressing using shared means that every WebSocket message is an isolated compression stream, it does not have a sliding compression window, kept between multiple send calls like the dedicated variants do.

You probably want shared compressor if dealing with larger JSON messages, or 4kb dedicated compressor if dealing with smaller JSON messages and if doing binary messaging you probably want to disable it completely.

idleTimeout is roughly the amount of seconds that may pass between messages. Being idle for more than this, and the connection is severed. This means you should make your clients send small ping messages every now and then, to keep the connection alive. You can also make the server send ping messages but I would definitely put that labor on the client side. (outdated text - this is not entirely true anymore. The server will automatically send pings in case it needs to). 监听端口

  定义路由及其行为后,就该开始侦听新连接了。 您可以通过致电来做到这一点

App.listen(port, [](auto *listenSocket) { /* listenSocket is either nullptr or us_listen_socket */ })

  取消监听是通过uSockets函数调用us_listen_socket_close完成的。

App.run and fallthrough

  完成并想进入事件循环后,只需调用一次App.run。 这将阻塞调用线程,直到“ fallthrough”。 事件循环将一直阻塞,直到不再安排异步工作为止,就像Node.js一样。

  许多用户问应该如何停止事件循环。那不是完成它的正确方法,永远不要停止它,让它失败。 通过关闭所有套接字,停止侦听套接字,删除所有计时器等,该循环将自动导致App.run正常返回,这将不会发生内存泄漏。

  因为该应用程序本身处于RAII的控制之下,所以一旦阻塞的.run调用返回并且该应用程序超出范围,则所有内存将被正常删除。

汇总 int main() { uWS::App().get("/*", [](auto *res, auto *req) { res->end("Hello World!"); }).listen(9001, [](auto *listenSocket) { if (listenSocket) { std::cout


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有