您可知道如何通过`HTTP2`实现TCP的内网穿透???

news2024/11/24 15:41:58

可能有人很疑惑应用层 转发传输层?,为什么会有这样的需求啊???哈哈技术无所不用其极,由于一些场景下,对于一个服务器存在某一个内部网站中,但是对于这个服务器它没有访问外网的权限,虽然也可以申请端口访问外部指定的ip+端口,但是对于访问服务内部的TCP的时候我们就会发现忘记申请了!这个时候我们又要提交申请,又要等审批,然后开通端口,对于这个步骤不是一般的麻烦,所以我在想是否可以直接利用现有的Http网关的端口进行转发内部的TCP服务?这个时候我询问了我们的老九大佬,由于我之前也做过通过H2实现HTTP内网穿透,可以利用H2将内部网络中的服务映射出来,但是由于底层是基于yarp的一些方法实现,所以并没有考虑过TCP,然后于老九大佬交流深究,决定尝试验证可行性,然后我们的Taibai项目就诞生了,为什么叫Taibai?您仔细看看这个拼音,翻译过来就是太白,确实全称应该叫太白金星,寓意上天遁地无所不能!下面我们介绍一下具体实现逻辑,确实您仔细看会发现实现是真的超级简单的!

创建Core项目用于共用的核心类库

创建项目名Taibai.Core

下面几个方法都是用于操作Stream的类

DelegatingStream.cs

namespace Taibai.Core;

/// <summary>
/// 委托流
/// </summary>
public abstract class DelegatingStream : Stream
{
    /// <summary>
    /// 获取所包装的流对象
    /// </summary>
    protected readonly Stream Inner;

    /// <summary>
    /// 委托流
    /// </summary>
    /// <param name="inner"></param>
    public DelegatingStream(Stream inner)
    {
        this.Inner = inner;
    }

    /// <inheritdoc/>
    public override bool CanRead => Inner.CanRead;

    /// <inheritdoc/>
    public override bool CanSeek => Inner.CanSeek;

    /// <inheritdoc/>
    public override bool CanWrite => Inner.CanWrite;

    /// <inheritdoc/>
    public override long Length => Inner.Length;

    /// <inheritdoc/>
    public override bool CanTimeout => Inner.CanTimeout;

    /// <inheritdoc/>
    public override int ReadTimeout
    {
        get => Inner.ReadTimeout;
        set => Inner.ReadTimeout = value;
    }

    /// <inheritdoc/>
    public override int WriteTimeout
    {
        get => Inner.WriteTimeout;
        set => Inner.WriteTimeout = value;
    }


    /// <inheritdoc/>
    public override long Position
    {
        get => Inner.Position;
        set => Inner.Position = value;
    }

    /// <inheritdoc/>
    public override void Flush()
    {
        Inner.Flush();
    }

    /// <inheritdoc/>
    public override Task FlushAsync(CancellationToken cancellationToken)
    {
        return Inner.FlushAsync(cancellationToken);
    }

    /// <inheritdoc/>
    public override int Read(byte[] buffer, int offset, int count)
    {
        return Inner.Read(buffer, offset, count);
    }

    /// <inheritdoc/>
    public override int Read(Span<byte> destination)
    {
        return Inner.Read(destination);
    }

