深入理解rtmp(三)之手把手实现握手协议

RTMP是基于TCP协议的应用层协议,默认通信端口1935.实现握手协议前先了解一下rtmp握手协议吧!!!

握手过程

要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点如下:

  1. 客户端要等收到S1之后才能发送C2
  2. 客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)
  3. 服务端要等到收到C0之后发送S1
  4. 服务端必须等到收到C1之后才能发送S2
  5. 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)

用图形可以表示为:

+-------------+                            +-------------+
|   Client    |      TCP/IP Network        |     Server  |
+-------------+             |              +-------------+
       |                    |                     |
Uninitialized               |                Uninitialized
       |        C0          |                     |
       |------------------->|           C0        |
       |                    |-------------------->|
       |        C1          |                     |
       |------------------->|           S0        |
       |                    |<--------------------|
       |                    |           S1        |
  Version sent              |<--------------------|
       |        S0          |                     |
       |<-------------------|                     |
       |        S1          |                     |
       |<-------------------|               Version sent
       |                    |           C1        |
       |                    |-------------------->|
       |        C2          |                     |
       |------------------->|           S2        |
       |                    |<--------------------|
    Ack sent                |                   Ack Sent
       |        S2          |                     |
       |<-------------------|                     |
       |                    |           C2        |
       |                    |-------------------->|
Handshake Done              |               Handshake Done
      |                     |                     |
          Pictorial Representation of Handshake

总结一下:

  • 握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。
  • 当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。
  • 当客户端和服务器分别收到S2和C2后,握手完成。

注意事项: 在实际工程应用中,一般是客户端先将C0, C1块同时发出,服务器在收到C1 之后同时将S0, S1, S2发给客户端。S2的内容就是收到的C1块的内容。之后客户端收到S1块,并原样返回给服务器,简单握手完成。按照RTMP协议个要求,客户端需要校验C1块的内容和S2块的内容是否相同,相同的话才彻底完成握手过程,实际编写程序用一般都不去做校验。

RTMP握手的这个过程就是完成了两件事:

  1. 校验客户端和服务器端RTMP协议版本号
  2. 发了一堆随机数据,校验网络状况。

握手包格式

简单握手

C0和S0:1个字节,包含了RTMP版本, 当前RTMP协议的版本为 3

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|     version   |
+-+-+-+-+-+-+-+-+
 C0 and S0 bits

