您可知道如何通過HTTP2實現(xiàn)TCP的內(nèi)網(wǎng)穿透?
可能有人很疑惑應(yīng)用層 轉(zhuǎn)發(fā)傳輸層?,為什么會有這樣的需求?????哈哈技術(shù)無所不用其極,由于一些場景下,對于一個服務(wù)器存在某一個內(nèi)部網(wǎng)站中,但是對于這個服務(wù)器它沒有訪問外網(wǎng)的權(quán)限,雖然也可以申請端口訪問外部指定的ip+端口,但是對于訪問服務(wù)內(nèi)部的TCP的時候我們就會發(fā)現(xiàn)忘記申請了!這個時候我們又要提交申請,又要等審批,然后開通端口,對于這個步驟不是一般的麻煩,所以我在想是否可以直接利用現(xiàn)有的Http網(wǎng)關(guān)的端口進(jìn)行轉(zhuǎn)發(fā)內(nèi)部的TCP服務(wù)?這個時候我詢問了我們的老九大佬,由于我之前也做過通過H2實現(xiàn)HTTP內(nèi)網(wǎng)穿透,可以利用H2將內(nèi)部網(wǎng)絡(luò)中的服務(wù)映射出來,但是由于底層是基于yarp的一些方法實現(xiàn),所以并沒有考慮過TCP,然后于老九大佬交流深究,決定嘗試驗證可行性,然后我們的Taibai項目就誕生了,為什么叫Taibai?您仔細(xì)看看這個拼音,翻譯過來就是太白,確實全稱應(yīng)該叫太白金星,寓意上天遁地?zé)o所不能!下面我們介紹一下具體實現(xiàn)邏輯,確實您仔細(xì)看會發(fā)現(xiàn)實現(xiàn)是真的超級簡單的!
創(chuàng)建Core項目用于共用的核心類庫
創(chuàng)建項目名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();
}
}
創(chuàng)建服務(wù)端
創(chuàng)建一個WebAPI的項目項目名Taibai.Server并且依賴Taibai.Core項目
創(chuàng)建ServerService.cs,這個類是用于管理內(nèi)網(wǎng)的客戶端的,這個一般是部署在內(nèi)網(wǎng)服務(wù)器上,用于將內(nèi)網(wǎng)的端口映射出來,但是我們的Demo只實現(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é)議,我們不處理, 因為我們只支持http2
if (context.Request.Protocol != HttpProtocol.Http2)
{
return;
}
// 獲取query
var query = context.Request.Query;
// 我們需要強制要求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>();
// 禁用超時
context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();
// 得到雙工流
var stream = new SafeWriteStream(await http2Feature.AcceptAsync());
// 將其添加到集合中,以便我們可以在其他地方使用
CreateConnectionChannel(name, context.RequestAborted, stream);
// 注冊取消連接
context.RequestAborted.Register(() =>
{
// 當(dāng)取消時,我們需要從集合中刪除
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));
}
}
然后再創(chuàng)建ClientMiddleware.cs,并且繼承IMiddleware,這個是我們本地使用的客戶端鏈接的時候進(jìn)入的中間件,再這個中間件會獲取query中攜帶的name去找到指定的Stream,然后會將客戶端的Stream和獲取的server的Stream進(jìn)行Copy,在這里他們會將讀取的數(shù)據(jù)寫入到對方的流中,這樣就實現(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é)議,我們不處理, 因為我們只支持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鏈接,然后進(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);
// 在這里他們會將讀取的數(shù)據(jù)寫入到對方的流中,這樣就實現(xiàn)了雙工通信,這個非常簡單并且性能也不錯。
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中間件。
創(chuàng)建客戶端
上面我們實現(xiàn)了服務(wù)端,其實服務(wù)端可以完全放置到現(xiàn)有的WebApi項目當(dāng)中的,而且代碼也不是很多。
客戶端我們創(chuàng)建一個控制臺項目名:Taibai.Client,并且依賴Taibai.Core項目
由于我們的客戶端有些特殊,再server中部署的它不需要監(jiān)聽端口,它只需要將服務(wù)器的數(shù)據(jù)轉(zhuǎn)發(fā)到指定的一個地址即可,所以我們需要將客戶端的server部署的和本地部署的分開實現(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
{
// 允許多個http2連接
EnableMultipleHttp2Connections = true,
// 設(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ù)器的連接,然后返回一個流,這個是H2的流
var serverStream = await this.CreateServerConnectionAsync(cancellationToken);
Console.WriteLine("鏈接服務(wù)器成功");
// 將兩個流連接起來,這樣我們就可以進(jìn)行雙工通信了。它們會自動進(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方法,因為我們需要建立一個雙工流, 這樣我們就可以進(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è)置一下超時時間,這樣我們就可以在超時的時候取消連接了。
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)建我們的本地客戶端實現(xiàn)類。
Client.cs這個就是在我們本地部署的服務(wù),然后會監(jiān)聽本地的60112的端口,然后會吧這個端口的數(shù)據(jù)轉(zhuǎn)發(fā)到我們的服務(wù)器,然后服務(wù)器會根據(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
{
// 允許多個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ù)器的連接,然后返回一個流, 這個是H2的流
var serverStream = await this.CreateServerConnectionAsync(cancellationToken);
Console.WriteLine("Connected to server");
// 將兩個流連接起來, 這樣我們就可以進(jìn)行雙工通信了. 它們會自動進(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方法, 因為我們需要建立一個雙工流
var request = new HttpRequestMessage(HttpMethod.Connect, serverUri);
// 由于我們設(shè)置了Connect方法, 所以我們需要設(shè)置協(xié)議,這樣服務(wù)器才能識別
request.Headers.Protocol = Protocol;
// 設(shè)置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);
// 發(fā)送請求,等待服務(wù)器驗證。
var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token);
// 返回一個流
return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token);
}
}
然后再Program.cs中,我們封裝一個簡單的控制臺版本。
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)提供了命令去使用指定的一個模式去鏈接客戶端,
然后我們發(fā)布一下Taibai.Client,發(fā)布完成以后我們使用ide啟動我們的Taibai.Server,請注意我們需要使用HTTPS進(jìn)行啟動的,HTTP是不支持H2的!
然后再客戶端中打開倆個控制臺面板,一個作為監(jiān)聽的monitor,一個作為client進(jìn)行鏈接到我們的服務(wù)器中。
圖片
圖片
然后我們使用遠(yuǎn)程桌面訪問我們的127.0.0.1:60112,然后我們發(fā)現(xiàn)鏈接成功!如果您跟著寫代碼您會您發(fā)您也成功了,哦耶您獲得了一個牛逼的技能,來源于微軟MVP token的雙休大法的傳授!