双向 HTTP 连接





5.00/5 (15投票s)
一篇关于使用单个开放连接进行双向通信的文章。
摘要
实现点对点应用程序需要双方都设置为客户端和服务器。最简单的方法是让各方连接到各自的端点。但是,如果各方被防火墙隔开,而网络管理员通常不愿意允许出站连接,则此方法通常不起作用。因此,维护和重用已建立的连接进行双向流量是唯一的选择。
基本概念
为了实现这一点,我们必须区分传入的请求和传入的响应,解释如下。在一种情况下,我们期望另一方向我们发送一个请求,我们只需返回一个响应;在另一种情况下,是我们发送一个请求并等待另一方发送响应。我们将借助我们可以传递的带外信息来区分这一点,采用 HTTP 协议。
因此,我们将使用 HTTP 消息进行点对点通信。一旦监听套接字接受连接,我们将接收并完全解析一个 HTTP 消息,该消息可以轻松地标识和处理为请求或响应。以下是一些代码来说明基本思想
// create and start the listener
TcpListener listener = new TcpListener(7070);
listener.Start();
// wait and accept a client connection
Socket socket = listener.AcceptSocket();
// create an HTTP message object and receive the HTTP message
HttpMessage msg = new HttpMessage();
msg.Receive(_socket);
// process the HTTP message as either a request or a response
if(msg.IsResponse)
ProcessResponse(msg);
else
ProcessRequest(msg);
HttpMessage
对象仅从套接字流中读取 HTTP 协议的关键部分:第一行、可选标头列表和消息正文。通过检查第一行的第一个令牌来区分请求和响应。对于响应,第一行必须以“HTTP”开头。这就是 msg.IsResponse
所确定的。
我们最感兴趣的是 ProcessResponse(msg)
方法。我们如何关联我们之前发出的请求的响应?让我们检查发送请求的方法。
void SendRequest(Socket socket, HttpMessage msg)
{
// create a correlation id and assign it to the message
msg.CorrelationID = Guid.NewGuid().ToString();
// send the message
msg.Send(socket);
// retain the message for future reference in a hash table
_requests[msg.CorrelationID] = msg;
}
核心思想是创建并附加一个唯一的标识符到出站 HTTP 消息。我们期望这个标识符也出现在返回的 HTTP 响应消息中。以下是基本 HTTP 协议交换的说明。
// outgoing request
GET / HTTP/1.1
Correlation-ID: 0B83745D-2AAB-4bce-8AC9-B8A590F07768
// incoming response
HTTP/1.1 200 Ok
Correlation-ID: 0B83745D-2AAB-4bce-8AC9-B8A590F07768
现在我们可以将注意力转向 ProcessResponse(msg)
方法。这是模型
void ProcessResponse(HttpMessage response)
{
// use the correlation id to match the response to the request
HttpMessage request = _requests[response.CorrelationID];
// use the request and the response for further processing
}
双向通信管理器
双向通信的核心思想已经描述完毕。让我们继续开发一个可以实际部署的模块。我们想要一个可以管理双向通信细节的类,我们可以像这样部署它
// create and start the listener
TcpListener listener = new TcpListener(7070);
listener.Start();
while(true)
{
// wait for and accept a client connection
Socket socket = listener.AcceptSocket();
// create the connection object that will
// manage the bi-directional communication
Connection conn = new Connection(socket);
// spawn a worker thread for this connection
// and loop back to accept another client
new Thread( new ThreadStart(conn.ThreadProc) ).Start();
}
Connection
类负责管理双向通信。我们将接受的套接字传递给它,并依赖连接对象使用同一套接字发送和接收 HTTP 消息。为了等待和接收其他连接,我们创建一个工作线程来管理已建立的连接。这是连接的线程过程
// the connection object's thread procedure
void ThreadProc()
{
while(Continue())
{
// create and receive the HTTP message
HttpMessage msg = new HttpMessage();
msg.Receive(_socket);
// process the message as either a response or request
if(msg.IsResponse)
ProcessResponse(msg);
else
ProcessRequest(msg);
}
}
线程过程中的代码应该很熟悉。让我们重新审视发送请求的方法。我们想发送一个请求并同步等待响应,如下所示
HttpMessage request = new HttpMessage();
request.Verb = "GET";
request.RequestUri = "/";
request.Version = "HTTP/1.1";
HttpMessage response = conn.SendMessage(request);
这意味着 SendMessage(request)
方法必须等到响应被接收。我们需要一种方法来信号化响应的到来。解决这个问题的最佳方法是实现互补的异步方法 BeginSendMessage
和 EndSendMessage
。
public IAsyncResult BeginSendMessage(HttpMessage request)
{
// create a correlation id and assign it to the message
request.CorrelationID = Guid.NewGuid().ToString();
// send the request on the assigned socket
request.Send(_socket);
// create the waitable object
IAsyncResult async = new HttpAsyncResult();
// map the correlation id to the waitable object
_requests[request.CorrelationID] = async;
return async;
}
public HttpMessage EndSendMessage(IAsyncResult async)
{
// wait until the signal is set, e.g the response has been received
if(!async.IsCompleted)
async.AsyncWaitHandle.WaitOne();
// get the matching response
HttpMessage response = (HttpMessage)_requests[async];
// clear the requests table
_requests.Remove(async);
// return the response
return response;
}
在讨论此代码之前,让我们展示同步版本的实现。它非常简单。
public HttpResponse SendRequest(HttpRequest request)
{
IAsyncResult async = BeginRequest(request);
return EndRequest(async);
}
让我们讨论异步版本。我们将出站消息的相关 ID 映射到一个实现 IAsyncResult
接口的可等待对象。显然,当出站消息的响应到达时,需要设置可等待对象。这必须在 ProcessResponse
方法中发生。这是它的实现
void ProcessResponse(HttpMessage response)
{
// use the correlation id to get the waitable object
HttpAsyncResult async = (HttpAsyncResult)_requests[response.CorrelationID];
// remove the correlation id from the requests table
_requests.Remove(response.CorrelationID);
// map the waitable object to the response
_requests[async] = response;
// set the signal so that the response becomes availabe
async.Set();
}
您需要仔细比较 EndSendMessage
和 ProcessResponse
方法。可以理解的是,一旦我们发送了请求,就必须等到响应到达。
现在,让我们转向处理传入请求的情况,如 ProcessRequest
所示。我们的 Connection
在这里主要负责管理双向通信。因此,将 HTTP 请求的处理委托给某个外部代理才有意义。我们可以通过定义一个适当的委托来实现这一点:public delegate HttpMessage ProcessRequestDelegate(HttpMessage request);
。因此,这是 PrecessRequest
方法的简单实现。
// delegate member
public ProcessRequestDelegate DelegateRequest;
// private method
void ProcessRequest(HttpMessage request)
{
// let an external agent process the request
HttpMessage response = DelegateRequest(request);
// Take the response and set the correlation id
response.CorrelationID = request.CorrelationID;
// now send the response on the assigned socket
response.Send(_socket);
}
我们只是想确保请求处理是并行完成的。当我们寻求准备好处理任何传入的 HTTP 消息时,我们不能等待。因此,让我们改进 ProcessRequest
方法。
// temporary queue for storing the request
Queue _queue = Queue.Synchronized( new Queue() );
void ProcessRequest(HttpMessage request)
{
// store request temporarily in a queue
_queue.Enqueue(request);
new Thread( new ThreadStart(this.ProcessRequestThreadProc) ).Start();
}
// delegate member
public ProcessRequestDelegate DelegateRequest;
// private method
void ProcessRequestThreadProc()
{
// get request from temporary queue
HttpMessage request = (HttpMessage)_queue.Dequeue();
// let an external agent process the request
HttpMessage response = DelegateRequest(request);
// Take the response and set the correlation id
response.CorrelationID = request.CorrelationID;
// now send the response on the assigned socket
response.Send(_socket);
}
使用源代码
您可以下载源代码并进行尝试。zip 文件包含一个 Visual Studio 2003 解决方案。连接管理器位于一个单独的程序集“Connection”中。此外,还有一个“client”和一个“server”项目供您尝试。