rust网络编程以及unsafe模块

news2024/10/7 20:28:55

网络层目前 IPv4 和 IPv6 分庭抗礼,IPv6 还未完全对 IPv4 取而代之;传输层除了对延迟非常敏感的应用(比如游戏quic协议),绝大多数应用都使用 TCP;而在应用层,对用户友好,且对防火墙友好的 HTTP 协议家族:HTTP、WebSocket、HTTP/2,以及尚处在草案之中的 HTTP/3,在漫长的进化中,脱颖而出,成为应用程序主流的选择。
Rust 标准库提供了 std::net,为整个 TCP/IP 协议栈的使用提供了封装。然而 std::net 是同步的,所以,如果你要构建一个高性能的异步网络,可以使用 tokio。tokio::net 提供了和 std::net 几乎一致的封装,一旦你熟悉了 std::net,tokio::net 里的功能对你来说都并不陌生。所以,我们先从 std::net 开始了解。
同步方式
TcpListener/TcpStream如果要创建一个 TCP server,我们可以使用 TcpListener 绑定某个端口,然后用 loop 循环处理接收到的客户端请求。接收到请求后,会得到一个 TcpStream,它实现了 Read / Write trait,可以像读写文件一样,进行 socket 的读写:


use std::{
    io::{Read, Write},
    net::TcpListener,
    thread,
};

fn main() {
    let listener = TcpListener::bind("0.0.0.0:9527").unwrap();
    loop {
        let (mut stream, addr) = listener.accept().unwrap();
        println!("Accepted a new connection: {}", addr);
        thread::spawn(move || {
            let mut buf = [0u8; 12];
            stream.read_exact(&mut buf).unwrap();
            println!("data: {:?}", String::from_utf8_lossy(&buf));
            // 一共写了 17 个字节
            stream.write_all(b"glad to meet you!").unwrap();
        });
    }
}

对于客户端,我们可以用 TcpStream::connect() 得到一个 TcpStream。一旦客户端的请求被服务器接受,就可以发送或者接收数据:


use std::{
    io::{Read, Write},
    net::TcpStream,
};

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:9527").unwrap();
    // 一共写了 12 个字节
    stream.write_all(b"hello world!").unwrap();

    let mut buf = [0u8; 17];
    stream.read_exact(&mut buf).unwrap();
    println!("data: {:?}", String::from_utf8_lossy(&buf));
}

(这里的一个缺点就是客户端和服务器都需要硬编码要接收数据的大小,这样不够灵活,后续我们会看到如何通过使用消息帧(frame)更好地处理。)

从客户端的代码中可以看到,我们无需显式地关闭 TcpStream,因为 TcpStream 的内部实现也处理了 Drop trait,使得其离开作用域时会被关闭。

可以看到,主线程处理新链接,处理连接的过程,需要放在另一个线程或者另一个异步任务中进行,而不要在主循环中直接处理,因为这样会阻塞主循环,使其在处理完当前的连接前,无法 accept() 新的连接。这点和之前的项目是一致的。

但是使用线程处理频繁连接和退出的网络连接,一来会有效率上的问题,二来线程间如何共享公共的数据也让人头疼,我们来详细看看。

如果不断创建线程,那么当连接数一高,就容易把系统中可用的线程资源吃光。此外,因为线程的调度是操作系统完成的,每次调度都要经历一个复杂的、不那么高效的 save and load 的上下文切换过程,所以如果使用线程,那么,在遭遇到 C10K 的瓶颈,也就是连接数到万这个级别,系统就会遭遇到资源和算力的双重瓶颈**。从资源的角度,过多的线程占用过多的内存,Rust 缺省的栈大小是 2M,10k 连接就会占用 20G 内存(当然缺省栈大小也可以根据需要修改);从算力的角度,太多线程在连接数据到达时,会来来回回切换线程,导致 CPU 过分忙碌,无法处理更多的连接请求。**
可以考虑线程池来防止线程频繁创建销毁,但是可能效果也不明显。
如果要突破 C10K 的瓶颈,达到 C10M,我们就只能使用在用户态的协程来处理,要么是类似 Erlang/Golang 那样的有栈协程(stackful coroutine),要么是类似 Rust 异步处理这样的无栈协程(stackless coroutine)。所以,在 Rust 下大部分处理网络相关的代码中,你会看到,很少直接有用 std::net 进行处理的**,大部分都是用某个异步网络运行时,比如 tokio**。
这里可以和之前那个类比一下

