在网络编程中,客户端和服务器之间的双向通信是一个常见的需求。本文将介绍如何使用C#实现这种通信方式。通常情况下,如果双方被防火墙隔开,直接连接到对方端点的方法可能行不通,因为网络管理员通常不愿意允许出站连接。因此,维护并重用已建立的连接进行双向通信是唯一的选择。
要实现这一点,必须区分传入的请求和传入的响应。在一种情况下,期望对方发送给一个请求,只需返回一个响应;在另一种情况下,是发送一个请求并等待对方发送给一个响应。将使用HTTP协议来传递的带外信息来区分这两种情况。
将使用HTTP消息进行点对点通信。一旦监听套接字接受连接,将假设并完全接收一个HTTP消息,然后可以很容易地将其识别并处理为请求或响应。以下是一些代码来说明基本思想:
TcpListener listener = new TcpListener(7070);
listener.Start();
Socket socket = listener.AcceptSocket();
HttpMessage msg = new HttpMessage();
msg.Receive(socket);
if (msg.IsResponse) {
ProcessResponse(msg);
} else {
ProcessRequest(msg);
}
HttpMessage对象简单地从套接字流读取HTTP协议的基本部分,第一行,可选的头列表和消息体。通过检查第一行的第一个标记来区分请求和响应。在响应的情况下,第一行必须以"HTTP"开头。这就是msg.IsResponse确定的。
最关心的是如何将响应与之前发出的请求相关联。让检查发送请求的方法。
void SendRequest(Socket socket, HttpMessage msg) {
msg.CorrelationID = Guid.NewGuid().ToString();
msg.Send(socket);
_requests[msg.CorrelationID] = msg;
}
核心思想是创建并附加一个唯一的标识符到传出的HTTP消息。期望这个标识符也出现在返回的HTTP响应消息中。以下是基本的HTTP协议交换的示例。
// 传出请求
GET / HTTP/1.1
Correlation-ID: 0B83745D-2AAB-4bce-8AC9-B8A590F07768
// 传入响应
HTTP/1.1 200 Ok
Correlation-ID: 0B83745D-2AAB-4bce-8AC9-B8A590F07768
双向通信的核心思想现在已经描述了。让继续开发一个可以实际部署的模块。想要的是一个类,它可以管理双向通信的细节,可以像这样部署:
TcpListener listener = new TcpListener(7070);
listener.Start();
while (true) {
Socket socket = listener.AcceptSocket();
Connection conn = new Connection(socket);
new Thread(new ThreadStart(conn.ThreadProc)).Start();
}
Connection类负责管理双向通信。将接受的套接字传递给它,并依赖连接对象使用同一个套接字发送和接收HTTP消息。为了等待并接收额外的连接,生成一个工作线程来管理已建立的连接。以下是连接的线程过程:
void ThreadProc() {
while (Continue()) {
HttpMessage msg = new HttpMessage();
msg.Receive(_socket);
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) {
request.CorrelationID = Guid.NewGuid().ToString();
request.Send(_socket);
IAsyncResult async = new HttpAsyncResult();
_requests[request.CorrelationID] = async;
return async;
}
public HttpMessage EndSendMessage(IAsyncResult async) {
if (!async.IsCompleted) async.AsyncWaitHandle.WaitOne();
HttpMessage response = (HttpMessage)_requests[async];
_requests.Remove(async);
return response;
}
在讨论这段代码之前,让展示同步版本的实现。它非常简单。
public HttpResponse SendRequest(HttpRequest request) {
IAsyncResult async = BeginRequest(request);
return EndRequest(async);
}
让讨论异步版本。将传出消息的相关ID映射到一个可等待的对象,该对象实现了IAsyncResult接口。显然,可等待的对象需要设置,当传出消息的响应到达时。这必须发生在ProcessResponse方法中。以下是它的实现:
void ProcessResponse(HttpMessage response) {
HttpAsyncResult async = (HttpAsyncResult)_requests[response.CorrelationID];
_requests.Remove(response.CorrelationID);
_requests[async] = response;
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) {
HttpMessage response = DelegateRequest(request);
response.CorrelationID = request.CorrelationID;
response.Send(_socket);
}
temporary queue for storing the request Queue _queue = Queue.Synchronized(new Queue());
void ProcessRequest(HttpMessage request) {
_queue.Enqueue(request);
new Thread(new ThreadStart(this.ProcessRequestThreadProc)).Start();
}
delegate member public ProcessRequestDelegate DelegateRequest;
private method void ProcessRequestThreadProc() {
HttpMessage request = (HttpMessage)_queue.Dequeue();
HttpMessage response = DelegateRequest(request);
response.CorrelationID = request.CorrelationID;
response.Send(_socket);
}