经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
您可知道如何通过`HTTP2`实现TCP的内网穿透???
来源:cnblogs  作者:tokengo  时间:2024/5/6 16:20:35  对本文有异议

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

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

创建项目名Taibai.Core

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

DelegatingStream.cs

  1. namespace Taibai.Core;
  2. /// <summary>
  3. /// 委托流
  4. /// </summary>
  5. public abstract class DelegatingStream : Stream
  6. {
  7. /// <summary>
  8. /// 获取所包装的流对象
  9. /// </summary>
  10. protected readonly Stream Inner;
  11. /// <summary>
  12. /// 委托流
  13. /// </summary>
  14. /// <param name="inner"></param>
  15. public DelegatingStream(Stream inner)
  16. {
  17. this.Inner = inner;
  18. }
  19. /// <inheritdoc/>
  20. public override bool CanRead => Inner.CanRead;
  21. /// <inheritdoc/>
  22. public override bool CanSeek => Inner.CanSeek;
  23. /// <inheritdoc/>
  24. public override bool CanWrite => Inner.CanWrite;
  25. /// <inheritdoc/>
  26. public override long Length => Inner.Length;
  27. /// <inheritdoc/>
  28. public override bool CanTimeout => Inner.CanTimeout;
  29. /// <inheritdoc/>
  30. public override int ReadTimeout
  31. {
  32. get => Inner.ReadTimeout;
  33. set => Inner.ReadTimeout = value;
  34. }
  35. /// <inheritdoc/>
  36. public override int WriteTimeout
  37. {
  38. get => Inner.WriteTimeout;
  39. set => Inner.WriteTimeout = value;
  40. }
  41. /// <inheritdoc/>
  42. public override long Position
  43. {
  44. get => Inner.Position;
  45. set => Inner.Position = value;
  46. }
  47. /// <inheritdoc/>
  48. public override void Flush()
  49. {
  50. Inner.Flush();
  51. }
  52. /// <inheritdoc/>
  53. public override Task FlushAsync(CancellationToken cancellationToken)
  54. {
  55. return Inner.FlushAsync(cancellationToken);
  56. }
  57. /// <inheritdoc/>
  58. public override int Read(byte[] buffer, int offset, int count)
  59. {
  60. return Inner.Read(buffer, offset, count);
  61. }
  62. /// <inheritdoc/>
  63. public override int Read(Span<byte> destination)
  64. {
  65. return Inner.Read(destination);
  66. }
  67. /// <inheritdoc/>
  68. public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
  69. {
  70. return Inner.ReadAsync(buffer, offset, count, cancellationToken);
  71. }
  72. /// <inheritdoc/>
  73. public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
  74. {
  75. return Inner.ReadAsync(destination, cancellationToken);
  76. }
  77. /// <inheritdoc/>
  78. public override long Seek(long offset, SeekOrigin origin)
  79. {
  80. return Inner.Seek(offset, origin);
  81. }
  82. /// <inheritdoc/>
  83. public override void SetLength(long value)
  84. {
  85. Inner.SetLength(value);
  86. }
  87. /// <inheritdoc/>
  88. public override void Write(byte[] buffer, int offset, int count)
  89. {
  90. Inner.Write(buffer, offset, count);
  91. }
  92. /// <inheritdoc/>
  93. public override void Write(ReadOnlySpan<byte> source)
  94. {
  95. Inner.Write(source);
  96. }
  97. /// <inheritdoc/>
  98. public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
  99. {
  100. return Inner.WriteAsync(buffer, offset, count, cancellationToken);
  101. }
  102. /// <inheritdoc/>
  103. public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
  104. {
  105. return Inner.WriteAsync(source, cancellationToken);
  106. }
  107. /// <inheritdoc/>
  108. public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
  109. {
  110. return TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state);
  111. }
  112. /// <inheritdoc/>
  113. public override int EndRead(IAsyncResult asyncResult)
  114. {
  115. return TaskToAsyncResult.End<int>(asyncResult);
  116. }
  117. /// <inheritdoc/>
  118. public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback,
  119. object? state)
  120. {
  121. return TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count), callback, state);
  122. }
  123. /// <inheritdoc/>
  124. public override void EndWrite(IAsyncResult asyncResult)
  125. {
  126. TaskToAsyncResult.End(asyncResult);
  127. }
  128. /// <inheritdoc/>
  129. public override int ReadByte()
  130. {
  131. return Inner.ReadByte();
  132. }
  133. /// <inheritdoc/>
  134. public override void WriteByte(byte value)
  135. {
  136. Inner.WriteByte(value);
  137. }
  138. /// <inheritdoc/>
  139. public sealed override void Close()
  140. {
  141. base.Close();
  142. }
  143. }