如何处理共享信息?
二个问题,在构建服务器时,我们总会有一些共享的状态供所有的连接使用,比如数据库连接。对于这样的场景,如果共享数据不需要修改,我们可以考虑使用 Arc,如果需要修改,可以使用 Arc>。

但使用锁,就意味着一旦在关键路径上需要访问被锁住的资源,整个系统的吞吐量都会受到很大的影响。
一种思路是,我们把锁的粒度降低,这样冲突就会减少。比如在 kv server 中,我们把 key 哈希一下模 N,将不同的 key 分摊到 N 个 memory store 中,这样,锁的粒度就降低到之前的 1/N 了:

另一种思路是我们改变共享资源的访问方式,使其只被一个特定的线程访问;其它线程或者协程只能通过给其发消息的方式与之交互。如果你用 Erlang / Golang,这种方式你应该不陌生,在 Rust 下,可以使用 channel 数据结构。
Rust 下 channel,无论是标准库,还是第三方库,都有非常棒的的实现。同步 channel 的有标准库的 mpsc:channel 和第三方的 crossbeam_channel,异步 channel 有 tokio 下的 mpsc:channel,以及 flume。

处理网络数据的一般方法
大部分时候,我们可以使用已有的应用层协议来处理网络数据,比如 HTTP。在 HTTP 协议下,基本上使用 JSON 构建 REST API / JSON API 是业界的共识,客户端和服务器也有足够好的生态系统来支持这样的处理。你只需要使用 serde 让你定义的 Rust 数据结构具备 Serialize/Deserialize 的能力,然后用 serde_json 生成序列化后的 JSON 数据。

如果你出于性能或者其他原因,可能需要定义自己的客户端 / 服务器间的协议,那么,使用更加高效简洁的 protobuf。不过,使用 protobuf 构建协议消息的时候需要注意,因为 protobuf 生成的是不定长消息,所以你需要在客户端和服务器之间约定好**,如何界定一个消息帧(frame)。**常用的界定消息帧的方法有在消息尾添加 “\r\n”,以及在消息头添加长度。
因为这种处理方式很常见,所以 tokio 提供了 length_delimited codec,来处理用长度隔离的消息帧,它可以和 Framed 结构配合使用。
和刚才的 TcpListener / TcpStream 代码相比,双方都不需要知道对方发送的数据的长度,就可以通过 StreamExt trait 的 next() 接口得到下一个消息;在发送时,只需要调用 SinkExt trait 的 send() 接口发送,相应的长度就会被自动计算并添加到要发送的消息帧的开头。
服务端


use anyhow::Result;
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_util::codec::{Framed, LengthDelimitedCodec};

#[tokio::main]
async fn main() -> Result<()> {
    let listener = TcpListener::bind("127.0.0.1:9527").await?;
    loop {
        let (stream, addr) = listener.accept().await?;
        println!("accepted: {:?}", addr);
        // LengthDelimitedCodec 默认 4 字节长度
        let mut stream = Framed::new(stream, LengthDelimitedCodec::new());

        tokio::spawn(async move {
            // 接收到的消息会只包含消息主体(不包含长度)
            while let Some(Ok(data)) = stream.next().await {
                println!("Got: {:?}", String::from_utf8_lossy(&data));
                // 发送的消息也需要发送消息主体,不需要提供长度
                // Framed/LengthDelimitedCodec 会自动计算并添加
                stream.send(Bytes::from("goodbye world!")).await.unwrap();
            }
        });
    }
}

客户端


use anyhow::Result;
use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio_util::codec::{Framed, LengthDelimitedCodec};

