可能有人很疑惑應(yīng)用層 轉(zhuǎn)發(fā)傳輸層?,為什么會(huì)有這樣的需求?????哈哈技術(shù)無所不用其極,由于一些場景下,對(duì)于一個(gè)服務(wù)器存在某一個(gè)內(nèi)部網(wǎng)站中,但是對(duì)于這個(gè)服務(wù)器它沒有訪問外網(wǎng)的權(quán)限,雖然也可以申請端口訪問外部指定的ip+端口,但是對(duì)于訪問服務(wù)內(nèi)部的TCP的時(shí)候我們就會(huì)發(fā)現(xiàn)忘記申請了!這個(gè)時(shí)候我們又要提交申請,又要等審批,然后開通端口,對(duì)于這個(gè)步驟不是一般的麻煩,所以我在想是否可以直接利用現(xiàn)有的Http網(wǎng)關(guān)的端口進(jìn)行轉(zhuǎn)發(fā)內(nèi)部的TCP服務(wù)?這個(gè)時(shí)候我詢問了我們的老九大佬,由于我之前也做過通過H2實(shí)現(xiàn)HTTP內(nèi)網(wǎng)穿透,可以利用H2將內(nèi)部網(wǎng)絡(luò)中的服務(wù)映射出來,但是由于底層是基于yarp的一些方法實(shí)現(xiàn),所以并沒有考慮過TCP,然后于老九大佬交流深究,決定嘗試驗(yàn)證可行性,然后我們的Taibai項(xiàng)目就誕生了,為什么叫Taibai?您仔細(xì)看看這個(gè)拼音,翻譯過來就是太白,確實(shí)全稱應(yīng)該叫太白金星,寓意上天遁地?zé)o所不能!下面我們介紹一下具體實(shí)現(xiàn)邏輯,確實(shí)您仔細(xì)看會(huì)發(fā)現(xiàn)實(shí)現(xiàn)是真的超級(jí)簡單的!
創(chuàng)建項(xiàng)目名Taibai.Core
下面幾個(gè)方法都是用于操作Stream的類
DelegatingStream.cs
namespace Taibai.Core;/// <summary>/// 委托流/// </summary>public abstract class DelegatingStream : Stream{ /// <summary> /// 獲取所包裝的流對(duì)象 /// </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(); }}
創(chuàng)建一個(gè)WebAPI的項(xiàng)目項(xiàng)目名Taibai.Server并且依賴Taibai.Core項(xiàng)目
創(chuàng)建ServerService.cs,這個(gè)類是用于管理內(nèi)網(wǎng)的客戶端的,這個(gè)一般是部署在內(nèi)網(wǎng)服務(wù)器上,用于將內(nèi)網(wǎng)的端口映射出來,但是我們的Demo只實(shí)現(xiàn)了簡單的管理不做端口的管理。
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協(xié)議,我們不處理, 因?yàn)槲覀冎恢С謍ttp2 if (context.Request.Protocol != HttpProtocol.Http2) { return; } // 獲取query var query = context.Request.Query; // 我們需要強(qiáng)制要求name參數(shù) 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>(); // 禁用超時(shí) context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout(); // 得到雙工流 var stream = new SafeWriteStream(await http2Feature.AcceptAsync()); // 將其添加到集合中,以便我們可以在其他地方使用 CreateConnectionChannel(name, context.RequestAborted, stream); // 注冊取消連接 context.RequestAborted.Register(() => { // 當(dāng)取消時(shí),我們需要從集合中刪除 ClusterConnections.TryRemove(name, out _); }); // 由于我們需要保持連接,所以我們需要等待,直到客戶端主動(dòng)斷開連接。 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)); }}
然后再創(chuàng)建ClientMiddleware.cs,并且繼承IMiddleware,這個(gè)是我們本地使用的客戶端鏈接的時(shí)候進(jìn)入的中間件,再這個(gè)中間件會(huì)獲取query中攜帶的name去找到指定的Stream,然后會(huì)將客戶端的Stream和獲取的server的Stream進(jìn)行Copy,在這里他們會(huì)將讀取的數(shù)據(jù)寫入到對(duì)方的流中,這樣就實(shí)現(xiàn)了雙工通信
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協(xié)議,我們不處理, 因?yàn)槲覀冎恢С謍ttp2 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鏈接,然后進(jìn)行轉(zhuǎn)發(fā)。 var (cancellationToken, reader) = ServerService.GetConnectionChannel(name); try { // 注冊取消連接 cancellationToken.Register(() => { Console.WriteLine("斷開連接"); stream.Close(); }); // 得到客戶端的流,然后給我們的SafeWriteStream,然后我們就可以進(jìn)行轉(zhuǎn)發(fā)了 var socketStream = new SafeWriteStream(reader); // 在這里他們會(huì)將讀取的數(shù)據(jù)寫入到對(duì)方的流中,這樣就實(shí)現(xiàn)了雙工通信,這個(gè)非常簡單并且性能也不錯(cuò)。 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會(huì)請求這個(gè)地址,
而/client則給了ClientMiddleware中間件。
上面我們實(shí)現(xiàn)了服務(wù)端,其實(shí)服務(wù)端可以完全放置到現(xiàn)有的WebApi項(xiàng)目當(dāng)中的,而且代碼也不是很多。
客戶端我們創(chuàng)建一個(gè)控制臺(tái)項(xiàng)目名:Taibai.Client,并且依賴Taibai.Core項(xiàng)目
由于我們的客戶端有些特殊,再server中部署的它不需要監(jiān)聽端口,它只需要將服務(wù)器的數(shù)據(jù)轉(zhuǎn)發(fā)到指定的一個(gè)地址即可,所以我們需要將客戶端的server部署的和本地部署的分開實(shí)現(xiàn),再服務(wù)器部署的客戶端我們命名為MonitorClient.cs
ClientOption.cs用于傳遞我們的客戶端地址配置
public class ClientOption{ /// <summary> /// 服務(wù)地址 /// </summary> public string ServiceUri { get; set; } }
MonitorClient.cs,作為服務(wù)器的轉(zhuǎn)發(fā)客戶端。
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 { // 允許多個(gè)http2連接 EnableMultipleHttp2Connections = true, // 設(shè)置連接超時(shí)時(shí)間 ConnectTimeout = TimeSpan.FromSeconds(60), SslOptions = new SslClientAuthenticationOptions { // 由于我們沒有證書,所以我們需要設(shè)置為true RemoteCertificateValidationCallback = (_, _, _, _) => true, }, }; } public async Task TransportAsync(CancellationToken cancellationToken) { Console.WriteLine("鏈接中!"); // 由于是測試,我們就目前先寫死遠(yuǎn)程地址 await socket.ConnectAsync(new IPEndPoint(IPAddress.Parse("192.168.31.250"), 3389), cancellationToken); Console.WriteLine("連接成功"); // 將Socket轉(zhuǎn)換為流 var stream = new NetworkStream(socket); try { // 創(chuàng)建服務(wù)器的連接,然后返回一個(gè)流,這個(gè)是H2的流 var serverStream = await this.CreateServerConnectionAsync(cancellationToken); Console.WriteLine("鏈接服務(wù)器成功"); // 將兩個(gè)流連接起來,這樣我們就可以進(jìn)行雙工通信了。它們會(huì)自動(dòng)進(jìn)行數(shù)據(jù)的傳輸。 await Task.WhenAll( stream.CopyToAsync(serverStream, cancellationToken), serverStream.CopyToAsync(stream, cancellationToken) ); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } /// <summary> /// 創(chuàng)建服務(wù)器的連接 /// </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> /// 創(chuàng)建http2連接 /// </summary> /// <param name="cancellationToken"></param> /// <returns></returns> private async Task<Stream> Http20ConnectServerAsync(CancellationToken cancellationToken) { var serverUri = new Uri(option.ServiceUri); // 這里我們使用Connect方法,因?yàn)槲覀冃枰⒁粋€(gè)雙工流, 這樣我們就可以進(jìn)行雙工通信了。 var request = new HttpRequestMessage(HttpMethod.Connect, serverUri); // 如果設(shè)置了Connect,那么我們需要設(shè)置Protocol request.Headers.Protocol = Protocol; // 我們需要設(shè)置http2的版本 request.Version = HttpVersion.Version20; // 我們需要確保我們的請求是http2的 request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; // 設(shè)置一下超時(shí)時(shí)間,這樣我們就可以在超時(shí)的時(shí)候取消連接了。 using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken); // 發(fā)送請求,然后等待響應(yīng) var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token); // 返回h2的流,用于傳輸數(shù)據(jù) return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token); }}
創(chuàng)建我們的本地客戶端實(shí)現(xiàn)類。
Client.cs這個(gè)就是在我們本地部署的服務(wù),然后會(huì)監(jiān)聽本地的60112的端口,然后會(huì)吧這個(gè)端口的數(shù)據(jù)轉(zhuǎn)發(fā)到我們的服務(wù)器,然后服務(wù)器會(huì)根據(jù)我們使用的name去找到指定的客戶端進(jìn)行交互傳輸。
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); // 監(jiān)聽本地端口 this.socket.Bind(new IPEndPoint(IPAddress.Loopback, 60112)); this.socket.Listen(10); } private static SocketsHttpHandler CreateDefaultHttpHandler() { return new SocketsHttpHandler { // 允許多個(gè)http2連接 EnableMultipleHttp2Connections = true, ConnectTimeout = TimeSpan.FromSeconds(60), ResponseDrainTimeout = TimeSpan.FromSeconds(60), SslOptions = new SslClientAuthenticationOptions { // 由于我們沒有證書,所以我們需要設(shè)置為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轉(zhuǎn)換為流 var stream = new NetworkStream(client); // 創(chuàng)建服務(wù)器的連接,然后返回一個(gè)流, 這個(gè)是H2的流 var serverStream = await this.CreateServerConnectionAsync(cancellationToken); Console.WriteLine("Connected to server"); // 將兩個(gè)流連接起來, 這樣我們就可以進(jìn)行雙工通信了. 它們會(huì)自動(dòng)進(jìn)行數(shù)據(jù)的傳輸. await Task.WhenAll( stream.CopyToAsync(serverStream, cancellationToken), serverStream.CopyToAsync(stream, cancellationToken) ); } catch (Exception e) { Console.WriteLine(e); throw; } } /// <summary> /// 創(chuàng)建與服務(wù)器的連接 /// </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方法, 因?yàn)槲覀冃枰⒁粋€(gè)雙工流 var request = new HttpRequestMessage(HttpMethod.Connect, serverUri); // 由于我們設(shè)置了Connect方法, 所以我們需要設(shè)置協(xié)議,這樣服務(wù)器才能識(shí)別 request.Headers.Protocol = Protocol; // 設(shè)置http2版本 request.Version = HttpVersion.Version20; // 強(qiáng)制使用http2 request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken); // 發(fā)送請求,等待服務(wù)器驗(yàn)證。 var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token); // 返回一個(gè)流 return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token); }}
然后再Program.cs中,我們封裝一個(gè)簡單的控制臺(tái)版本。
using Taibai.Client;const string commandTemplate = @"當(dāng)前是 Taibai 客戶端,輸入以下命令:- `help` 顯示幫助- `monitor` 使用監(jiān)控模式,監(jiān)聽本地端口,將流量轉(zhuǎn)發(fā)到服務(wù)端的指定地址 - `monitor=https://localhost:7153/server?name=test` 監(jiān)聽本地端口,將流量轉(zhuǎn)發(fā)到服務(wù)端指定的客戶端名稱為 test 的地址- `client` 使用客戶端模式,連接服務(wù)端的指定地址,將流量轉(zhuǎn)發(fā)到本地端口 - `client=https://localhost:7153/client?name=test` 連接服務(wù)端指定當(dāng)前客戶端名稱為 test,將流量轉(zhuǎn)發(fā)到本地端口- `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("未知命令"); }}
我們默認(rèn)提供了命令去使用指定的一個(gè)模式去鏈接客戶端,
然后我們發(fā)布一下Taibai.Client,發(fā)布完成以后我們使用ide啟動(dòng)我們的Taibai.Server,請注意我們需要使用HTTPS進(jìn)行啟動(dòng)的,HTTP是不支持H2的!
然后再客戶端中打開倆個(gè)控制臺(tái)面板,一個(gè)作為監(jiān)聽的monitor,一個(gè)作為client進(jìn)行鏈接到我們的服務(wù)器中。
圖片
圖片
然后我們使用遠(yuǎn)程桌面訪問我們的127.0.0.1:60112,然后我們發(fā)現(xiàn)鏈接成功!如果您跟著寫代碼您會(huì)您發(fā)您也成功了,哦耶您獲得了一個(gè)牛逼的技能,來源于微軟MVP token的雙休大法的傳授!
本文鏈接:http://www.tebozhan.com/showinfo-26-87050-0.html您可知道如何通過HTTP2實(shí)現(xiàn)TCP的內(nèi)網(wǎng)穿透?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: Python 網(wǎng)絡(luò)爬蟲利器:執(zhí)行 JavaScript 實(shí)現(xiàn)數(shù)據(jù)抓取
下一篇: DevSecOps 是什么?你知道嗎?