C1和S1:4字节时间戳,4字节的0,1528字节的随机数

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           time (4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           zero (4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random bytes                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random bytes                        |
|                               (cont)                          |
|                               ....                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                        C1 and S1 bits
  • C1/S1 长度为 1536B。主要目的是确保握手的唯一性。
  • 格式为 time + zero + random
  • time 发送时间戳,长度 4 byte
  • zero 保留值 0,长度 4 byte
  • random 随机值,长度 1528 byte,保证此次握手的唯一性,确定握手的对象

C2和S2:4字节时间戳,4字节从对端读到的时间戳,1528字节随机数

0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          time (4 bytes)                       |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          time2 (4 bytes)                      |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          random echo                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          random echo                          |
 |                             (cont)                            |
 |                              ....                             |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                            C2 and S2 bits
  • C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值,对应 C1/S1 的 Copy 值,在于字段有点区别
  • time, C2/S2 发送的时间戳,长度 4 byte
  • time2, S1/C1 发送的时间戳,长度 4 byte
  • random,S1/C1 发送的随机数,长度为 1528B

携带上内容的流程图:
在这里插入图片描述

复杂握手

介绍复杂模式前,先介绍一个哈希签名算法,即hmac-sha256算法。复杂模式会使用它做一些签名运算和验证。
简单来说,这个算法的输入为一个key(长度可以为任意)和一个input字符串(长度可以为任意),经过hmac-sha256运算后得到一个32字节的签名串。

key和input固定时,hmac-sha256运算结果也是固定唯一的。
相对于简单握手,复杂握手增加了严格的验证,主要是 random 字段上进行更细化的划分

1528Bytes随机数的部分平均分成两部分,一部分764Bytes存储public key(公共密钥),另一部分764Bytes存储digest(密文,32字节)。
在这里插入图片描述

c0

固定为0x03

c1

格式如下:

| 4字节时间戳time | 4字节模式串 | 1528字节复杂二进制串 |

time字段参照简单模式下time的说明。

4字节模式串, 使用的是[0x0C, 0x00, 0x0D, 0x0E]。

1528字节复杂二进制串生成规则如下:
步骤一,将1528字节复杂二进制串进行随机化处理。
步骤二,在1528字节随机二进制串中写入32字节的digest签名。

digest的位置
先说明digest的位置如何确定。digest的位置可以在前半部分,也可以在后半部分。

digest在前半部分

当digest在前半部分时,digest的位置信息(以下简称offset)保存在前半部分的起始位置。

c1格式展开如下:

| 4字节time | 4字节模式串 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
offset = (c1[8] + c1[9] + c1[10] + c1[11]) % 728 + 12

几点说明

  • 计算出的offset是相对于整个c1的起始位置而言的。
  • 为什么要取余728呢,因为前半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
  • 为什么要加12呢,是因为要跳过4字节time+4字节模式串+4字节offset。
  • offset的取值范围为[12,740)。
  • 当offset=12时, left 部分就不存在,当offset=739时, right 部分就不存在。
digest在后半部分

当digest在后半部分时,offset保存在后半部分的起始位置。
c1格式展开如下:

| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
offset = (c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3]) % 728 + 8 + 764 + 4

几点说明:

  • 计算出的offset依赖是相对于c1的其实位置而言的。
  • 为什么要取余728呢,因为后半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
  • 为什么加8加764加4呢,是因为要跳过4字节time+4字节模式串+前半部分764字节+4字节offset。
  • offset的取值范围为[776,1504)。
  • 当offset=776时, left 部分就不存在,当offset=1503时, right 部分就不存在。
digest如何生成

说完digest的位置,再说digest如何生成。

即将c1 digest左边部分拼接上c1 digest右边部分(如果右边部分存在的话)作为hmac-sha256的input(整个大小是1536-32),以下大小为30字节固定key作为hmac-sha256的key,进过hmac-sha256计算得出32字节的digest填入c1中digest字段中。

'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ',
'0', '0', '1',

服务端在收到c1后,首先通过c1中的模式串,初步判断是否为复杂模式,如果是复杂模式,则通过c1重新digest,看计算得出的digest和c1中的包含的digest字段是否相同来确定握手是否为复杂模式。

注意,由于服务端无法直接得知客户端是将digest放在前半部分还是后半部分,所以服务端只能先验证其中一种,如果验证失败,再验证另外一种,如果都失败了,就考虑回退使用简单模式和客户端继续握手。

s0

固定为0x03

s1

s1的构造方法和c1相同。

只不过将模式串换成了 [0x0D, 0x0E, 0x0A, 0x0D]。

并且将hmac-sha256的key换成了如下36字节固定key

'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ',
'S', 'e', 'r', 'v', 'e', 'r', ' ',
'0', '0', '1',
s2

格式如下:

| 4字节时间戳time | 4字节time2 | 1528字节随机二进制串 |

其中time和time2字段参考简单模式下s2的说明。

1528字节随机二进制串中也需要填入digest。

将32字节digest直接填入s2的尾部,也即没有设置相应的offset ,digest的计算方法是,使用digest的左边部分作为hmac-sha256的input(大小是1536-32), 使用c1中的digest作为hmac-sha256的key ,通过hmac-sha256计算得出digest。

c2

c2的构造方法和s2相同。

只不过它是用s2中的digest作为hmac-sha256的key。

握手实现

我们继续写代码,实现简单握手协议.
协议实现相关的代码我们放到protocol文件夹下,我们先定义一个rtmp_stack.hpp文件,用来存放我们后面封装的rtmp相关数据结构,rtmp_stack.hpp中,增加HandshakeBytes类来存放握手相关的数据

// store the handshake bytes,
class HandshakeBytes
{
public:
    // For RTMP proxy, the real IP.
    uint32_t proxy_real_ip;
    // [1+1536]
    char* c0c1;
    // [1+1536+1536]
    char* s0s1s2;
    // [1536]
    char* c2;
public:
    HandshakeBytes();
    virtual ~HandshakeBytes();
public:
    virtual void dispose();
public:
    virtual error_t read_c0c1(SimpleSocketStream* io);
    virtual error_t read_s0s1s2(SimpleSocketStream* io);
    virtual error_t read_c2(SimpleSocketStream* io);
    virtual error_t create_c0c1();
    virtual error_t create_s0s1s2(const char* c1 = NULL);
    virtual error_t create_c2();
};

我们作为客户端只实现c0,c1,c2的生成发送和s0,s1,s2的读取即可:

HandshakeBytes::HandshakeBytes()
{
    c0c1 = s0s1s2 = c2 = NULL;
    proxy_real_ip = 0;
}

HandshakeBytes::~HandshakeBytes()
{
    dispose();
}

void HandshakeBytes::dispose()
{
    freepa(c0c1);
    freepa(s0s1s2);
    freepa(c2);
}


error_t HandshakeBytes::read_s0s1s2(SimpleSocketStream* io)
{
    error_t err = srs_success;
    
    if (s0s1s2) {
        return err;
    }
    
    ssize_t nsize;
    
    s0s1s2 = new char[3073];
    if ((err = io->read_fully(s0s1s2, 3073, &nsize)) != srs_success) {
        return error_wrap(err, "read s0s1s2");
    }
    
    return err;
}

error_t HandshakeBytes::create_c0c1()
{
    error_t err = srs_success;
    
    if (c0c1) {
        return err;
    }
    
    c0c1 = new char[1537];
    random_generate(c0c1, 1537);
    
    // plain text required.
    SBuffer stream(c0c1, 9);
    
    stream.write_1bytes(0x03);
    stream.write_4bytes((int32_t)::time(NULL));
    stream.write_4bytes(0x00);
    
    return err;
}

error_t HandshakeBytes::create_c2()
{
    error_t err = srs_success;
    
    if (c2) {
        return err;
    }
    
    c2 = new char[1536];
    srs_random_generate(c2, 1536);
    
    // time
    SBuffer stream(c2, 8);
    
    stream.write_4bytes((int32_t)::time(NULL));
    // c2 time2 copy from s1
    if (s0s1s2) {
        stream.write_bytes(s0s1s2 + 1, 4);
    }
    
    return err;
}

random_generate实现:

//rand()随机数生成
void random_generate(char* bytes, int size)
{
    static bool _random_initialized = false;
    if (!_random_initialized) {
        srand(0);
        _random_initialized = true;
    }
    
    for (int i = 0; i < size; i++) {
        // the common value in [0x0f, 0xf0]
        bytes[i] = 0x0f + (rand() % (256 - 0x0f - 0x0f));
    }
}

最基本的客户端握手协议就实现了,服务端的实现也类似.

接下来我们把握手封装到一个类里面:

//rtmp_handshake.hpp
class SimpleHandshake
{
public:
    SimpleHandshake();
    virtual ~SimpleHandshake();
public:
    // Simple handshake.
    virtual srs_error_t handshake_with_client(HandshakeBytes* hs_bytes, SimpleSocketStream* io);
    virtual srs_error_t handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io);
};