    /// <inheritdoc/>
    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        return Inner.ReadAsync(buffer, offset, count, cancellationToken);
    }

    /// <inheritdoc/>
    public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
    {
        return Inner.ReadAsync(destination, cancellationToken);
    }

    /// <inheritdoc/>
    public override long Seek(long offset, SeekOrigin origin)
    {
        return Inner.Seek(offset, origin);
    }

    /// <inheritdoc/>
    public override void SetLength(long value)
    {
        Inner.SetLength(value);
    }

    /// <inheritdoc/>
    public override void Write(byte[] buffer, int offset, int count)
    {
        Inner.Write(buffer, offset, count);
    }

    /// <inheritdoc/>
    public override void Write(ReadOnlySpan<byte> source)
    {
        Inner.Write(source);
    }

    /// <inheritdoc/>
    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        return Inner.WriteAsync(buffer, offset, count, cancellationToken);
    }

    /// <inheritdoc/>
    public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
    {
        return Inner.WriteAsync(source, cancellationToken);
    }

    /// <inheritdoc/>
    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
    {
        return TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state);
    }

    /// <inheritdoc/>
    public override int EndRead(IAsyncResult asyncResult)
    {
        return TaskToAsyncResult.End<int>(asyncResult);
    }

    /// <inheritdoc/>
    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback,
        object? state)
    {
        return TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count), callback, state);
    }

    /// <inheritdoc/>
    public override void EndWrite(IAsyncResult asyncResult)
    {
        TaskToAsyncResult.End(asyncResult);
    }

    /// <inheritdoc/>
    public override int ReadByte()
    {
        return Inner.ReadByte();
    }

    /// <inheritdoc/>
    public override void WriteByte(byte value)
    {
        Inner.WriteByte(value);
    }

    /// <inheritdoc/>
    public sealed override void Close()
    {
        base.Close();
    }
}

SafeWriteStream.cs

public class SafeWriteStream(Stream inner) : DelegatingStream(inner)
{
    private readonly SemaphoreSlim semaphoreSlim = new(1, 1);

    public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
    {
        try
        {
            await this.semaphoreSlim.WaitAsync(CancellationToken.None);
            await base.WriteAsync(source, cancellationToken);
            await this.FlushAsync(cancellationToken);
        }
        finally
        {
            this.semaphoreSlim.Release();
        }
    }

    public override ValueTask DisposeAsync()
    {
        this.semaphoreSlim.Dispose();
        return this.Inner.DisposeAsync();
    }

    protected override void Dispose(bool disposing)
    {
        this.semaphoreSlim.Dispose();
        this.Inner.Dispose();
    }
}

创建服务端

创建一个WebAPI的项目项目名Taibai.Server并且依赖Taibai.Core项目

创建ServerService.cs,这个类是用于管理内网的客户端的,这个一般是部署在内网服务器上,用于将内网的端口映射出来,但是我们的Demo只实现了简单的管理不做端口的管理。

using System.Collections.Concurrent;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Timeouts;
using Taibai.Core;

namespace Taibai.Server;

public static class ServerService
{
    private static readonly ConcurrentDictionary<string, (CancellationToken, Stream)> ClusterConnections = new();

    public static async Task StartAsync(HttpContext context)
    {
        // 如果不是http2协议,我们不处理, 因为我们只支持http2
        if (context.Request.Protocol != HttpProtocol.Http2)
        {
            return;
        }

        // 获取query
        var query = context.Request.Query;

        // 我们需要强制要求name参数
        var name = query["name"];

        if (string.IsNullOrEmpty(name))
        {
            context.Response.StatusCode = 400;
            Console.WriteLine("Name is required");
            return;
        }
        
        Console.WriteLine("Accepted connection from " + name);

        // 获取http2特性
        var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();
        
        // 禁用超时
        context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();

        // 得到双工流
        var stream = new SafeWriteStream(await http2Feature.AcceptAsync());

        // 将其添加到集合中,以便我们可以在其他地方使用
        CreateConnectionChannel(name, context.RequestAborted, stream);

        // 注册取消连接
        context.RequestAborted.Register(() =>
        {
            // 当取消时,我们需要从集合中删除
            ClusterConnections.TryRemove(name, out _);
        });
        
        // 由于我们需要保持连接,所以我们需要等待,直到客户端主动断开连接。
        await Task.Delay(-1, context.RequestAborted);
    }

    /// <summary>
    /// 通过名称获取连接
    /// </summary>
    /// <param name="host"></param>
    /// <returns></returns>
    public static (CancellationToken, Stream) GetConnectionChannel(string host)
    {
        return ClusterConnections[host];
    }

    /// <summary>
    /// 注册连接
    /// </summary>
    /// <param name="host"></param>
    /// <param name="cancellationToken"></param>
    /// <param name="stream"></param>
    public static void CreateConnectionChannel(string host, CancellationToken cancellationToken, Stream stream)
    {
        ClusterConnections.GetOrAdd(host,
            _ => (cancellationToken, stream));
    }
}

