我正在尝试用c#编写一个简单的异步https代理服务器。
我想知道当请求完成时我应该如何检测/处理,以及如何退出我的bActive循环,并假设这样的循环是合适的。
非常感谢您提供一些有关我的方法是否正确以及如何改进逻辑的建议。
我似乎遇到的问题是,端点响应所需的时间以及网络延迟意味着我DataAvailable
确实总是有数据,但可能仍然有一些发送。需要睡眠和另一个尝试,这反过来又导致请求中的完成时间很长。
监听TCP连接
提取CONNECT标头并打开与请求的服务器的连接
将requestStream复制到proxyStream
将proxyStream复制到requestStream
睡眠以等待数据,并重复3-4,直到两个流都没有数据可用为止。然后打破循环并关闭连接。
public async Task Start()
{
listener.Start();
while (listen)
{
if (listener.Pending())
{
HandleClient(await listener.AcceptTcpClientAsync());
}
else
{
await Task.Delay(100); //<--- timeout
}
}
}
private static async Task HandleClient(TcpClient clt)
{
var bytes = new byte[clt.ReceiveBufferSize];
var hostHeaderAvailable = 0;
NetworkStream requestStream = null;
int count;
const string connectText = "connect";
const string hostText = "Host: ";
bool bActive = true;
List<Task> tasks = new List<Task>();
try
{
using (NetworkStream proxyStream = clt.GetStream())
using (TcpClient requestClient = new TcpClient())
{
proxyStream.ReadTimeout = 100;
proxyStream.WriteTimeout = 100;
while (bActive)
{
if (proxyStream.DataAvailable && hostHeaderAvailable == 0)
{
count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);
var text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
if (text.ToLower().StartsWith(connectText))
{
// extract the url and port
var host = text.Remove(0, connectText.Length + 1);
var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
// connect to the url and prot supplied
await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
requestStream = requestClient.GetStream();
requestStream.ReadTimeout = 100;
requestStream.WriteTimeout = 100;
// send 200 response to proxyStream
const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);
// delay here seems to prevent the following proxyStream.read from failing as data is not yet avaiable
// without it the loop runs and has to timeout before running again
await Task.Delay(1);
}
}
hostHeaderAvailable++;
if (requestStream == null || !requestClient.Connected || !clt.Connected)
{
bActive = false;
break;
}
Console.WriteLine(proxyStream.DataAvailable || requestStream.DataAvailable);
if (proxyStream.DataAvailable || requestStream.DataAvailable)
{
Task task = proxyStream.CopyToAsync(requestStream);
Task task2 = requestStream.CopyToAsync(proxyStream);
tasks.Add(task);
tasks.Add(task2);
await Task.WhenAll(tasks).ConfigureAwait(false);
bActive = false;
break;
}
await Task.Delay(10);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
clt.Close();
}
使用ReadAsync
/ WriteAsync
进行响应的时间过长,但仍然存在超时问题。
监听TCP连接
提取CONNECT标头并打开与请求的服务器的连接
从requestStream读取数据并复制到proxyStream
等待检查数据是否在两个流上都可用
如果有可用数据从proxyStream读取并写入requestStream
如果有可用数据从requestStream读取并写入proxyStream
睡眠以等待数据,然后重复5-6,直到两个数据流都没有可用数据为止。然后打破循环并关闭连接。
private static TcpListener listener = new TcpListener(IPAddress.Parse("192.168.0.25"), 13000);
private static bool listen = true;
public async Task Start()
{
listener.Start();
while (listen)
{
if (listener.Pending())
{
await HandleClient(await listener.AcceptTcpClientAsync());
}
else
{
await Task.Delay(100);
}
}
}
private static async Task HandleClient(TcpClient clt)
{
var bytes = new byte[clt.ReceiveBufferSize];
var hostHeaderAvailable = 0;
NetworkStream requestStream = null;
int count;
const string connectText = "connect";
const string hostText = "Host: ";
bool bActive = true;
try
{
using (NetworkStream proxyStream = clt.GetStream())
using (TcpClient requestClient = new TcpClient())
{
while (bActive)
{
while (proxyStream.DataAvailable)
{
// handle connect
if (hostHeaderAvailable == 0)
{
count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);
var text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
if (text.ToLower().StartsWith(connectText))
{
// extract the url and port
var host = text.Remove(0, connectText.Length + 1);
var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
// connect to the url and prot supplied
await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
requestStream = requestClient.GetStream();
// send 200 response to proxyStream
const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);
// delay here seems to prevent the following proxyStream.read from failing as data is not yet avaiable
// without it the loop runs and has to timeout before running again
await Task.Delay(20);
}
}
hostHeaderAvailable++;
if (requestClient.Connected && hostHeaderAvailable > 1)
{
count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);
await requestStream.WriteAsync(bytes, 0, count);
}
}
while (requestStream.DataAvailable)
{
count = await requestStream.ReadAsync(bytes, 0, bytes.Length);
await proxyStream.WriteAsync(bytes, 0, count);
}
// attempt to detect a timeout / end of data avaiable
var timeout = 0;
while (!proxyStream.DataAvailable && !requestStream.DataAvailable)
{
if (timeout > 5)
{
bActive = false;
break;
}
await Task.Delay(10);
timeout++;
}
}
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
更新
根据AgentFire的回答,我现在进入以下工作代码:
public static async Task PollUntilDisconnect(TcpClient tcp, TcpClient tcp2)
{
while (true)
{
if (tcp.Client.Poll(0, SelectMode.SelectRead))
{
byte[] buff = new byte[1];
if (tcp.Client.Receive(buff, SocketFlags.Peek) == 0)
{
// Client disconnected
Console.WriteLine("The requesting client has dropped its connection.");
break;
}
}
if (tcp2.Client.Poll(0, SelectMode.SelectRead))
{
byte[] buff = new byte[1];
if (tcp2.Client.Receive(buff, SocketFlags.Peek) == 0)
{
// Server disconnected
Console.WriteLine("The destination has dropped a the connection.");
break;
}
}
await Task.Delay(1);
}
}
private static async Task HandleClient(TcpClient clt)
{
var bytes = new byte[clt.ReceiveBufferSize];
var hostHeaderAvailable = 0;
NetworkStream requestStream = null;
int count;
const string connectText = "connect";
const string hostText = "Host: ";
bool bActive = true;
List<Task> tasks = new List<Task>();
try
{
using (NetworkStream proxyStream = clt.GetStream())
using (TcpClient requestClient = new TcpClient())
{
proxyStream.ReadTimeout = 100;
proxyStream.WriteTimeout = 100;
if (proxyStream.DataAvailable && hostHeaderAvailable == 0)
{
count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);
var text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
if (text.ToLower().StartsWith(connectText))
{
// extract the url and port
var host = text.Remove(0, connectText.Length + 1);
var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
// connect to the url and prot supplied
await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
requestStream = requestClient.GetStream();
requestStream.ReadTimeout = 100;
requestStream.WriteTimeout = 100;
// send 200 response to proxyStream
const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);
// delay here seems to prevent the following proxyStream.read from failing as data is not yet avaiable
// without it the loop runs and has to timeout before running again
await Task.Delay(1);
}
}
hostHeaderAvailable++;
Task task = proxyStream.CopyToAsync(requestStream);
Task task2 = requestStream.CopyToAsync(proxyStream);
Task handleConnection = PollUntilDisconnect(clt, requestClient);
tasks.Add(task);
tasks.Add(task2);
tasks.Add(handleConnection);
await Task.WhenAll(tasks).ConfigureAwait(false);
// close the connections
requestClient.Close();
clt.Close();
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
好吧,告诉你。就HTTP而言,数据可用性仅取决于一个参数(如果我们省略WebSocket之类的东西),该参数称为Connection,并作为标头传递为以下两种可能状态之一:Close
或[C0 ]。
如果客户端选择了Keep-Alive
,则服务器有义务在服务请求后立即关闭连接,而Close
则告诉服务器,如果不想,则可能会保持连接打开另一个请求。
如果客户端选择保持活动状态,则连接将持久存在并按预期方式正常工作。但是:
如果任何一方断开连接,都有一种简单的方法可以检测到该连接。这段代码是在StackOverflow上找到的,并被告知仍然可以正常工作:
Keep-Alive
因此,我相信您作为代理服务器,根本没有义务管理连接状态,而是可以将其留给实际的通信方。您所需要做的就是检测何时断开了两个连接(代理或请求),然后断开另一个连接并进行呼叫。
我必须补充说,TCP连接被认为是public static bool CheckIfDisconnected(this TcpClient tcp)
{
if (tcp.Client.Poll(0, SelectMode.SelectRead))
{
byte[] buff = new byte[1];
if (tcp.Client.Receive(buff, SocketFlags.Peek) == 0)
{
// Client disconnected
return true;
}
}
return false;
}
。这意味着您可以自由创建两个异步运行任务,分别对它们自己的接收器进行读写。我的想法是,这是最佳的行动方案。