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

翻译并删改自µ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 <SSL> 将处于活动状态并且可访问,直到发出.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.onWritableres.onAborted回调函数结合使用。

The App.ws route

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

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

uWS::App().ws<PerSocketData>("/*", {
    /* 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 << "Listening for connections..." << std::endl;
        }
    }).run();

    std::cout << "Shoot! We failed to listen and the App fell through, exiting now!" << std::endl;
}

拓展

  每个线程一个事件循环,隔离且没有共享数据。 这就是这里的设计。 就像Node.js一样,它不是每个进程而是每个线程。

  如果愿意的话,可以简单地采用前面的示例,将其放在几个std::thread中,然后侦听单独的端口或共享相同的端口(在Linux上可以使用。 诸如此类的更多功能可能会出现,例如主/从设置,但实际上并不难理解这个概念-保持事物隔离并生成具有任何代码的多个实例。

  通过新的Worker线程支持,最新的Node.js版本可以使用多个线程进行扩展。使用该功能进行缩放与使用C++中使用多个线程进行缩放相同。

理解

  在1970年代,编程是精英们的任务。如今,编程是由未经培训的“农民”完成的,结果,与之相比,对智能算法,内存使用量,CPU时间使用量等的关注逐渐减少。看看有多少Web开发人员表示时间-Web开发人员通常以JSON文档中带有实际文本键的30多个实际字母形式发送时间的整个文本表示形式,这并不罕见。 这太可怕了。 自1970年代以来,我们就以二进制,有效的4字节表示法对时间进行了标准化的时区中立表示。 它被称为unix时间戳,它是一种优雅而有效的方式,可以表示时区中立时间(低至秒)。

  这只是我们如何回归算法思维的一个例子。如今,通常使用文本表示法(例如JSON)来表示数据,尽管大多数膨胀是明显的重复并且本质上效率低下。但是我们不在乎,因为我们有压缩能力。没错,即使是最臃肿的源格式也可以压缩成很少的有效载荷而很少重复-但是,这是以极大的代价付出的。

  压缩应被视为万不得已,这是当您无法坐下来考虑更好的东西时的一种临时的鸭嘴式解决方案。 设计巧妙,二进制和最少重复的协议将节省大量因压缩而浪费的CPU时间。

  本质上,压缩实际上只是动态扫描重复并逐步建立通常重复的块的动态调色板。这需要大量的CPU时间,而且效率极低。总体而言,如果使用压缩,则只剩下20-30%的I/O性能。与其让某些通用的动态算法扫描您效率低下的数据表示,不如花时间设计一些本来就不会吸引人的东西。

  您可能已经定义了一个静态调色板并使用二进制整数有效地引用了它,而不是让每个单独的套接字尝试并动态地,无效率地在其自己的消耗内存的滑动窗口中动态找出并构建该调色板。

  如果计划提高效率,那么ProtoBuf等(其中可以使用整数引用而不是文本字符串)是关键。如果您打算使用必须压缩的JSON,则最好将计算机推下碎纸机,然后再执行其他操作。那是我的选择,我有很多。

  的确,与其他许多解决方案无法处理的未压缩消息/秒相比,我们每秒可以处理更多的permessage-deflate消息,是的,尽管这样做我们是完全稳定的,但是JSON仍然很糟糕。

  所以您可能会说-嘿-太复杂了。然后为您的用户构建一个SDK。只需将“复杂”协议包装在内部知道该调色板的JavaScript库中,并仅向最终用户公开易于使用的功能。解决这个问题并不难。

TLS/SSL

  TLS与压缩完全不同。使用TLS 1.3,您仍然可以看到与非TLS相比,性能保持率约为80%。这是因为TLS是基于块的,并且可以有效地映射到现代CPU。现代CPU对此也有硬件支持。使用现代加密标准对流量进行加密的要求不是很高。到目前为止,压缩是您对连接的CPU需求最大的事情,它需要每插槽的内存TONS。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,709评论 2 59
  • 为什么学习Python? 通过什么途径学习的Python? 上网收集视频,资料 关注公证号 买教程,书籍 Pyth...
    130920阅读 1,199评论 0 0
  • 1、谈谈对http协议的认识流程:1.域名解析域名解析检查顺序为:浏览器自身DNS缓存---》OS自身的DNS缓存...
    Zzmi阅读 692评论 0 0
  • 随着web技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求。实时通讯越来越多应用于...
    什么都不会的程序员阅读 1,876评论 0 4
  • 1.项目所遇到的问题集合 [if !supportLists]1.[endif]设置div的背景阴影 用:box-...
    一笑奈何_3bea阅读 1,160评论 0 0