然后再创建ClientMiddleware.cs,并且继承IMiddleware,这个是我们本地使用的客户端链接的时候进入的中间件,再这个中间件会获取query中携带的name去找到指定的Stream,然后会将客户端的Stream和获取的server的Stream进行Copy,在这里他们会将读取的数据写入到对方的流中,这样就实现了双工通信

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Timeouts;
using Taibai.Core;

namespace Taibai.Server;

public class ClientMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        
        // 如果不是http2协议,我们不处理, 因为我们只支持http2
        if (context.Request.Protocol != HttpProtocol.Http2)
        {
            return;
        }

        var name = context.Request.Query["name"];

        if (string.IsNullOrEmpty(name))
        {
            context.Response.StatusCode = 400;
            Console.WriteLine("Name is required");
            return;
        }
        
        Console.WriteLine("Accepted connection from " + name);

        var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();
        context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();

        // 得到双工流
        var stream = new SafeWriteStream(await http2Feature.AcceptAsync());

        // 通过name找到指定的server链接,然后进行转发。
        var (cancellationToken, reader) = ServerService.GetConnectionChannel(name);

        try
        {
            // 注册取消连接
            cancellationToken.Register(() =>
            {
                Console.WriteLine("断开连接");
                stream.Close();
            });

            // 得到客户端的流,然后给我们的SafeWriteStream,然后我们就可以进行转发了
            var socketStream = new SafeWriteStream(reader);

            // 在这里他们会将读取的数据写入到对方的流中,这样就实现了双工通信,这个非常简单并且性能也不错。
            await Task.WhenAll(
                stream.CopyToAsync(socketStream, context.RequestAborted),
                socketStream.CopyToAsync(stream, context.RequestAborted)
            );
        }
        catch (Exception e)
        {
            Console.WriteLine("断开连接" + e.Message);
            throw;
        }
    }
}

打开Program.cs

using Taibai.Server;

var builder = WebApplication.CreateBuilder(new WebApplicationOptions());

builder.Host.ConfigureHostOptions(host => { host.ShutdownTimeout = TimeSpan.FromSeconds(1d); });

builder.Services.AddSingleton<ClientMiddleware>();

var app = builder.Build();

app.Map("/server", app =>
{
    app.Use(Middleware);

    static async Task Middleware(HttpContext context, RequestDelegate _)
    {
        await ServerService.StartAsync(context);
    }
});

app.Map("/client", app => { app.UseMiddleware<ClientMiddleware>(); });

app.Run();

在这里我们将server的所有路由都交过ServerService.StartAsync接管,再server会请求这个地址,

/client则给了ClientMiddleware中间件。

创建客户端

上面我们实现了服务端,其实服务端可以完全放置到现有的WebApi项目当中的,而且代码也不是很多。

客户端我们创建一个控制台项目名:Taibai.Client,并且依赖Taibai.Core项目

由于我们的客户端有些特殊,再server中部署的它不需要监听端口,它只需要将服务器的数据转发到指定的一个地址即可,所以我们需要将客户端的server部署的和本地部署的分开实现,再服务器部署的客户端我们命名为MonitorClient.cs

ClientOption.cs用于传递我们的客户端地址配置

public class ClientOption
{
    /// <summary>
    /// 服务地址
    /// </summary>
    public string ServiceUri { get; set; }
    
}

MonitorClient.cs,作为服务器的转发客户端。

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using Taibai.Core;

namespace Taibai.Client;

public class MonitorClient(ClientOption option)
{
    private string Protocol = "taibai";
    private readonly HttpMessageInvoker httpClient = new(CreateDefaultHttpHandler(), true);
    private readonly Socket socket = new(SocketType.Stream, ProtocolType.Tcp);