实现(同样的我们先只实现客户端连接服务端):

SimpleHandshake::SimpleHandshake()
{
}

SimpleHandshake::~SimpleHandshake()
{
}

error_t SimpleHandshake::handshake_with_server(HandshakeBytes* hs_bytes, SimpleSocketStream* io)
{
    error_t err = srs_success;
    
    ssize_t nsize;
    
    // simple handshake
    if ((err = hs_bytes->create_c0c1()) != success) {
        return error_wrap(err, "create c0c1");
    }
    
    if ((err = io->write(hs_bytes->c0c1, 1537, &nsize)) != srs_success) {
        return error_wrap(err, "write c0c1");
    }
    
    if ((err = hs_bytes->read_s0s1s2(io)) != srs_success) {
        return error_wrap(err, "read s0s1s2");
    }
    
    // plain text required.
    if (hs_bytes->s0s1s2[0] != 0x03) {
        return error_new(ERROR_RTMP_HANDSHAKE, "handshake failed, plain text required, version=%X", (uint8_t)hs_bytes->s0s1s2[0]);
    }
    
    if ((err = hs_bytes->create_c2()) != success) {
        return srs_error_wrap(err, "create c2");
    }

    memcpy(hs_bytes->c2, hs_bytes->s0s1s2 + 1, 1536);
    
    if ((err = io->write(hs_bytes->c2, 1536, &nsize)) != success) {
        return error_wrap(err, "write c2");
    }
    
    std::cout << "simple handshake success." << std::endl;
    
    return err;
}