#[tokio::main]
async fn main() -> Result<()> {
    let stream = TcpStream::connect("127.0.0.1:9527").await?;
    let mut stream = Framed::new(stream, LengthDelimitedCodec::new());
    stream.send(Bytes::from("hello world")).await?;

    // 接收从服务器返回的数据
    if let Some(Ok(data)) = stream.next().await {
        println!("Got: {:?}", String::from_utf8_lossy(&data));
    }

    Ok(())
}

这里为了代码的简便,我并没有直接使用 protobuf。你可以把发送和接收到的 Bytes 里的内容视作 protobuf 序列化成的二进制(如果你想看 protobuf 的处理,可以回顾 thumbor 和 kv server 的源代码)。我们可以看到,使用 LengthDelimitedCodec,构建一个自定义协议,变得非常简单。短短二十行代码就完成了非常繁杂的工作。

Unsafe Rust:如何用C++的方式打开Rust?
在这里插入图片描述

安全的 Rust 并不能适应所有的使用场景。首先,为了内存安全,Rust 所做的这些规则往往是普适性的,编译器会把一切可疑的行为都严格地制止掉。可是,这种一丝不苟的铁面无情往往会过于严苛,导致错杀。

其次,无论 Rust 将其内部的世界构建得多么纯粹和完美,它总归是要跟不纯粹也不完美的外界打交道,无论是硬件还是软件。
计算机硬件本身是 unsafe 的,比如操作 IO 访问外设,或者使用汇编指令进行特殊操作(操作 GPU 或者使用 SSE 指令集)。这样的操作,编译器是无法保证内存安全的,所以我们需要 unsafe 来告诉编译器要法外开恩。同样的,当 Rust 要访问其它语言比如 C/C++ 的库,因为它们并不满足 Rust 的安全性要求,这种跨语言的 FFI(Foreign Function Interface),也是 unsafe 的。这两种使用 unsafe Rust 的方式是不得而为之,所以情有可原,是我们需要使用 unsafe Rust 的主要原因。

还有一大类使用 unsafe Rust 纯粹是为了性能。比如略过边界检查、使用未初始化内存等。这样的 unsafe 我们要尽量不用,除非通过 benchmark 发现用 unsafe 可以解决某些性能瓶颈,否则使用起来得不偿失。因为,在使用 unsafe 代码的时候,我们已经把 Rust 的内存安全性,降低到了和 C++ 同等的水平。

好,在了解了为什么需要 unsafe Rust 之后,我们再来看看在日常工作中,都具体有哪些地方会用到 unsafe Rust。
实现 unsafe trait
Rust 里,名气最大的 unsafe 代码应该就是 Send / Sync 这两个 trait 了:相信你应该对这两个 trait 非常了解了,但凡遇到和并发相关的代码,尤其是接口的类型声明时,少不了要使用 Send / Sync 来约束。我们也知道,绝大多数数据结构都实现了 Send / Sync,但有一些例外,比如 Rc / RefCell / 裸指针等。

pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}

因为 Send / Sync 是 auto trait,所以大部分情况下,你自己的数据结构不需要实现 Send / Sync,然而,当你在数据结构里使用裸指针时,因为裸指针是没有实现 Send/Sync 的,连带着你的数据结构也就没有实现 Send/Sync。
但很可能你的结构是线程安全的,你也需要它线程安全。此时,如果你可以保证它能在线程中安全地移动,那可以实现 Send;如果可以保证它能在线程中安全地共享,也可以去实现 Sync。之前我们讨论过的 Bytes 就在使用裸指针的情况下实现了 Send / Sync:
unsafe trait 是对 trait 的实现者的约束,它告诉 trait 的实现者:实现我的时候要小心,要保证内存安全,所以实现的时候需要加 unsafe 关键字。
而 unsafe fn 是函数对调用者的约束,它告诉函数的调用者:如果你胡乱使用我,会带来内存安全方面的问题,请妥善使用,所以调用 unsafe fn 时,需要加 unsafe block 提醒别人注意。
在这里插入图片描述
好的 unsafe 代码,足够短小、精简,只包含不得不包含的内容。unsafe 代码是开发者对编译器和其它开发者的一种庄重的承诺:我宣誓,这段代码是安全的。今天讲的内容里的很多代码都是反面教材,并不建议你大量使用,尤其是初学者。那为什么我们还要讲 unsafe 代码呢?老子说:知其雄守其雌。我们要知道 Rust 的阴暗面(unsafe rust),才更容易守得住它光明的那一面(safe rust)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/417827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PageRank算法介绍

