.Net Socket编程基础 异步编程  同步socket并发性很差,特别是对于服务器端来说,要处理很多客户端连接,同步Socket力不从心,要提高系统的并发处理能力,就要借助.Net异步编程模式。  .Net Socket的异步编程模式和.Net 通用的APM编程模式是一致的,调用BeginXXX方法开始异步操作,系统系统处理完成后调用回调函数,在回调函数中调用EndXXX结束操作。  常用的Socket对象的异步API有 接受客户端连接 BeginAccept, EndAccept 接收数据 BeginReceive, EndReceive 发送数据 BeginSend, EndSend  下面是简单的Socket服务端实现的部分,演示Socket异步API的使用。  首先New一个Socket对象,绑定到特定端口,开始监听客户端发来的连接请求。  
01 <span style="font-size: 10pt;">Socket socket;
02 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
03 socket.Bind(new IPEndPoint(address, port));
04 socket.Listen(backlog);
05 // backlog 参数指定队列中最多可容纳的等待接受的传入连接数,不同操作系统的backlog参数的上限是不一样的
06  
07 //然后调用BeginAccept开始等待客户端连接。
08 while (true)
09 {
10     acceptDone.Reset();
11     socket.BeginAccept(new AsyncCallback(AcceptCallback), socket);
12     acceptDone.WaitOne();
13 }
14  
15 //acceptDone是信号量
16 private ManualResetEvent acceptDone = new ManualResetEvent(false);
17  
18 //AcceptCallback是回调函数,下面是这个函数的部分代码
19  
20 private void AcceptCallback(IAsyncResult ar)
21 {
22     acceptDone.Set();
23     // EndAccept 将会返回Windows自动分配的 Socket 对象实例
24    Socket handler = listener.EndAccept(ar);
25     log.InfoFormat("{0} connected", handler.RemoteEndPoint.ToString());
26  
27     //可以限制允许的最大连接数连接,保存当前对话的session信息等
28    SocketState state = new SocketState(handler);
29     SocketError errorCode;
30     handler.BeginReceive(state.Buffer, 0, state.BufferSize, SocketFlags.None, out errorCode, new AsyncCallback(ReceiveCallback), state);
31  
32     if ((errorCode != SocketError.Success) && (errorCode != SocketError.IOPending))
33     {
34         ProcessError(handler, errorCode, "BeginReceive");
35     }
36 }
37 </span>
BeginXXX的重载列表中一般有一个函数重载允许你传一个SocketError 类型的out参数,可以根据这个参数判断是否出错。  SocketState对象是为了方便写代码构造的用来保存当前Socket状态的类,比如上次接受数据是什么时间,一共接受了多少数据等。  
1 <span style="font-size: 10pt;">public class SocketState : IDisposable
2 {
3 private Socket socket = null;
4 private const int bufferSize = 1024;
5 private byte[] buffer = new byte[bufferSize];
6     。。。
7 }
8 </span>
操作系统接受到数据后会调用回调函数ReceiveCallback   private void ReceiveCallback(IAsyncResult ar) {     SocketState state = (SocketState)ar.AsyncState;     Socket handler = state.Socket;      int bytesRead = handler.EndReceive(ar); 	     if (bytesRead > 0)     {        //处理接受的数据         。。。         //如果还有数据,继续接受         if (handler.Available > 0 || handler.Connected )           {               SocketError errorCode;               handler.BeginReceive(state.Buffer, 0, state.BufferSize, SocketFlags.None, out errorCode, new AsyncCallback(ReceiveCallback), state);          }          else         {             handler.Close();             //其他处理代码,比如重置连接计数器等       }      } } 
上面代码只是简单介绍API的用法,很多地方没有考虑,完整的Socket服务代码比这个要复杂得多。  BeginXXX、EndXXX这种异步模式比同步方式要高效得多,但是每次需要New一个IAsyncResult对象和state对象,并发高的情况下对GC也是不小的压力,频繁的申请、释放小块内存,容易产生很多小的内存碎片,内存碎片多的话内存利用率低,而且可能出现即使内存没有全部用完,但是.Net Runtime还是会报告没有可分配的内存的情况。另一方便,编程模型比较复杂。  为了进一步简化Socket异步编程,.Net3.5引入了新的编程模型。
 .Net 3.x 的Socket异步编程  在.Net 3.5中引入了一组增强功能,提供可供专用的高性能套接字应用程序使用的可选异步模式,并且简化了Socket异步编程复杂度。SocketAsyncEventArgs类是这组增强功能中最常用的一个类,专为需要高性能的网络服务器应用程序而设计。它主要的成员如下:  OnCompleted事件:在异步操作完成后,系统会触发OnCompleted事件,在事件处理代码中可以进行后续异步套接字操作的处理。 SetBuffer方法:初始化要用于异步套接字方法的数据缓冲区。从上面的一些例子可以看到,不管同步还是异步,都需要一个缓冲数组来接受数据,使用SetBuffer可以简化缓冲区的设置。 LastOperation属性:获取最近使用此上下文对象执行的套接字操作类型。Accept、Receive、Send都可以用SocketAsyncEventArgs类,需要通过这个属性来判断上次进行的是什么操作。 SocketError:异步套接字操作的结果, SocketError.Success 表示操作成功完成  .Net 3.X这些Socket增强功能的主要特点是可以避免在异步套接字 I/O 量非常大时发生重复的对象分配和同步,怎么实现呢?就是利用很通用的对象缓冲池技术。  一般需要对两类对象使用对象池,一个对象池存放SocketAsyncEventArgs对象实例,一个对象池存放缓存byte[]实例,每次需要SocketAsyncEventArgs对象或者缓存数组时,就从对象池中取一个,用完了再放回对象池。对象池方式管理分配的对象属于通用的高效内存使用方式,不是Socket编程特有的模式。  这两个对象池实现的例子可以参考MSDN上的示例代码。 	 使用此组执行异步套接字操作的模式包含以下步骤:(来自MSDN) 1.分配一个新的 SocketAsyncEventArgs 上下文对象,或者从应用程序池中获取一个空闲的此类对象。 2.将该上下文对象的属性设置为要执行的操作(例如,完成回调方法、数据缓冲区、缓冲区偏移量以及要传输的最大数据量)。 3.调用适当的套接字方法 (xxxAsync) 以启动异步操作。 4.如果异步套接字方法 (xxxAsync) 返回 true,则在回调中查询上下文属性来获取完成状态。 5.如果异步套接字方法 (xxxAsync) 返回 false,则说明操作是同步完成的。可以查询上下文属性来获取操作结果。 6.将该上下文重用于另一个操作,将它放回到应用程序池中,或者将它丢弃。  用代码来说明更直接一些,我们来改写上面的例子  
01 <span style="font-size: 10pt;">//创建Socket对象,开始监听
02 //创建Accept用的SocketAsyncEventArgs对象实例,指定事件处理函数
03 SocketAsyncEventArgs  acceptArgs = new SocketAsyncEventArgs();
04 acceptArgs.Completed += Process_Accept;      
05  
06 //开始等待客户端的连接
07 if (!socket.AcceptAsync(e))
08 {
09     Process_Accept(this, e);
10 }
11  
12 //在事件处理函数Process_Accept中
13 ///。。。
14  
15 //继续等待其他客户端连接
16 Socket acceptSocket = e.AcceptSocket;
17 StartAccept(e);
18  
19 //开始异步接收数据
20 SocketAsyncEventArgs  socketReceiveArgs = new SocketAsyncEventArgs();
21 socketReceiveArgs.Completed += Process_Receive;
22 socketReceiveArgs.SetBuffer(receiveBuffer, 0, bufferSize);
23 if (!socket.ReceiveAsync(socketReceiveArgs))
24 {
25     Process_Receive(this, socketReceiveArgs);
26 }
27  
28 //在事件处理函数Process_Receive中处理接收到的数据
29  
30 if (e.SocketError == SocketError.Success)
31 {
32     if (e.BytesTransferred > 0)
33     {
34         byte[] data = new byte[e.BytesTransferred];
35         Buffer.BlockCopy(e.Buffer, e.Offset, data, 0, e.BytesTransferred);
36         //处理数据
37       。。。
38       //继续接受数据
39       socketReceiveArgs.SetBuffer(receiveBuffer, 0, bufferSize);
40         if (!socket.ReceiveAsync(socketReceiveArgs))
41         {
42              Process_Receive(this, socketReceiveArgs);
43         }
44     }
45     else
46     {
47         //关闭连接
48     }
49 }
50 else
51 {
52     //错误处理
53 }
54 </span>
同样以上代码仅供观赏,只演示了API的用法,很多情况没考虑,不具有实用价值  下一节介绍Socket的错误处理 
Socket错误检测  这里只介绍一些基本的错误检测概念,不涉及一些具体类型的错误处理,比如检测客户端异常断开。  Socket编程常见的错误类型是SocketException,ScoketException是从System.ComponentModel.Win32Exception继承下来的,所以也有ErrorCode属性,NativeErrorCode属性和GetObjectData方法。NativeErrorCode对应Windows的Scoket错误代码,具体含义可以在MSDN上查找。  程序出现了Socket异常也不一定出现了严重的网络错误,需要关闭Socket连接,比如NativeErrorCode为10035,代表操作没法马上完成,比如缓冲区已满,或者缓冲区暂时没有数据可以接受。  另外一个会遇到的异常是ObjectDisposedException,Socket连接断开端口已经释放时会引发这个异常。  在Socket异步编程模型中,有些错误是不能通过Try Catche这样的方式检测的,为了判断是否发生错误,在.Net 2.0的异步编程模型里可以在调用BeginXXX或者EndXXX时使用SocketError类型的参数来检测,SocketError是一个枚举类型,表示Socket操作的处理结果,SocketError.Success代表成功处理,除此之外代表可能发生了错误,错误代码的含义应该和上面SocketException异常的NativeErrorCode是一致的。  .Net 3.0提供的Socket异步编程可以用SocketAsyncEventArgs类的SocketError属性来检测是否发生错误,SocketError属性是SocketError枚举类型,在OnCompleted的事件处理函数中应该先检测是否有错误发生,如果有错误要先根据错误类型处理错误。  接下来简单介绍Silverlight中的Scoket编程。
 

Silverlight中的socket编程

 

如果没有特声明,我们下面所讨论的内容都是基于Silverlight 3.0的。

Silverlight Socket编程和前面所述.net的socket编程类似,但是因为web的特殊环境,出于安全等因素的考虑,Silverlight Socket有一些特殊的限制。

首先,Silverlight Socket都是异步的,也就是说,需要用前面讲的.net 3.5的异步编程模式开发。

其次,Silverlight Socket只支持Tcp协议,并且Silverlight Socket限制了可使用端口的范围,端口必须在4502-4534范围之内。

另外一个比较重要的限制是Silverlight的安全策略系统。

 

Silverlight网络安全策略

 

出于安全方面的原因,Silverlight socket发起一个新的连接请求时,需要先向远端服务器请求一个策略文件,之后才允许网络连接访问该目标域下的网络资源。

请求策略文件时,Silverlight发送一个字符串<policy-file-request/>到服务器的943端口,服务器程序需要接收该请求,分析是否是策略请求后,发送一个策略文件的字符串给客户端。这个策略文件定义了Silverlight能访问那些资源以及允许使用什么样的方式访问。

 

Silverlight支持两种网络安全策略文件。

 

Flash 策略文件,此策略文件只可由 System.Net 命名空间中的 WebClient 和 HTTP 类使用。

Silverlight 策略文件,既可由 System.Net 命名空间中的 WebClient 和 HTTP 类使用,也可由 System.Net.Sockets 命名空间中的套接字类使用的 Silverlight 策略文件。

在连接某个网络资源之前,Silverlight会尝试从目标域下载安全策略文件。具体哪种类型取决于连接请求是来自WebClient或HTTP 类,还是来自Socket。客户端请求策略文件的过程是Silverlight自动处理的,不需要你写代码去控制。

 

如果Silverlight收到了服务器返回的策略文件,在Silverlight应用程序的整个会话期间,该文件将用作Socket针对该目标站点的所有后续请求的策略文件。

 

如果没有收到策略文件或者策略文件分析失败,Silverlight将拒绝到网络资源的连接,任何连接请求都将失败。

 

下面的内容使用 DTD 介绍 Silverlight 策略文件格式, 每个元素的含义请参考msdn

http://msdn.microsoft.com/zh-cn/library/cc645032(VS.95).aspx

<?xml version=”1.0″ encoding=”ISO-8859-1″?>

<!– A DTD for the Silverlight Policy File –>

<!ELEMENT access-policy (cross-domain-access)>

<!ELEMENT cross-domain-access (policy+)>

<!ELEMENT policy (allow-from)>

<!ELEMENT policy (grant-to)>

<!ELEMENT allow-from (domain+)>

<!ATTLIST allow-from http-request-headers CDATA>

<!ELEMENT domain EMPTY >

<!ATTLIST domain uri CDATA #REQUIRED>

<!ELEMENT allow-from http-methods CDATA>

<!ELEMENT grant-to (resource+)>

<!ELEMENT grant-to (socket-resource+)>

<!ELEMENT grant-to EMPTY>

<!ATTLIST resource path CDATA #REQUIRED>

<!ATTLIST resource include-subpaths (true|false) “false”>

<!ATTLIST socket-resource port CDATA #REQUIRED protocol #REQUIRED>

<!– End of file. –>

 

下面是一个Socket策略文件的示例(MSDN的例子)

<?xml version=”1.0″ encoding =”utf-8″?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from>
<domain uri=”file:///” />
</allow-from>
<grant-to>
<socket-resource port=”4502-4506″ protocol=”tcp” />
</grant-to>
</policy>

</cross-domain-access>

</access-policy>

Socket策略请求服务器端的实现比较简单,可以参考MSDN Silverlight文档中《Silverlight中的网络安全访问限制》,网上的例子也很多,不再累述。

 

Web上其他的通信方式

 

除了Silverlight Socket之外,还有很多种web端和服务器的通信方式。

 

HTTP Polling

基于AJAX的轮询。比较容易理解,Web端以轮询方式向服务器发请求。这种方式从直观来看,实现起来最简单,性能也有问题,但是简单和复杂,快和慢都是相对的。

为了提高效率,可以减少每次request和reponse传输数据量,比如简化http header,简化cookie,采用Restful web service等等。

好像web qq和google wave都是HTTP Polling机制。

 

Comet

基于HTTP长连接的”服务器推”技术,大概两种类型:一种是服务器端阻塞异步AJAX http请求直到有数据或超时才返回,另外一种是利用Iframe服务器端将数据推到web端,与第一种不同的是,服务器端并不直接返回数据,而是返回对客户端Javascript函数的调用。

Comnet效率比HTTP Polling高,但是架构比较复杂。web客户端角度要注意浏览器对HTTP长连接数的限制,另外客户端的控制请求和数据请求应该使用不同的HTTP连接。服务器端因

为要维护大量的长连接需要注意服务的性能和可扩展性,有些web服务器专门针对comet优化过。

长连接通信的通用模式,服务器端和客户端之间需要实现心跳算法来检测另一方是否在线。

 

Flash XMLSocket

利用Flash的socket机制与Server通信。

 

Wcf HTTP Duplex Services

Silverlight 3.0可以使用Wcf HTTP Duplex Services实现Web端和服务器端的双向通信,网上有很多例子,HTTP Duplex Services是基于HTTP Polling的。

 

Web Sockets API

还只是草案,参考地址:http://dev.w3.org/html5/websockets/

现在google chrome最新的开发版已经支持Web Sockets API,测试代码如下

01 <span style="font-size: 10pt;">if ("WebSocket" in window) {
02   var ws = new WebSocket("ws://example.com/service");
03   ws.onopen = function() {
04     ws.send("message to send");
05   };
06   ws.onmessage = function (evt) {alert(evt.data); };
07   ws.onclose = function() { alert("closed");};
08 } else {
09   alert("sad");
10 }
11 </span>

http tunnel

讲到HTTP,顺便讲HTTP 隧道(http tunnel)。

因为很多防火墙或者代理服务器只支持http的相关端口,为了能够绕过这些限制,就要想办法利用http(https)协议。

一般防火墙和代理服务器只是简单的检查http头信息,可以要把要传输的数据伪装成符合http协议的文本数据。

另外防火墙和代理服务器一般不会检查https协议传输的内容,所以可以利用这一点来绕开端口限制,具体方法就是先发送一个http头:

CONNECT xxxxx.com:443 HTTP/1.0
HOST xxxxx.com:443
一些http头信息
//空行结束

代理服务器看到是https协议就会放行,返回

HTTP/1.0 200 Connection Established
http 头信息//空行结束

连接就建立起来了。

 

通过配置文件来配置System.Net

 

详细配置元素信息参考

msdn 网络设置架构 http://msdn.microsoft.com/zh-cn/library/dacty7ed(VS.80).aspx
msdn system.Net 元素(网络设置)
http://msdn.microsoft.com/zh-cn/library/6484zdc1(VS.80).aspx

 

Socket Trace

要Trace Socket的信息,可以自己写日志文件,也可以利用.Net自带的Trace功能

配置节例子

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

<system.diagnostics>

<trace autoflush="true" />

<sources>

<source name="System.Net" maxdatasize="1024">

<listeners>

<add name="MyTraceFile"/>

</listeners>

</source>

<source name="System.Net.Sockets">

<listeners>

<add name="MyTraceFile"/>

</listeners>

</source>

<sharedListeners>

<add

name="MyTraceFile"

type="System.Diagnostics.TextWriterTraceListener"

initializeData="System.Net.trace.log"

/>

</sharedListeners>

<switches>

<add name="System.Net" value="Verbose" />

</switches>

</system.diagnostics>

</configuration>

.Net 4.0中Socket编程新功能

 

DnsEndPoint

silverlight中已经有DnsEndPoint了,.Net 4.0也会增加。
IPAddress[] IPs = Dns.GetHostAddresses(“www.contoso.com”);

 

IP Version Neutrality

设置Socket的属性为IPv6Only为false就能兼容IPv4,可以不用分别为IPv4和IPv6创建socket对象了。

socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);

 

NAT Traversal

 

TcpListener的示例代码

var listener = new TcpListener(IPAddress.IPv6Any, 8000);
listener.AllowNatTraversal(true);
listener.Start();

 

使用Socket的示例代码

Socket新增了一个方法
public void SetIPProtectionLevel (IPProtectionLevel);

枚举IPProtectionLevel的定义如下

public enum IPProtectionLevel {     Unspecified = –1,    // platform default     Unrestricted = 10,   // global with NAT traversal     EdgeRestricted = 20, // global without NAT traversal     Restricted = 30,     // site local }

或者通过配置文件来设置

<system.net>

<settings>

<!– default is platform defined (Unspecified) –>

<socket ipProtectionLevel=”Unrestricted | EdgeRestricted | Restricted | Unspecified”/>

</settings>

</system.net>

详细信息参考End-to-end connectivity with NAT traversal

http://blogs.msdn.com/ncl/archive/2009/07/27/end-to-end-connectivity-with-nat-traversal-.aspx

 

Silverlight 4中的网络新功能

UDP Multicast

Silverlight 4增加了两个新类:UdpSingleSourceMulticastClient和UdpAnySourceMulticastClient来处理UDP Multicast。

 

Net.TCP Port Sharing Service

按照官方的说法,性能和比HTTP Polling Duplex有极大的提高。

吞吐量:对于UI线程来说提升了5.5倍,对于worker线程来说提升了870倍。
客户端数量数:服务器可支持的连接客户端数量是之前的5-6倍。