SafeWriteStream.cs

  1. public class SafeWriteStream(Stream inner) : DelegatingStream(inner)
  2. {
  3. private readonly SemaphoreSlim semaphoreSlim = new(1, 1);
  4. public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
  5. {
  6. try
  7. {
  8. await this.semaphoreSlim.WaitAsync(CancellationToken.None);
  9. await base.WriteAsync(source, cancellationToken);
  10. await this.FlushAsync(cancellationToken);
  11. }
  12. finally
  13. {
  14. this.semaphoreSlim.Release();
  15. }
  16. }
  17. public override ValueTask DisposeAsync()
  18. {
  19. this.semaphoreSlim.Dispose();
  20. return this.Inner.DisposeAsync();
  21. }
  22. protected override void Dispose(bool disposing)
  23. {
  24. this.semaphoreSlim.Dispose();
  25. this.Inner.Dispose();
  26. }
  27. }

创建服务端

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

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

  1. using System.Collections.Concurrent;
  2. using Microsoft.AspNetCore.Http.Features;
  3. using Microsoft.AspNetCore.Http.Timeouts;
  4. using Taibai.Core;
  5. namespace Taibai.Server;
  6. public static class ServerService
  7. {
  8. private static readonly ConcurrentDictionary<string, (CancellationToken, Stream)> ClusterConnections = new();
  9. public static async Task StartAsync(HttpContext context)
  10. {
  11. // 如果不是http2协议,我们不处理, 因为我们只支持http2
  12. if (context.Request.Protocol != HttpProtocol.Http2)
  13. {
  14. return;
  15. }
  16. // 获取query
  17. var query = context.Request.Query;
  18. // 我们需要强制要求name参数
  19. var name = query["name"];
  20. if (string.IsNullOrEmpty(name))
  21. {
  22. context.Response.StatusCode = 400;
  23. Console.WriteLine("Name is required");
  24. return;
  25. }
  26. Console.WriteLine("Accepted connection from " + name);
  27. // 获取http2特性
  28. var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();
  29. // 禁用超时
  30. context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();
  31. // 得到双工流
  32. var stream = new SafeWriteStream(await http2Feature.AcceptAsync());
  33. // 将其添加到集合中,以便我们可以在其他地方使用
  34. CreateConnectionChannel(name, context.RequestAborted, stream);
  35. // 注册取消连接
  36. context.RequestAborted.Register(() =>
  37. {
  38. // 当取消时,我们需要从集合中删除
  39. ClusterConnections.TryRemove(name, out _);
  40. });
  41. // 由于我们需要保持连接,所以我们需要等待,直到客户端主动断开连接。
  42. await Task.Delay(-1, context.RequestAborted);
  43. }
  44. /// <summary>
  45. /// 通过名称获取连接
  46. /// </summary>
  47. /// <param name="host"></param>
  48. /// <returns></returns>
  49. public static (CancellationToken, Stream) GetConnectionChannel(string host)
  50. {
  51. return ClusterConnections[host];
  52. }
  53. /// <summary>
  54. /// 注册连接
  55. /// </summary>
  56. /// <param name="host"></param>
  57. /// <param name="cancellationToken"></param>
  58. /// <param name="stream"></param>
  59. public static void CreateConnectionChannel(string host, CancellationToken cancellationToken, Stream stream)
  60. {
  61. ClusterConnections.GetOrAdd(host,
  62. _ => (cancellationToken, stream));
  63. }
  64. }

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

  1. using Microsoft.AspNetCore.Http.Features;
  2. using Microsoft.AspNetCore.Http.Timeouts;
  3. using Taibai.Core;
  4. namespace Taibai.Server;
  5. public class ClientMiddleware : IMiddleware
  6. {
  7. public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  8. {
  9. // 如果不是http2协议,我们不处理, 因为我们只支持http2
  10. if (context.Request.Protocol != HttpProtocol.Http2)
  11. {
  12. return;
  13. }
  14. var name = context.Request.Query["name"];
  15. if (string.IsNullOrEmpty(name))
  16. {
  17. context.Response.StatusCode = 400;
  18. Console.WriteLine("Name is required");
  19. return;
  20. }
  21. Console.WriteLine("Accepted connection from " + name);
  22. var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();
  23. context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();
  24. // 得到双工流
  25. var stream = new SafeWriteStream(await http2Feature.AcceptAsync());
  26. // 通过name找到指定的server链接,然后进行转发。
  27. var (cancellationToken, reader) = ServerService.GetConnectionChannel(name);
  28. try
  29. {
  30. // 注册取消连接
  31. cancellationToken.Register(() =>
  32. {
  33. Console.WriteLine("断开连接");
  34. stream.Close();
  35. });
  36. // 得到客户端的流,然后给我们的SafeWriteStream,然后我们就可以进行转发了
  37. var socketStream = new SafeWriteStream(reader);
  38. // 在这里他们会将读取的数据写入到对方的流中,这样就实现了双工通信,这个非常简单并且性能也不错。
  39. await Task.WhenAll(
  40. stream.CopyToAsync(socketStream, context.RequestAborted),
  41. socketStream.CopyToAsync(stream, context.RequestAborted)
  42. );
  43. }
  44. catch (Exception e)
  45. {
  46. Console.WriteLine("断开连接" + e.Message);
  47. throw;
  48. }
  49. }
  50. }