    private static SocketsHttpHandler CreateDefaultHttpHandler()
    {
        return new SocketsHttpHandler
        {
            // 允许多个http2连接
            EnableMultipleHttp2Connections = true,
            // 设置连接超时时间
            ConnectTimeout = TimeSpan.FromSeconds(60),
            SslOptions = new SslClientAuthenticationOptions
            {
                // 由于我们没有证书,所以我们需要设置为true
                RemoteCertificateValidationCallback = (_, _, _, _) => true,
            },
        };
    }

    public async Task TransportAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("链接中!");

        // 由于是测试,我们就目前先写死远程地址
        await socket.ConnectAsync(new IPEndPoint(IPAddress.Parse("192.168.31.250"), 3389), cancellationToken);

        Console.WriteLine("连接成功");

        // 将Socket转换为流
        var stream = new NetworkStream(socket);
        try
        {
            // 创建服务器的连接,然后返回一个流,这个是H2的流
            var serverStream = await this.CreateServerConnectionAsync(cancellationToken);

            Console.WriteLine("链接服务器成功");

            // 将两个流连接起来,这样我们就可以进行双工通信了。它们会自动进行数据的传输。
            await Task.WhenAll(
                stream.CopyToAsync(serverStream, cancellationToken),
                serverStream.CopyToAsync(stream, cancellationToken)
            );
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw;
        }
    }

    /// <summary>
    /// 创建服务器的连接
    /// </summary> 
    /// <param name="cancellationToken"></param>
    /// <exception cref="OperationCanceledException"></exception>
    /// <returns></returns>
    public async Task<SafeWriteStream> CreateServerConnectionAsync(CancellationToken cancellationToken)
    {
        var stream = await Http20ConnectServerAsync(cancellationToken);
        return new SafeWriteStream(stream);
    }

    /// <summary>
    /// 创建http2连接
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    private async Task<Stream> Http20ConnectServerAsync(CancellationToken cancellationToken)
    {
        var serverUri = new Uri(option.ServiceUri);
        // 这里我们使用Connect方法,因为我们需要建立一个双工流, 这样我们就可以进行双工通信了。
        var request = new HttpRequestMessage(HttpMethod.Connect, serverUri);
        // 如果设置了Connect,那么我们需要设置Protocol
        request.Headers.Protocol = Protocol;
        // 我们需要设置http2的版本
        request.Version = HttpVersion.Version20;
        
        // 我们需要确保我们的请求是http2的
        request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;

        // 设置一下超时时间,这样我们就可以在超时的时候取消连接了。
        using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
        using var linkedTokenSource =
            CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);

        // 发送请求,然后等待响应
        var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token);

        // 返回h2的流,用于传输数据
        return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token);
    }
}

创建我们的本地客户端实现类。

Client.cs这个就是在我们本地部署的服务,然后会监听本地的60112的端口,然后会吧这个端口的数据转发到我们的服务器,然后服务器会根据我们使用的name去找到指定的客户端进行交互传输。

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using Taibai.Core;
using HttpMethod = System.Net.Http.HttpMethod;

namespace Taibai.Client;


public class Client
{
    private readonly ClientOption option;

    private string Protocol = "taibai";
    private readonly HttpMessageInvoker httpClient;
    private readonly Socket socket;

    public Client(ClientOption option)
    {
        this.option = option;
        this.httpClient = new HttpMessageInvoker(CreateDefaultHttpHandler(), true);

        this.socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

        // 监听本地端口
        this.socket.Bind(new IPEndPoint(IPAddress.Loopback, 60112));
        this.socket.Listen(10);
    }

    private static SocketsHttpHandler CreateDefaultHttpHandler()
    {
        return new SocketsHttpHandler
        {
            // 允许多个http2连接
            EnableMultipleHttp2Connections = true,
            ConnectTimeout = TimeSpan.FromSeconds(60),
            ResponseDrainTimeout = TimeSpan.FromSeconds(60),  
            SslOptions = new SslClientAuthenticationOptions
            {
                // 由于我们没有证书,所以我们需要设置为true
                RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
            },
        };
    }

    public async Task TransportAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Listening on 60112");

        // 等待客户端连接
        var client = await this.socket.AcceptAsync(cancellationToken);