互联网上有数百亿个网页&#xff0c;可以分为这么几类&#xff1a;不含有用信息的&#xff0c;比如垃圾邮件&#xff1b;少数人比较感兴趣的&#xff0c;但范围不是很广的&#xff0c;比如个人博客、婚礼公告或家庭像册&#xff1b;很多人感兴趣的并且十分有用的&#xff0c;比…

基于DBN神经网络的语音分类识别

目录 背影 DBN神经网络的原理 DBN神经网络的定义 受限玻尔兹曼机(RBM) DBN的语音分类识别 基本结构 主要参数 数据 MATALB代码 结果图 展望 背影 DBN是一种深度学习神经网络,拥有提取特征,非监督学习的能力,是一种非常好的分类算法,本文将DBN算法应用于语音分类 DBN神…

排污口漂浮物监测系统 yolov5

排污口漂浮物监测系统通过YOLOV5网络模型技术&#xff0c;排污口漂浮物监测算法模型对河道两岸各处排污口进行7*24小时不间断实时监测&#xff0c;当监测到河道两岸的排污口违规乱排乱放时&#xff0c;不需人为干预系统立即抓拍存档告警。我们选择当下YOLO卷积神经网络YOLOv5来…

实现数字化转型升级,快速开发平台流辰信息来助力!

在如今迅猛发展的年代里&#xff0c;科技的力量是伟大的&#xff0c;可以让老百姓的生活变得丰富多彩&#xff0c;也可以让各行各业的企业实现办公自动化&#xff0c;朝着数字化转型升级&#xff0c;快速提升办公协作效率。快速开发平台流辰信息作为一家低代码平台服务商&#…

(顺序栈)有效括号序列

文章目录前言&#xff1a;问题描述&#xff1a;问题解析&#xff1a;代码实现&#xff1a;总结&#xff1a;前言&#xff1a; 此篇是针对顺序栈的经典练习题。 问题描述&#xff1a; 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘…

iOS / iPadOS 15.7.4发布安全更新 旧版iPhone和ipad无法升级系统解决方法

苹果于昨日面向无法升级到 iOS / iPadOS 16 的旧款 iPhone 和 iPad&#xff0c;发布了 iOS / iPadOS 15.7.4 更新。本次更新并未引入新的功能&#xff0c;而是重点修复了存在于 WebKit 的两个“关键”漏洞。 iOS / iPadOS 15.7.4发布安全更新&#xff1a; 目前已经有充足的证据…

adb基本操作操作

Mumu模拟器 进入终端 cd C:\Program Files\Netease\MuMuPlayer-12.0\shell 获取root权限 adb root 连接手机 adb connect 127.0.0.1:7555 adb shell 查看连接多少台设备 adb devices 安装程序apk adb install C:\Android\CalcTest.apk adb install -r C:\Android\CalcT…

遥感常用库的比较:Rasterio vs GDAL

一、前言本博客对三种栅格处理工具做一个小小的比较&#xff1a;Python (rasterio) 和 GDAL。当我开始使用 GIS 和栅格处理时&#xff0c;我并没有真正关注我编写的脚本和使用的工具的性能。 但现在&#xff0c;处理更大的数据、更高的空间分辨率、快速处理我的光栅文件是成功的…

国产PIE-SAR软件

1.产品定位 PIE-SAR是一款针对国内外主流星载SAR传感器的数据处理分析软件&#xff0c;提供图形化操作界面。目前已支持国内外主流星载SAR传感器的数据处理与分析&#xff0c;包括强度图像基础处理、区域网平差处理&#xff08;含异源匹配、RD/RPC区域网平差&#xff09;、InS…

TensorFlow 1.x 深度学习秘籍:1~5

原文&#xff1a;TensorFlow 1.x Deep Learning Cookbook 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只关心如…

知识图谱学习笔记——(二)知识图谱的表示

