1.1 基本概念
.NET中的System.Net.Socktes命名空间提供了大量的对网络编程的支持类,这些类对Socket编程提供了良好的封装和支持,涵盖了TCP、UDP等连接和无连接的通信。应用程序可以通过 TCPClient、TCPListener 和 UDPClient 类使用传输控制协议 (TCP) 和用户数据文报协议 (UDP) 服务。
这些协议类建立在 System.Net.Sockets.Socket 类的基础之上,负责数据传送的细节。TCPListener、TCPClient、UDPClient类在底层使用 Socket 类的同步方法提供对网络服务的简单直接的访问,没有维护状态信息的系统开销,使用他们可以不需要了解协议特定的套接字的设置细节。
另一方面,上述包装类都是封装Socket的同步处理过程,如果要使用异步 Socket 方法,可以使用 NetworkStream 类提供的异步方法。
这三个协议类的主要功能描述如下:
TCPListener | 从TCP的客户端监听连接。 1、TcpListener 类提供一些简单方法,用于在阻塞同步模式下侦听和接受传入连接请求。可使用 TcpClient 或 Socket 来连接 TcpListener。可使用 IPEndPoint、本地 IP 地址及端口号或者仅使用端口号,来创建 TcpListener。可以将本地 IP 地址指定为 Any,将本地端口号指定为 0(如果希望基础服务提供程序为您分配这些值)。如果您选择这样做,可在连接套接字后使用 LocalEndpoint 属性来标识已指定的信息。 2、Start 方法用来开始侦听传入的连接请求。Start 将对传入连接进行排队,直至您调用 Stop 方法或它已经完成 MaxConnections 排队为止。可使用 AcceptSocket 或 AcceptTcpClient 从传入连接请求队列提取连接。这两种方法将阻塞线程。如果要避免阻塞,可首先使用 Pending 方法来确定队列中是否有可用的连接请求。 3、调用 Stop 方法来关闭 TcpListener。 |
TCPClient | 为TCP网络服务提供客户端连接。 1、TcpClient 类提供了一些简单的方法,用于在同步阻塞模式下通过网络来连接、发送和接收流数据。 2、为使 TcpClient 连接并交换数据,使用 TCP ProtocolType 创建的 TcpListener 或 Socket 必须侦听是否有传入的连接请求。可以使用下面两种方法之一连接到该侦听器:
3、要发送和接收数据,请使用 GetStream 方法来获取一个 NetworkStream。调用 NetworkStream 的 Write 和 Read 方法与远程主机之间发送和接收数据。使用 Close 方法释放与 TcpClient 关联的所有资源。 |
UDPClient | 提供用户数据包(UDP)网络服务。 1、UdpClient 类提供了一些简单的方法,用于在阻塞同步模式下发送和接收无连接 UDP 数据报。因为 UDP 是无连接传输协议,所以不需要在发送和接收数据前建立远程主机连接。但您可以选择使用下面两种方法之一来建立默认远程主机:
2、可以使用在 UdpClient 中提供的任何一种发送方法将数据发送到远程设备。使用 Receive 方法可以从远程主机接收数据。 3、UdpClient 方法还允许发送和接收多路广播数据报。使用 JoinMulticastGroup 方法可以将 UdpClient 预订给多路广播组。使用 DropMulticastGroup 方法可以从多路广播组中取消对 UdpClient 的预订。 |
此外,TcpClient 和 TcpListener类创建在Socket之上,在Tcp服务方面提供了更高层次的抽象,体现在网络数据的发送和接受方面,是TcpClient使用标准的Stream流处理技术——使用 NetworkStream 类表示网络——使用 TcpClient的GetStream 方法返回网络流,然后调用该流的 Read 和 Write 方法实现网络数据的读取和写入(接收和发送)。值得说明的是:NetworkStream 不拥有协议类的基础套接字,因此关闭它并不影响套接字。
采用流技术,使得网络数据的读写数据更加方便直观,同时,.Net框架负责提供更丰富的结构来处理流,贯穿于整个.Net框架中的流具有更广泛的兼容性,构建在更一般化的流操作上的通用方法使我们不再需要困惑于文件的实际内容(HTML、XML 或其他任何内容),应用程序都将使用一致的方法(Stream.Write、Stream.Read) 发送和接收数据。另外,流在数据从 Internet 下载的过程中提供对数据的即时访问,可以在部分数据到达时立即开始处理,而不需要等待应用程序下载完整个数据集。.Net中通过NetworkStream类实现了这些处理技术。
NetworkStream 类包含在.Net框架的System.Net.Sockets 命名空间里,该类专门提供用于网络访问的基础数据流。NetworkStream 实现通过网络套接字发送和接收数据的标准.Net 框架流机制。NetworkStream 支持对网络数据流的同步和异步访问。NetworkStream 从 Stream 继承,后者提供了一组丰富的用于方便网络通讯的方法和属性。
同其它继承自抽象基类Stream的所有流一样,NetworkStream网络流也可以被视为一个数据通道,架设在数据来源端(客户Client)和接收端(服务Server)之间,而后的数据读取及写入均针对这个通道来进行。
.Net框架中,NetworkStream流支持两方面的操作:
1、 写入流。写入是从数据结构到流的数据传输。
2、读取流。读取是从流到数据结构(如字节数组)的数据传输。
与普通流Stream不同的是,网络流没有当前位置的统一概念,因此不支持查找和对数据流的随机访问。相应属性CanSeek 始终返回 false,而 Seek 和 Position 方法也将引发 NotSupportedException。
1.2 应用示例
1、使用TcpListener建立TCP网络侦听
使用TcpListener建立网络侦听,实际上是很简单的过程,示例如下:
//在本机的1300端口建立侦听 TcpListener server=null; try { // Set the TcpListener on port 13000. Int32 port = 13000; IPAddress localAddr = IPAddress.Parse("127.0.0.1"); server = new TcpListener(localAddr, port);
// Start listening for client requests. server.Start(); //省去了对数据接收、发送的处理代码 …… } catch(SocketException e) { Console.WriteLine(e.Message); } finally { server.Stop(); } |
2、使用TcpClient建立和服务器的连接
方法一: String server = “192.168.0.18”; //服务器地址 Int32 port = 13000; //端口号 TcpClient client = new TcpClient(server, port); //构造客户端并尝试连接到服务器 方法二: TcpClient tcpClient = new TcpClient (); tcpClient.Connect ("www.contoso.com", 11002); |
3、读写数据
// Buffer for reading data Byte[] bytes = new Byte[256]; String data = null;
//这里将阻塞线程,直到有客户端连接建立起来 TcpClient client = server.AcceptTcpClient();
// Get a stream object for reading and writing NetworkStream stream = client.GetStream();
int i; // Loop to receive all the data sent by the client. while((i = stream.Read(bytes, 0, bytes.Length))!=0) { // Translate data bytes to a ASCII string. data = System.Text.Encoding.ASCII.GetString(bytes, 0, i); Console.WriteLine("Received: {0}", data);
// Process the data sent by the client. data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
// Send back a response. stream.Write(msg, 0, msg.Length); Console.WriteLine("Sent: {0}", data); }
// Shutdown and end connection client.Close(); |
1.3 应用感触
1、ReadToEnd阻塞线程
在我们最开始的代码中,为了省事儿,在获得了NetworkStream对象后,又使用他构造了一个StreamReader对象,然后使用这个对象的ReadToEnd方法,来一次读完数据,代码如下:
/*建立起的连接*/ _StateDataTcpClient = _StateDataTcpListener.AcceptTcpClient();
/*读取发送到此监听端口上的数据*/ StreamReader streamReader = new StreamReader(_StateDataTcpClient.GetStream(), System.Text.Encoding.Default); string str_result = streamReader.ReadToEnd(); |
这段代码,对读取数据倒是非常简洁,而且程序在刚开始运行的时候一切正常,然而,当系统开发完毕,进行系统测试时,在网络上添加了网络视频服务,它采用UDP广播的形势在局域网内发送视频数据,这样以来,我们的系统问题就来了,症状如下:系统启动后,运行正常,可以正常接收数据,但是在接收到数十条不等的数据后,就再也不接收数据了,但系统其它部分可正常工作。
由于对网络编程的经验欠缺,解决这个问题就花费了很长的时间,几经调试和周折,才最终定位了问题之所在。原来StreamReader的ReadToEnd方法是要阻塞线程的,MSDN给的解释是“ReadToEnd 假定流在到达末尾时会知道已到达末尾。对于交互式协议(服务器仅当被请求时才发送数据而且不关闭连接),ReadToEnd 可能被无限期阻塞,应避免出现这种情况。”对于我们的系统来说,发送数据的客户端是集成的其它系统,至于其数据发送方式,无法准确掌握,但事实是,在这个函数这里,的确阻塞了线程。
在掌握了上述情况后,我们调整了数据接收部分的代码,采用了NetworkStream的Read方法,这样以来,问题就得以解决,经测试,数据接收部分非常正常了,也不再收网络上的UDP数据报的影响了。目前,这个系统已经连续运行了15天,没有出现任何问题。
2、数据要循环读取
在使用NetworkStream读取数据的时候,我采取了大缓冲区,一次读取的方式。也就是说,明知道数据量很小,我构建了一个明显大得多的数据缓冲区,然后使用Read方法读取数据,但是从试验结果来看,一次Read是不能读取完所有数据的。
参考MSDN,解释说:该方法将数据读入 buffer 参数并返回成功读取的字节数。如果没有可以读取的数据,则 Read 方法返回 0。Read 操作将读取尽可能多的可用数据,直至达到由 size 参数指定的字节数为止。如果远程主机关闭了连接并且已接收到所有可用数据,Read 方法将立即完成并返回零字节。
这里说的是,该方法一次读取尽可能多的数据,然而这尽可能多,到底是多少了,我查了网络,也没有获得深入的解释。最后不得不使用MSDN上的示例代码,采用一个循环,来读取数据,直到数据读取为0才停止,如此以来,数据读取部分才完全正常。
1.4 完整示例
下面,是一个使用TcpListener、TcpClient、NetworkStream来读取数据的完整示例。
/*获得本机地址、端口,建立侦听*/ IPAddress localIP = GetLocalIPAddress(); _AlarmDataTcpListener = new TcpListener(localIP, _AlarmDataPort); _AlarmDataTcpListener.Start(int.MaxValue);
//数据接收缓冲区 byte[] buffer = new byte[1024];
//循环侦听并处理数据 while (_IsWorking) { //基础网络流 NetworkStream ns = null;
try { //等待客户端连接 _AlarmDataTcpClient = _AlarmDataTcpListener.AcceptTcpClient();
//已经建立起与客户端的连接,准备接受数据 ns = _AlarmDataTcpClient.GetStream(); ns.ReadTimeout = 3000;
StringBuilder strBuilder = new StringBuilder();
int readLength = 0; //循环接收数据,直到超时 do { try { readLength = ns.Read(buffer, 0, buffer.Length); if (readLength > 0) { strBuilder.AppendFormat("{0}", Encoding.Default.GetString(buffer, 0, readLength)); ns.ReadTimeout = 1000; } } catch (IOException e) { Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace);
readLength = 0; } } while (readLength > 0);
string str_result = strBuilder.ToString();
//清理使用的资源 ns.Close(); ns = null; _AlarmDataTcpClient.Close(); _AlarmDataTcpClient = null;
//显示接收到的数据 Console.WriteLine(str_result);
//使用线程池,启动一个单独的线程来处理此次接收到数据 if (str_result != string.Empty) { ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessReceivedAlarmData), str_result); } }//end try catch (Exception exp) { if (ns != null) { ns.Close(); ns = null; }
if (_AlarmDataTcpClient != null) { _AlarmDataTcpClient.Close(); _AlarmDataTcpClient = null; }
Console.WriteLine(exp.Message); Console.WriteLine(exp.StackTrace); }//end catch }//end while (_IsWorking) |