        Console.WriteLine("Accepted connection from " + client.RemoteEndPoint);

        try
        {
            // 将Socket转换为流
            var stream = new NetworkStream(client);

            // 创建服务器的连接,然后返回一个流, 这个是H2的流
            var serverStream = await this.CreateServerConnectionAsync(cancellationToken);

            Console.WriteLine("Connected to server");

            // 将两个流连接起来, 这样我们就可以进行双工通信了. 它们会自动进行数据的传输.
            await Task.WhenAll(
                stream.CopyToAsync(serverStream, cancellationToken),
                serverStream.CopyToAsync(stream, cancellationToken)
            );
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    /// <summary>
    /// 创建与服务器的连接
    /// </summary> 
    /// <param name="cancellationToken"></param>
    /// <exception cref="OperationCanceledException"></exception>
    /// <returns></returns>
    public async Task<SafeWriteStream> CreateServerConnectionAsync(CancellationToken cancellationToken)
    {
        var stream = await this.Http20ConnectServerAsync(cancellationToken);
        return new SafeWriteStream(stream);
    }

    private async Task<Stream> Http20ConnectServerAsync(CancellationToken cancellationToken)
    {
        var serverUri = new Uri(option.ServiceUri);
        // 这里我们使用Connect方法, 因为我们需要建立一个双工流
        var request = new HttpRequestMessage(HttpMethod.Connect, serverUri);

        // 由于我们设置了Connect方法, 所以我们需要设置协议,这样服务器才能识别
        request.Headers.Protocol = Protocol;
        // 设置http2版本
        request.Version = HttpVersion.Version20;
        // 强制使用http2
        request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;

        using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
        using var linkedTokenSource =
            CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);

        // 发送请求,等待服务器验证。
        var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token);

        // 返回一个流
        return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token);
    }
}

然后再Program.cs中,我们封装一个简单的控制台版本。

using Taibai.Client;

const string commandTemplate = @"

当前是 Taibai 客户端,输入以下命令:

- `help` 显示帮助
- `monitor` 使用监控模式,监听本地端口,将流量转发到服务端的指定地址
    - `monitor=https://localhost:7153/server?name=test`  监听本地端口,将流量转发到服务端指定的客户端名称为 test 的地址
- `client` 使用客户端模式,连接服务端的指定地址,将流量转发到本地端口
    - `client=https://localhost:7153/client?name=test`  连接服务端指定当前客户端名称为 test,将流量转发到本地端口
- `exit` 退出

输入命令:

";

while (true)
{
    Console.WriteLine(commandTemplate);

    var command = Console.ReadLine();


    if (command?.StartsWith("monitor=") == true)
    {
        var client = new MonitorClient(new ClientOption()
        {
            ServiceUri = command[8..]
        });

        await client.TransportAsync(new CancellationToken());
    }
    else if (command?.StartsWith("client=") == true)
    {
        var client = new Client(new ClientOption()
        {
            ServiceUri = command[7..]
        });

        await client.TransportAsync(new CancellationToken());
    }
    else if (command == "help")
    {
        Console.WriteLine(commandTemplate);
    }
    else if (command == "exit")
    {
        Console.WriteLine("Bye!");
        break;
    }
    else
    {
        Console.WriteLine("未知命令");
    }
}

我们默认提供了命令去使用指定的一个模式去链接客户端,

然后我们发布一下Taibai.Client,发布完成以后我们使用ide启动我们的Taibai.Server,请注意我们需要使用HTTPS进行启动的,HTTP是不支持H2的!

然后再客户端中打开俩个控制台面板,一个作为监听的monitor,一个作为client进行链接到我们的服务器中。

然后我们使用远程桌面访问我们的127.0.0.1:60112,然后我们发现链接成功!如果您跟着写代码您会您发您也成功了,哦耶您获得了一个牛逼的技能,来源于微软MVP token的双休大法的传授!

技术交流分享

来自微软MVP token

token | 最有价值专家 (microsoft.com)

技术交流群:737776595

当然如果您需要Demo的代码您可以联系我微信wk28u9123456789

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

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