接口封装及测试

我们在实现rtmpsdk.hpp对外暴露接口前,先封装一个上下文环境的Context:

struct Context
{
    // The original RTMP url.
    std::string url;
    
    // Parse from url.
    std::string tcUrl;
    std::string host;
    std::string vhost;
    std::string app;
    std::string stream;
    std::string param;
    
    // Parse ip:port from host.
    std::string ip;
    int port;

    SimpleSocketStream* skt;
    HandshakeBytes* hhb;
    
    // user set timeout, in ms.
    int64_t stimeout;
    int64_t rtimeout;
    
    Context() : port(0) {
        skt = NULL;
    }
    virtual ~Context() {
        srs_freep(skt);
     
    }
};

下面我们按前文步骤深入理解rtmp(二)之C++脚手架搭建封装接口步骤在rtmpsdk.hhp统一封装统一对外暴露接口

1.实现rtmp_create

rtmp_t rtmp_create(const char* url)
{
    int ret = ERROR_SUCCESS;
    
    Context* context = new Context();
    context->url = url;
    
    // create socket
    freep(context->skt);
    context->skt = new SimpleSocketStream();
    
    if ((ret = context->skt->create_socket(context->url)) != ERROR_SUCCESS) {//调用SimpleSocketStream的create_socket方法
        printf("Create socket failed, ret=%d", ret);
        
        // free the context and return NULL
        freep(context);
        return NULL;
    }
    
    return context;
}

2.封装rtmp_handshake