一、知识学习 声明&#xff1a;知识学习中本文主体按照浙江大学陈华钧教授的《知识图谱》公开课讲义进行介绍&#xff0c;并个别地方加入了自己的注释和思考&#xff0c;希望大家尊重陈华钧教授的知识产权&#xff0c;在使用时加上出处。感谢陈华钧教授。 &#xff08;一&…

Node版本管理工具

一、nvm简述 官网地址&#xff1a;https://nvm.uihtm.com/ nvm&#xff08;全称&#xff1a;node.js version management&#xff09;&#xff0c;是一个nodejs的版本管理工具&#xff0c;为解决node.js各种版本存在不兼容问题&#xff0c;可以使用nvm工具快速安装和切换不同版…

领英精灵安全吗?附LinkedIn(领英)开发客户的关键点

面对全球最大的职场社交平台——LinkedIn(领英)&#xff0c;很多蠢蠢欲动的外贸新人经常会问到&#xff1a;为什么那么多的外贸培训课程&#xff0c;机构和讲师以及一些外贸老鸟都会提到“如果想要把LinkedIn(领英)经营成一个有效的客户开发渠道&#xff0c;都会建议利用针对Li…

【机器学习】P17 梯度下降 与 梯度下降优化算法(BGD 等 与 Adam Optimizer、AdaGrad、RMSProp)

梯度下降与梯度下降算法梯度下降梯度下降算法基础优化算法批量梯度下降 BGD随机梯度下降 SGD小批量梯度下降 MBGD动量梯度下降 MGD基础优化算法上的改进和优化的算法自适应梯度算法 Adagrad均方根传播算法 RMSProp自适应矩估计算法 Adam代码如何实现梯度下降如何判断收敛梯度下…

pytorch搭建ResNet50实现鸟类识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 &#x1f366; 参考文章地址&#xff1a; 365天深度学习训练营-第J1周&#xff1a;ResNet-50算法实战与解析 &#x1f356; 作者&#xff1a;K同学啊 理论知识储备 深度残差网络ResNet&#xff08;dee…

KD2511N高精度直流低电阻值测试仪

一、产品简介 KD2511N直流低电阻测试仪采用32bits CPU和高密度 SMD贴装工艺&#xff0c; 拥有0.05%的 电阻测量精度及1uΩ 小电阻分辨率&#xff0c;内部比较器可让使用者自行设定上限/下限参考比较值。高达每秒15次的测试速度能显著提高工作效率&#xff0c;免清零的设计在小电…

数据结构刷题笔记 | 数组、字符串、链表、栈、队列、数、图

本篇为笔者学习数据结构时&#xff0c;在牛客网站的刷题笔记。 数据结构分为&#xff1a; 逻辑结构【面向问题的】 集合结构线性结构&#xff08;一对一&#xff09;树形结构&#xff08;一对多的层次关系&#xff09;图形结构&#xff08;多对多&#xff09; 物理结构&#x…

Docker 实战笔记-基础

转载请标明出处&#xff1a;http://blog.csdn.net/zhaoyanjun6/article/details/130181636 本文出自【赵彦军的博客】 文章目录官网下载安装安装Docker 镜像常用命令docker infodocker imagesdocker versiondocker 仓库官方仓库&#xff1a;[https://hub.docker.com/](https://…

移动端树形结构

该组件依据需求来做&#xff0c;当前包含三种选择状态&#xff0c;选中&#xff0c;未选中&#xff0c;半选。由于不需要做树形的收缩展开故没有写相关内容。树形展开与收缩与选中类似&#xff0c;只需要在节点上挂载相关字段即可实现。由于需求需要增加不限的功能&#xff0c;…

Moonbeam操作指南 | 如何设置Moonbeam开发节点

Moonbeam开发节点是为本地构建和测试应用的个人开发环境。对以太坊开发者来说&#xff0c;可以和Ganache相媲美。可以使你快速上手&#xff0c;且无需中继链的支出即可轻松实现。 有2种方式可以开始运行节点&#xff1a;使用Docker运行一个预构建的二进制文件&#xff0c;或者…