相关文章

《ElementPlus 与 ElementUI 差异集合》el-dialog 显示属性有差异

ElementPlus 用属性 v-model ElementUI 用属性 visible 其实也是 Vue2/Vue3 的差异&#xff1a;v-model 指令在组件上的使用已经被重新设计&#xff0c;替换掉了 v-bind.sync

新质生产力实践,我用chatgpt开发网站

是的&#xff0c;我用chatgpt开发了一个网站&#xff0c;很轻松。 我之前一点不懂前端&#xff0c;也没有网站开发的代码基础&#xff0c;纯正的0基础。 从0开始到最后成品上线&#xff0c;时间总计起来大致一共花了2-3周的时间。 初始想法我是想给我公司开发一个网站&#…

C# wpf 运行时替换方法实现mvvm自动触发刷新

文章目录 前言一、如何实现&#xff1f;1、反射获取属性2、定义替换方法3、交换属性的setter方法 二、完整代码1、接口2、项目 三、使用示例1、倒计时&#xff08;1&#xff09;、继承ViewModelBase&#xff08;2&#xff09;、定义属性&#xff08;3&#xff09;、属性赋值&am…

【Redis 开发】Lua语言

Lua Lua语法 Lua语法 Lua是一种小巧的脚本语言&#xff0c;底层用C语言实现&#xff0c;为了嵌入式应用程序中 官网&#xff1a;https://www.lua.org/ 创建lua文件 touch hello.lua 运行lua文件 lua hello.lua 输出语句 print("Hello World!")数据类型 可以通过t…

python与上位机开发day04

模块和包、异常、PyQt5 一、模块和包 1.1 模块 Python中模块就是一个.py文件&#xff0c;模块中可以定义函数&#xff0c;变量&#xff0c;类。模块可以被其他模块引用 1.1.1 导入模块 """ 导入格式1&#xff1a; import 模块名 使用格式&#xff1a; …

【算法基础实验】图论-最小生成树Prim的延迟实现

最小生成树-Prim的延迟实现 理论基础 树的基本性质 用一条边连接树中的任意两个顶点都会产生一个新的环&#xff1b; 从树中删去一条边将会得到两棵独立的树。 切分定理的定义 定义。图的一种切分是将图的所有顶点分为两个非空且不重叠的两个集合。横切边 是一条连接两个属…

认识认识DHCP

文章目录 认识认识DHCP一、什么是DHCP&#xff1f;1.1、为什么要使用DHCP&#xff1f;1.2、DHCP是怎么工作的&#xff1f;1.2.1、客户端首次接入网络的工作原理1.2.2、客户端重用曾经使用过的地址的工作原理1.2.3、客户端更新租期的工作原理 二、配置DHCP Server&#xff0c;为…

嵌入式开发二:搭建开发环境

工欲善其事必先利其器&#xff0c;本节我们从嵌入式开发的搭建环境开始学习&#xff0c;掌握最基本的搭建环境技能&#xff0c;了解每一部分的作用&#xff0c;以及如何使用是关键所在&#xff01; 目录 一、常用开发工具简介 二、如何安装KEIL5(MDK) 2.1认识MDK-ARM 2.2 …

Spring基于AspectJ实现验签切点

文章目录 引言I AspectJ 依赖II 验签切点2.1 匹配方法执行的连接点2.2 设置带有CustomAnnotation注解的方法为切点III 案例:验签2.1 用法2.2 定义注解2.3 定义切面和切点引言 需求:验签 实现:基于AspectJ实现验签切点 I AspectJ 依赖 AspectJ 是一个基于 Java 语言的 AOP …

windos蓝屏分析工具

BlueScreenView中文版 - Windows蓝屏分析工具 BlueScreenView中文版是一款非常方便的蓝屏诊断工具。它可以帮助您快速定位蓝屏问题&#xff0c;并提供详细的故障转储信息。该软件可以自动扫描您机器上的minidump文件夹&#xff0c;同时还支持根据路径查找蓝屏文件。 windos发…

Coursera: An Introduction to American Law 学习笔记 Week 06: Civil Procedure (完结)