打开Program.cs

  1. using Taibai.Server;
  2. var builder = WebApplication.CreateBuilder(new WebApplicationOptions());
  3. builder.Host.ConfigureHostOptions(host => { host.ShutdownTimeout = TimeSpan.FromSeconds(1d); });
  4. builder.Services.AddSingleton<ClientMiddleware>();
  5. var app = builder.Build();
  6. app.Map("/server", app =>
  7. {
  8. app.Use(Middleware);
  9. static async Task Middleware(HttpContext context, RequestDelegate _)
  10. {
  11. await ServerService.StartAsync(context);
  12. }
  13. });
  14. app.Map("/client", app => { app.UseMiddleware<ClientMiddleware>(); });
  15. app.Run();

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

/client则给了ClientMiddleware中间件。

创建客户端

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

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

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

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

  1. public class ClientOption
  2. {
  3. /// <summary>
  4. /// 服务地址
  5. /// </summary>
  6. public string ServiceUri { get; set; }
  7. }

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

  1. using System.Net;
  2. using System.Net.Security;
  3. using System.Net.Sockets;
  4. using Taibai.Core;
  5. namespace Taibai.Client;
  6. public class MonitorClient(ClientOption option)
  7. {
  8. private string Protocol = "taibai";
  9. private readonly HttpMessageInvoker httpClient = new(CreateDefaultHttpHandler(), true);
  10. private readonly Socket socket = new(SocketType.Stream, ProtocolType.Tcp);
  11. private static SocketsHttpHandler CreateDefaultHttpHandler()
  12. {
  13. return new SocketsHttpHandler
  14. {
  15. // 允许多个http2连接
  16. EnableMultipleHttp2Connections = true,
  17. // 设置连接超时时间
  18. ConnectTimeout = TimeSpan.FromSeconds(60),
  19. SslOptions = new SslClientAuthenticationOptions
  20. {
  21. // 由于我们没有证书,所以我们需要设置为true
  22. RemoteCertificateValidationCallback = (_, _, _, _) => true,
  23. },
  24. };
  25. }
  26. public async Task TransportAsync(CancellationToken cancellationToken)
  27. {
  28. Console.WriteLine("链接中!");
  29. // 由于是测试,我们就目前先写死远程地址
  30. await socket.ConnectAsync(new IPEndPoint(IPAddress.Parse("192.168.31.250"), 3389), cancellationToken);
  31. Console.WriteLine("连接成功");
  32. // 将Socket转换为流
  33. var stream = new NetworkStream(socket);
  34. try
  35. {
  36. // 创建服务器的连接,然后返回一个流,这个是H2的流
  37. var serverStream = await this.CreateServerConnectionAsync(cancellationToken);
  38. Console.WriteLine("链接服务器成功");
  39. // 将两个流连接起来,这样我们就可以进行双工通信了。它们会自动进行数据的传输。
  40. await Task.WhenAll(
  41. stream.CopyToAsync(serverStream, cancellationToken),
  42. serverStream.CopyToAsync(stream, cancellationToken)
  43. );
  44. }
  45. catch (Exception ex)
  46. {
  47. Console.WriteLine(ex.Message);
  48. throw;
  49. }
  50. }
  51. /// <summary>
  52. /// 创建服务器的连接
  53. /// </summary>
  54. /// <param name="cancellationToken"></param>
  55. /// <exception cref="OperationCanceledException"></exception>
  56. /// <returns></returns>
  57. public async Task<SafeWriteStream> CreateServerConnectionAsync(CancellationToken cancellationToken)
  58. {
  59. var stream = await Http20ConnectServerAsync(cancellationToken);
  60. return new SafeWriteStream(stream);
  61. }
  62. /// <summary>
  63. /// 创建http2连接
  64. /// </summary>
  65. /// <param name="cancellationToken"></param>
  66. /// <returns></returns>
  67. private async Task<Stream> Http20ConnectServerAsync(CancellationToken cancellationToken)
  68. {
  69. var serverUri = new Uri(option.ServiceUri);
  70. // 这里我们使用Connect方法,因为我们需要建立一个双工流, 这样我们就可以进行双工通信了。
  71. var request = new HttpRequestMessage(HttpMethod.Connect, serverUri);
  72. // 如果设置了Connect,那么我们需要设置Protocol
  73. request.Headers.Protocol = Protocol;
  74. // 我们需要设置http2的版本
  75. request.Version = HttpVersion.Version20;
  76. // 我们需要确保我们的请求是http2的
  77. request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
  78. // 设置一下超时时间,这样我们就可以在超时的时候取消连接了。
  79. using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
  80. using var linkedTokenSource =
  81. CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
  82. // 发送请求,然后等待响应
  83. var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token);
  84. // 返回h2的流,用于传输数据
  85. return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token);
  86. }
  87. }

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

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

  1. using System.Net;
  2. using System.Net.Security;
  3. using System.Net.Sockets;
  4. using Taibai.Core;
  5. using HttpMethod = System.Net.Http.HttpMethod;
  6. namespace Taibai.Client;
  7. public class Client
  8. {
  9. private readonly ClientOption option;
  10. private string Protocol = "taibai";
  11. private readonly HttpMessageInvoker httpClient;
  12. private readonly Socket socket;
  13. public Client(ClientOption option)
  14. {
  15. this.option = option;
  16. this.httpClient = new HttpMessageInvoker(CreateDefaultHttpHandler(), true);
  17. this.socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
  18. // 监听本地端口
  19. this.socket.Bind(new IPEndPoint(IPAddress.Loopback, 60112));
  20. this.socket.Listen(10);
  21. }
  22. private static SocketsHttpHandler CreateDefaultHttpHandler()
  23. {
  24. return new SocketsHttpHandler
  25. {
  26. // 允许多个http2连接
  27. EnableMultipleHttp2Connections = true,
  28. ConnectTimeout = TimeSpan.FromSeconds(60),
  29. ResponseDrainTimeout = TimeSpan.FromSeconds(60),
  30. SslOptions = new SslClientAuthenticationOptions
  31. {
  32. // 由于我们没有证书,所以我们需要设置为true
  33. RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
  34. },
  35. };
  36. }
  37. public async Task TransportAsync(CancellationToken cancellationToken)
  38. {
  39. Console.WriteLine("Listening on 60112");
  40. // 等待客户端连接
  41. var client = await this.socket.AcceptAsync(cancellationToken);
  42. Console.WriteLine("Accepted connection from " + client.RemoteEndPoint);
  43. try
  44. {
  45. // 将Socket转换为流
  46. var stream = new NetworkStream(client);
  47. // 创建服务器的连接,然后返回一个流, 这个是H2的流
  48. var serverStream = await this.CreateServerConnectionAsync(cancellationToken);
  49. Console.WriteLine("Connected to server");
  50. // 将两个流连接起来, 这样我们就可以进行双工通信了. 它们会自动进行数据的传输.
  51. await Task.WhenAll(
  52. stream.CopyToAsync(serverStream, cancellationToken),
  53. serverStream.CopyToAsync(stream, cancellationToken)
  54. );
  55. }
  56. catch (Exception e)
  57. {
  58. Console.WriteLine(e);
  59. throw;
  60. }
  61. }
  62. /// <summary>
  63. /// 创建与服务器的连接
  64. /// </summary>
  65. /// <param name="cancellationToken"></param>
  66. /// <exception cref="OperationCanceledException"></exception>
  67. /// <returns></returns>
  68. public async Task<SafeWriteStream> CreateServerConnectionAsync(CancellationToken cancellationToken)
  69. {
  70. var stream = await this.Http20ConnectServerAsync(cancellationToken);
  71. return new SafeWriteStream(stream);
  72. }
  73. private async Task<Stream> Http20ConnectServerAsync(CancellationToken cancellationToken)
  74. {
  75. var serverUri = new Uri(option.ServiceUri);
  76. // 这里我们使用Connect方法, 因为我们需要建立一个双工流
  77. var request = new HttpRequestMessage(HttpMethod.Connect, serverUri);
  78. // 由于我们设置了Connect方法, 所以我们需要设置协议,这样服务器才能识别
  79. request.Headers.Protocol = Protocol;
  80. // 设置http2版本
  81. request.Version = HttpVersion.Version20;
  82. // 强制使用http2
  83. request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
  84. using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60));
  85. using var linkedTokenSource =
  86. CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken);
  87. // 发送请求,等待服务器验证。
  88. var httpResponse = await this.httpClient.SendAsync(request, linkedTokenSource.Token);
  89. // 返回一个流
  90. return await httpResponse.Content.ReadAsStreamAsync(linkedTokenSource.Token);
  91. }
  92. }

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

  1. using Taibai.Client;
  2. const string commandTemplate = @"
  3. 当前是 Taibai 客户端,输入以下命令:
  4. - `help` 显示帮助
  5. - `monitor` 使用监控模式,监听本地端口,将流量转发到服务端的指定地址
  6. - `monitor=https://localhost:7153/server?name=test` 监听本地端口,将流量转发到服务端指定的客户端名称为 test 的地址
  7. - `client` 使用客户端模式,连接服务端的指定地址,将流量转发到本地端口
  8. - `client=https://localhost:7153/client?name=test` 连接服务端指定当前客户端名称为 test,将流量转发到本地端口
  9. - `exit` 退出
  10. 输入命令:
  11. ";
  12. while (true)
  13. {
  14. Console.WriteLine(commandTemplate);
  15. var command = Console.ReadLine();
  16. if (command?.StartsWith("monitor=") == true)
  17. {
  18. var client = new MonitorClient(new ClientOption()
  19. {
  20. ServiceUri = command[8..]
  21. });
  22. await client.TransportAsync(new CancellationToken());
  23. }
  24. else if (command?.StartsWith("client=") == true)
  25. {
  26. var client = new Client(new ClientOption()
  27. {
  28. ServiceUri = command[7..]
  29. });
  30. await client.TransportAsync(new CancellationToken());
  31. }
  32. else if (command == "help")
  33. {
  34. Console.WriteLine(commandTemplate);
  35. }
  36. else if (command == "exit")
  37. {
  38. Console.WriteLine("Bye!");
  39. break;
  40. }
  41. else
  42. {
  43. Console.WriteLine("未知命令");
  44. }
  45. }

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

然后我们发布一下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

原文链接:https://www.cnblogs.com/hejiale010426/p/18166935

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号