int rtmp_handshake(rtmp_t rtmp)
{
    int ret = ERROR_SUCCESS;
    
    if ((ret = rtmp_dns_resolve(rtmp)) != ERROR_SUCCESS) {
        return ret;
    }
    
    if ((ret = rtmp_connect_server(rtmp)) != ERROR_SUCCESS) {
        return ret;
    }
    
    if ((ret = rtmp_do_simple_handshake(rtmp)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}

握手我们分三步执行:

  1. dns解析
  2. 连接服务
  3. 进行握手
rtmp_dns_resolve

rtmp_dns_resolve我们又拆分成了解析uri和解析host:

int rtmp_dns_resolve(rtmp_t rtmp)
{
    int ret = ERROR_SUCCESS;
    
    assert(rtmp != NULL);
    Context* context = (Context*)rtmp;
    
    // parse uri
    if ((ret = librtmp_context_parse_uri(context)) != ERROR_SUCCESS) {
        return ret;
    }
    // resolve host
    if ((ret = librtmp_context_resolve_host(context)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}

解析uri:

int librtmp_context_parse_uri(Context* context)
{
    int ret = ERROR_SUCCESS;
    
    std::string schema;

    //1.通过最后边的斜线"/"将url拆分成tcUrl和stream两部分
    parse_rtmp_url(context->url, context->tcUrl, context->stream);
    
    // when connect, we only need to parse the tcUrl
    //2.将tcUrl拆分成scheme, host, 虚拟host,app,stream和端口
    srs_discovery_tc_url(context->tcUrl,
        schema, context->host, context->vhost, context->app, context->stream, context->port,
        context->param);
    
    return ret;
}

解析host:

int librtmp_context_resolve_host(Context* context)
{
    int ret = ERROR_SUCCESS;
    
    // connect to server:port
    int family = AF_UNSPEC;
    进行dns解析,将host解析成ip
    context->ip = dns_resolve(context->host, family);
    if (context->ip.empty()) {
        return ERROR_SYSTEM_DNS_RESOLVE;
    }
    
    return ret;
}

dns解析:

string dns_resolve(string host, int& family)
{
    addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = family;
    
    addrinfo* r = NULL;
    
    if(getaddrinfo(host.c_str(), NULL, &hints, &r)) {
        return "";
    }
    
    char shost[64];
    memset(shost, 0, sizeof(shost));
    if (getnameinfo(r->ai_addr, r->ai_addrlen, shost, sizeof(shost), NULL, 0, NI_NUMERICHOST)) {
        return "";
    }

   family = r->ai_family;
   return string(shost);
}
rtmp_connect_server
int librtmp_context_connect(Context* context)
{
    int ret = ERROR_SUCCESS;
    
    srs_assert(context->skt);
    
    std::string ip = context->ip;
    if ((ret = context->skt->connect(ip.c_str(), context->port)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}

int rtmp_connect_server(rtmp_t rtmp)
{
    int ret = ERROR_SUCCESS;
    
    assert(rtmp != NULL);
    Context* context = (Context*)rtmp;
    
    // set timeout if user not set.
    if (context->stimeout == SRS_UTIME_NO_TIMEOUT) {
        context->stimeout = SRS_SOCKET_DEFAULT_TMMS;
        context->skt->set_send_timeout(context->stimeout * SRS_UTIME_MILLISECONDS);
    }
    if (context->rtimeout == SRS_UTIME_NO_TIMEOUT) {
        context->rtimeout = SRS_SOCKET_DEFAULT_TMMS;
        context->skt->set_recv_timeout(context->rtimeout * SRS_UTIME_MILLISECONDS);
    }
    
    if ((ret = librtmp_context_connect(context)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}

设置完超时等参数后,调用SimpleSocketStream的connect连接服务器

rtmp_do_simple_handshake

调用我们上面封装的handshake_with_server与rtmp server进行握手

int rtmp_do_simple_handshake(rtmp_t rtmp)
{
    int ret = ERROR_SUCCESS;
    srs_error_t err = srs_success;
    
    srs_assert(rtmp != NULL);
    Context* context = (Context*)rtmp;
    
    srs_assert(context->skt != NULL);
    
    // simple handshake
    srs_freep(context->hhb);
    context->hhb = new HandshakeBytes();
    
    srs_assert(context->hhb);
    
    SimpleHandshake simple_hs;
    if ((err = simple_hs.handshake_with_server(context->hhb, context->skt)) != srs_success) {
        return -1;
    }
    
    context->hhb->dispose();
    
    cout << "handshake success..." << endl;
    
    return ret;
}

3.main中测试

改造我们上一篇的main方法:

int main(int argc,char* argv[])
{
    std::cout << "Hello rtmp server!" << std::endl;
    
    rtmp_t client = rtmp_create("rtmp://127.0.0.1:1935/live/livestream");
    int ret = rtmp_handshake(client);
    return 0;    
}

最终日志输出:

$ ./rtmpsdk 
Hello rtmp server!
simple handshake success.
handshake success...

srs服务端日志输出:

[2020-01-21 11:06:17.237][Trace][7503][531] RTMP client ip=172.17.0.1, fd=10
[2020-01-21 11:06:17.240][Trace][7503][531] simple handshake success.
[2020-01-21 11:06:17.240][Warn][7503][531][104] client disconnect peer. ret=1007

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux1(介绍与基本命令)

目录 一、初始Linux 1. Linux的起源 2. Linux是什么&#xff1f; 3. Linux内核版本 4. Linux的应用 5. 终端 6. Shell 7. Linux目录结构 二、基本命令 1. 基本的命令格式 2. shutdown 关机命令 3. pwd 当前工作目录 4. ls 查看目录内容 5. cd 改变工作目录 …

揭秘速卖通API接口:打破电商边界,用代码驱动全球业务增长

速卖通&#xff08;AliExpress&#xff09;通常指的是阿里巴巴集团旗下的国际零售电商平台。然而&#xff0c;直接通过API接口与速卖通进行交互通常涉及阿里巴巴的开放平台&#xff08;Open Platform&#xff09;和相关API。由于API的具体细节、认证方式、请求参数和返回值可能…

六种图算法的python实现

六种图算法的python实现 1. Prim 算法 基本原理 Prim算法是一种求解最小生成树的贪心算法。所谓最小生成树&#xff0c;就是对于给定的连通图&#xff0c;找到一棵包含所有顶点的树&#xff0c;且树上所有边的权重之和最小。Prim算法从一个顶点开始&#xff0c;每次选择与当…

数据丢失?揭秘easyrecovery破解版下载安装步骤教程,一键恢复!

“我不小心把硬盘里的重要文件删了&#xff0c;怎么都找不到了&#xff01;” “电脑突然崩溃了&#xff0c;所有的数据都没了&#xff0c;怎么办&#xff1f;” 这些情况是不是让你感到绝望&#xff1f;不过别担心&#xff0c;EasyRecovery数据恢复软件可以帮你轻松解决这些问…

[office] excel表格中双击鼠标左键有什么快捷作用- #经验分享#媒体

excel表格中双击鼠标左键有什么快捷作用? excel表格中双击鼠标左键有什么快捷作用&#xff1f;不要小看鼠标左键双击的作用&#xff0c;在excel中双击鼠标左键可以实现六个功能&#xff0c;提高工作效率&#xff0c;到底是那六个功能呢&#xff1f;请看下文详细介绍 在表格中…

R语言绘图 --- 桑基图(Biorplot 开发日志 --- 5)

「写在前面」 在科研数据分析中我们会重复地绘制一些图形&#xff0c;如果代码管理不当经常就会忘记之前绘图的代码。于是我计划开发一个 R 包&#xff08;Biorplot&#xff09;&#xff0c;用来管理自己 R 语言绘图的代码。本系列文章用于记录 Biorplot 包开发日志。 相关链接…

React基础教程:TodoList案例

todoList案例——增加 定义状态 // 定义状态state {list: ["kevin", "book", "paul"]}利用ul遍历list数组 <ul>{this.state.list.map(item ><li style{{fontWeight: "bold", fontSize: "20px"}} key{item.i…

MoE大模型大火,AI厂商们在新架构上看到了什么样的未来?

文 | 智能相对论 作者 | 陈泊丞 很久以前&#xff0c;在一个遥远的国度里&#xff0c;国王决定建造一座宏伟的宫殿&#xff0c;以展示国家的繁荣和权力。他邀请了全国最著名的建筑师来设计这座宫殿&#xff0c;这个人以其卓越的才能和智慧闻名。 然而&#xff0c;这位建筑师…

Apollo9.0 PNC源码学习之Control模块(三)

本文将对Apollo的纵向控制器进行讲解&#xff0c;看完本文&#xff0c;你将会对百度Apollo的纵向控制有更深的理解 前面文章&#xff1a; Apollo9.0 PNC源码学习之Control模块&#xff08;一&#xff09; Apollo9.0 PNC源码学习之Control模块&#xff08;二&#xff09; 1 纵向…

AI大模型的战场:通用与垂直的较量

AI大模型的战场&#xff1a;通用与垂直的较量 引言&#xff1a;AI界的“通才”与“专家” 在AI的大千世界里&#xff0c;有这样两类模型&#xff1a;一类是像瑞士军刀一样多功能的通用大模型&#xff0c;另一类则是像手术刀一样精准的垂直大模型。它们在AI战场上展开了一场激…

【0基础学爬虫】爬虫基础之自动化工具 DrissionPage 的使用

概述 前三期文章中已经介绍到了 Selenium 与 Playwright 、Pyppeteer 的使用方法&#xff0c;它们的功能都非常强大。而本期要讲的 DrissionPage 更为独特&#xff0c;强大&#xff0c;而且使用更为方便&#xff0c;目前检测少&#xff0c;强烈推荐&#xff01;&#xff01;&a…

GaN VCSEL:改进生产工艺

对腔体厚度的卓越控制宛如一位精准的狙击手&#xff0c;精确锁定了发射波长的目标。日本工程师们凭借一项革命性的工艺&#xff0c;成功打造出效率极高的VCSEL&#xff0c;其发射波长与目标波长如丝般顺滑地接近。 这一卓越的进步是名城大学与国家先进工业科学和技术研究所科研…

阿里云物联网平台案例教程

1、定义&#xff1a; ​ 物联网&#xff08;简称IOT&#xff09;把任何物体与物联网相连接&#xff0c;进行消息的交换和通信&#xff0c;实现对物品的智能化识别。简单说是&#xff1a;物联网就是把所有的物体连接起来相互作用&#xff0c;形成一个互联互通的网络&#xff0c…

解读光纤模块的参数有哪些

光模块的具体参数有传输速率、传输距离、中心波长、光纤类型、光口类型、工作温度范围、最大功耗等。下面给大家详解一下各个参数的作用 因为光纤本身对光信号有色散、损耗等副作用。因此不同类型的光源发出的光所能传输的距离不一样。对接光接口时&#xff0c;应根据最远的信号…

【架构之路】微服务中常用的几种通信方式

2024年&#xff0c;计算机相关专业还值得选择吗&#xff1f; 强烈推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站:人工智能 引言 微服务架构由于其灵活性、高可扩展性和易维护性&am…

Redis脑裂问题详解及解决方案

Redis脑裂问题 Redis脑裂问题是指在主从集群中同时存在两个主节点&#xff0c;这会导致不同客户端往不同的主节点写入数据&#xff0c;最终导致数据不一致&#xff0c;甚至数据丢失。 哨兵主从集群脑裂 场景描述 假设有三台服务器&#xff1a;一台主服务器&#xff0c;两台…

对Java中二维数组的深层认识

首先&#xff0c;在JAVA中&#xff0c;二维数组是一种数组的数组。它可以看作是一个矩阵&#xff0c;通常是由于表示二维数据节后&#xff0c;如表格和网格。 1.声明和初始化二维数组 声明 int[][] arr;初始化 int[][] arrnew int[3][4];或者用花括号嵌套 int[][] arr{{1,…

高温预警,快收下这份机房运维攻略

高温预警 华东区即将迎来最强高温&#xff0c;根据历史经验&#xff0c;数据机房在夏季高温环境导致设备温度过高&#xff0c;宕机事件明显增加&#xff0c;为保障系统健康稳定运行&#xff0c;需要针对数据机房空调、设备的运行状态及环境进行检查&#xff0c;并同时期开展防尘…

[Shell编程学习路线]--shell中重定向和管道符(详细介绍)

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f6e0;️Shell编程专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月12日10点50分 &#x1f004;️文章质量&#xff1a;93分 ——前言—— 在Shell编程中&#xff0c;重定向和管道符是两个…

MySQL 示例数据库大全

前言&#xff1a; 我们练习 SQL 时&#xff0c;总会自己创造一些测试数据或者网上找些案例来学习&#xff0c;其实 MySQL 官方提供了好几个示例数据库&#xff0c;在 MySQL 的学习、开发和实践中具有非常重要的作用&#xff0c;能够帮助初学者更好地理解和应用 MySQL 的各种功…