An Introduction to American Law Course Certificate Course Introduction 本文是 https://www.coursera.org/programs/career-training-for-nevadans-k7yhc/learn/american-law 这门课的学习笔记。 文章目录 An Introduction to American LawInstructors Week 06: Civil Pro…

Flink checkpoint 源码分析

序言 最近因为工作需要在阅读flink checkpoint处理机制&#xff0c;学习的过程中记录下来&#xff0c;并分享给大家。也算是学习并记录。 目前公司使用的flink版本为1.11。因此以下的分析都是基于1.11版本来的。 在分享前可以简单对flink checkpoint机制做一个大致的了解。 …

docker学习笔记3:VmWare CentOS7安装与静态ip配置

文章目录 一、安装CentOS71、下载centos镜像2、安装二、设置静态ip三、xshell连接centos本专栏的docker环境是在centos7里安装,因此首先需要会安装centos虚拟机。 本篇博客介绍如何在vm虚拟机里安装centos7。 一、安装CentOS7 1、下载centos镜像 推荐清华源,下载如下版本 …

【C++】学习笔记——string_1

文章目录 四、模板初阶2. 类模板 五、STL简介1. 什么是STL2. STL的六大组件3. 如何学习STL 六、string类1. string类对象的容量操作 未完待续 四、模板初阶 2. 类模板 函数模板就是&#xff1a;模板 函数&#xff1b;类模板就是&#xff1a;模板 类。和函数模板用法基本相同…

Kafka客户端工具:Offset Explorer 使用指南

Kafka作为一个分布式流处理平台&#xff0c;在大数据处理和实时数据流应用中扮演着至关重要的角色。管理Kafka的topics及其offsets对于维护系统稳定性和数据一致性至关重要。Offset Explorer是一个强大的桌面应用程序&#xff0c;它使得管理和监控Kafka集群变得简单直观。本文将…

数组 Leetcode 704 二分查找/Leetcode 59 螺旋矩阵/Leetcode 203移除链表元素

数组 Leetcode 704 二分查找 Leetcode 704 学习记录自代码随想录 二分法模板记忆&#xff0c;数值分析中牛顿迭代法 class Solution { public:int search(vector<int>& nums, int target) {int left 0, right nums.size()-1;// 是否需要等于号&#xff0c;假设…

verilog分析task的接口设计,证明这种写法:assign {a,b,c,d} = links;

verilog分析task的接口设计&#xff0c;证明这种写法&#xff1a;assign {a,b,c,d} links; 1&#xff0c;task在状态机中的使用好处&#xff1a;2&#xff0c;RTL设计3&#xff0c;测试testbench4&#xff0c;波形分析&#xff0c;正确&#xff01; 参考文献&#xff1a; 1&am…

C++初阶学习第四弹——类与对象(中)——刨析类与对象的核心点

类与对象&#xff08;上&#xff09;&#xff1a;C初阶学习第三弹——类与对象&#xff08;上&#xff09;——初始类与对象-CSDN博客 前言&#xff1a; 在前面文章中&#xff0c;我们已经讲了类与对象的思想和类与对象的一些基本操作&#xff0c;接下来这篇文章我们将讲解以下…

NLP(10)--TFIDF优劣势及其应用Demo

前言 仅记录学习过程&#xff0c;有问题欢迎讨论 TF*IDF&#xff1a; 优势&#xff1a; 可解释性好 可以清晰地看到关键词 即使预测结果出错&#xff0c;也很容易找到原因 计算速度快 分词本身占耗时最多&#xff0c;其余为简单统计计算 对标注数据依赖小 可以使用无标注语…

超级数据查看器 app v2.0发布 欢迎下载使用

超级数据查看器 app v2.0发布 欢迎下载使用 感谢大家的支持 &#xff1a;&#xff09; 点击访问APP下载界面 跳转 腾讯应用宝 简介 超级数据查看器软件&#xff08;简称超级数据查看器&#xff09;是一个提供数据查询和数据管理的手机APP&#xff0c;能导入文本数据&…