网上的开源socket服务一般都是go实现的,几乎没有见到用dotnet写的,为了压榨GPT的免费额度,无聊时,就给它提需求,让它用dotnetcore写一个TCP的,很简单能跑在linux终端上就行。后面有需求再改为socket协议。
服务端 ProxyServerApp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace ProxyServerApp
{
public class Program
{
public static async Task Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
var logger = loggerFactory.CreateLogger<ProxySocketServer>();
int port = 8080; // 默认监听端口
if (args.Length > 0 && int.TryParse(args[0], out int customPort))
{
port = customPort;
}
var proxyServer = new ProxySocketServer(port, logger);
await proxyServer.StartListeningAsync();
}
}
public class ProxySocketServer
{
private readonly int _listenPort;
private readonly ILogger _logger;
public ProxySocketServer(int listenPort, ILogger<ProxySocketServer> logger)
{
_listenPort = listenPort;
_logger = logger;
}
public async Task StartListeningAsync()
{
var listener = new TcpListener(IPAddress.Any, _listenPort);
listener.Start();
_logger.LogInformation($"Proxy server started, listening on port {_listenPort}...");
while (true)
{
var client = await listener.AcceptTcpClientAsync();
_logger.LogInformation("Client connected.");
_ = HandleClientAsync(client);
}
}
private async Task HandleClientAsync(TcpClient client)
{
try
{
using (var clientStream = client.GetStream())
{
var buffer = new byte[4096];
string request = string.Empty;
bool isConnectRequest = false;
// 循环读取,直到收到 "CONNECT" 请求
for (int readAttempt = 0; readAttempt < 3; readAttempt++)
{
int bytesRead = await clientStream.ReadAsync(buffer, 0, buffer.Length);
request = Encoding.UTF8.GetString(buffer, 0, bytesRead);
_logger.LogInformation($"Received request attempt {readAttempt + 1}: {request}");
if (request.StartsWith("CONNECT"))
{
isConnectRequest = true;
break;
}
else
{
_logger.LogWarning("Invalid request. Retrying to read the 'CONNECT' request...");
await Task.Delay(500); // 增加延迟避免快速重试
}
}
if (!isConnectRequest)
{
var errorResponse = "HTTP/1.1 400 Bad Request\r\n\r\n";
await clientStream.WriteAsync(Encoding.UTF8.GetBytes(errorResponse));
_logger.LogError("Failed to receive a valid CONNECT request.");
return;
}
// 解析 CONNECT 请求
var connectLine = request.Split(' ')[1];
var hostAndPort = connectLine.Split(':');
var destinationHost = hostAndPort[0];
var destinationPort = int.Parse(hostAndPort[1]);
bool connectionEstablished = false;
// 尝试连接到目标服务器,最多3次
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
using (var destinationClient = new TcpClient())
{
await destinationClient.ConnectAsync(destinationHost, destinationPort);
_logger.LogInformation($"Connected to destination: {destinationHost}:{destinationPort}");
// 发送成功响应给客户端
var response = "HTTP/1.1 200 Connection Established\r\n\r\n";
await clientStream.WriteAsync(Encoding.UTF8.GetBytes(response));
_logger.LogInformation("Sent 200 Connection Established to client.");
connectionEstablished = true;
// 双向传输:代理客户端和目标服务器的数据
await Task.WhenAny(
TransferData(clientStream, destinationClient.GetStream()),
TransferData(destinationClient.GetStream(), clientStream)
);
}
}
catch (Exception ex)
{
_logger.LogWarning($"Attempt {attempt + 1} to connect to {destinationHost}:{destinationPort} failed: {ex.Message}");
if (attempt == 2) // 最后一次尝试
{
var errorResponse = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
await clientStream.WriteAsync(Encoding.UTF8.GetBytes(errorResponse));
_logger.LogError($"Failed to establish connection to {destinationHost}:{destinationPort}");
}
}
if (connectionEstablished) break; // 成功则跳出重试
}
}
}
catch (Exception ex)
{
_logger.LogError($"Error: {ex.Message}");
}
finally
{
client.Close();
_logger.LogInformation("Client disconnected.");
}
}
private async Task TransferData(NetworkStream source, NetworkStream destination)
{
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await destination.WriteAsync(buffer, 0, bytesRead);
await destination.FlushAsync();
}
}
}
}
客户端测试用例
using System;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.NetworkInformation;
namespace ProxyClientApp
{
class Program
{
private static readonly string[] proxyServers = { "yourserverip1:port", "yourserverip2:port" }; // 设置多个
private const string targetUrl = "https://v2ex.com/index.xml"; // 目标 URL
private const int maxRetries = 3; // 最大重试次数
static async Task Main(string[] args)
{
try
{
//for (int i = 0; i < 100; i++)
{
await RetryOnExceptionAsync(async () => await ConnectThroughProxyAsync(), maxRetries, 3000);
}
}
catch (Exception ex)
{
Console.WriteLine($"Operation failed after {maxRetries} retries: {ex.Message}");
}
}
private static async Task ConnectThroughProxyAsync()
{
var uri = new Uri(targetUrl);
string targetHost = uri.Host;
int targetPort = uri.Scheme == "https" ? 443 : 80;
foreach (var proxy in proxyServers)
{
var parts = proxy.Split(':');
var proxyHost = parts[0];
var proxyPort = int.Parse(parts[1]);
if (!NetworkInterface.GetIsNetworkAvailable())
{
Console.WriteLine("Network unavailable. Retrying in 5 seconds...");
await Task.Delay(5000);
continue;
}
using (var client = new TcpClient())
{
try
{
Console.WriteLine($"Attempting to connect to proxy {proxyHost}:{proxyPort}");
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
client.Client.ReceiveTimeout = 5000;
client.Client.SendTimeout = 5000;
// 连接到代理服务器
await client.ConnectAsync(proxyHost, proxyPort);
Console.WriteLine($"Connected to proxy: {proxyHost}:{proxyPort}");
using (var networkStream = client.GetStream()) clientStream.ReadAsync(buffer, 0, buffer.Length);
using (var writer = new StreamWriter(networkStream, Encoding.UTF8) { AutoFlush = true })
using (var reader = new StreamReader(networkStream, Encoding.UTF8))
{
// 发送 CONNECT 请求
string connectRequest = $"CONNECT {targetHost}:{targetPort} HTTP/1.1\r\nHost: {targetHost}\r\n\r\n";
await writer.WriteAsync(connectRequest);
Console.WriteLine("Sent CONNECT request to proxy.");
var connectResponse = await reader.ReadLineAsync();
Console.WriteLine("Proxy server response: " + connectResponse);
if (!connectResponse.Contains("200"))
{
Console.WriteLine("Failed to establish connection through proxy.");
continue;
}
// 创建 SSL/TLS 流
var sslStream = new SslStream(networkStream, false);
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) // 设置超时
{
await sslStream.AuthenticateAsClientAsync(targetHost, null, System.Security.Authentication.SslProtocols.Tls12, false);
Console.WriteLine("SSL/TLS handshake successful.");
// 发送 GET 请求到目标服务器
string getRequest = $"GET {uri.PathAndQuery} HTTP/1.1\r\nHost: {targetHost}\r\nConnection: close\r\n\r\n";
await sslStream.WriteAsync(Encoding.UTF8.GetBytes(getRequest));
Console.WriteLine("Sent GET request to target server.");
StringBuilder httpContent = new StringBuilder();
// 读取响应数据
using (var responseReader = new StreamReader(sslStream, Encoding.UTF8))
{
string responseLine;
Console.WriteLine("Response from target server:");
while ((responseLine = await responseReader.ReadLineAsync()) != null)
{
Console.WriteLine(responseLine);
httpContent.Append(responseLine);
}
}
System.Diagnostics.Debug.WriteLine(httpContent.ToString());
}
}
}
catch (Exception ex)
{
Console.WriteLine($" proxy {proxyHost}:{proxyPort} , Error: {ex.Message}");
}
}
}
}
private static async Task RetryOnExceptionAsync(Func<Task> operation, int maxRetries = 3, int delayMilliseconds = 1000)
{
for (int retry = 0; retry < maxRetries; retry++)
{
try
{
await operation();
return; // 操作成功,直接返回
}
catch (Exception ex) when (retry < maxRetries - 1)
{
Console.WriteLine($"Retry {retry + 1}/{maxRetries} failed: {ex.Message}");
await Task.Delay(delayMilliseconds * (int)Math.Pow(2, retry)); // 指数退避
}
}
throw new Exception("Operation failed after retries.");
}
}
通过local网络客户端,访问https://v2ex.com/index.xml。测试结果:
服务端
客户端
分析
没有网络知识,又是应用级API,只能看个大概,大致就是S与C先连上,然后S 等待reader some data,C 开始write some data 并等待. S拿到C的some data,处理结果write result data, 这时C的wite方法返回了S 的 result data,关闭连接,结束。所C要与S保待节奏一致,GPT给的都是理想情况一次完成,出错几次后,再把调试信息给它,它会加上retries的次数。这也证明了GPT 的确根据理解生成的代码,不是搜索的代码。