65.9K
CodeProject 正在变化。 阅读更多。
Home

双向 HTTP 连接

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2004年1月23日

CPOL

4分钟阅读

viewsIcon

111752

downloadIcon

867

一篇关于使用单个开放连接进行双向通信的文章。

摘要

实现点对点应用程序需要双方都设置为客户端和服务器。最简单的方法是让各方连接到各自的端点。但是,如果各方被防火墙隔开,而网络管理员通常不愿意允许出站连接,则此方法通常不起作用。因此,维护和重用已建立的连接进行双向流量是唯一的选择。

基本概念

为了实现这一点,我们必须区分传入的请求和传入的响应,解释如下。在一种情况下,我们期望另一方向我们发送一个请求,我们只需返回一个响应;在另一种情况下,是我们发送一个请求并等待另一方发送响应。我们将借助我们可以传递的带外信息来区分这一点,采用 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) 方法必须等到响应被接收。我们需要一种方法来信号化响应的到来。解决这个问题的最佳方法是实现互补的异步方法 BeginSendMessageEndSendMessage

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();
}

您需要仔细比较 EndSendMessageProcessResponse 方法。可以理解的是,一旦我们发送了请求,就必须等到响应到达。

现在,让我们转向处理传入请求的情况,如 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”项目供您尝试。

© . All rights reserved.