31.5 较低层的协议
本节简要介绍一些在较低层次上进行通信的.NET类。
网络的通信分为几个不同的层次,本章迄今为止讨论的类都是工作在最高层,即处理某些命令的一层。如果考虑使用FTP传输文件,这个概念就非常容易理解,目前的GUI应用程序隐藏了许多FTP 细节,但在命令行上执行FTP还是不久之前的事。在这个环境中,我们显式地键入一些要发送至服务器的命令,以进行下载、上传和列出文件。
FTP并不是依赖于文本命令的惟一高层协议,HTTP、SMTP、POP和其他的协议都基于相似的行为类型,许多现代的图形工具隐藏了命令的传输过程,因此用户一般意识不到这些命令的存在。例如,在Web浏览器中键入URL时、Web请求发送给服务器时,浏览器实际上发送给服务器的是一个纯文本的GET命令,这个命令与FTP的get命令相似。此外,浏览器也可以发送POST命令,要求浏览器在请求上附有其他的数据。
但是,这些协议本身都不足以实现计算机之间的通信。即使客户和服务器都理解某个协议,例如HTTP,它们仍然不能互相理解对方,除非另外有协议说明字符是如何传输的,使用的是什么二进制格式,什么电压用于代表二进制数据中的0和1?这些问题都需要协议规定它们,网络领域的开发人员和硬件工程师通常要查阅协议栈。在列出两个主机进行通信所需的各种协议和机制时,就创建了一个协议栈,其中既有最高层的协议,也有最低层的协议。这种方法利用模块化和分层的方式获得了很有效的通信。
幸运的是,对于大多数的开发工作而言,我们都不需要使用协议堆栈或处理电压级别。但是,如果要编写代码,以便在计算机之间进行高效率的通信,则需要编写的代码可以直接在计算机之间传送二进制数据包。这是TCP之类协议的领域,Microsoft提供的许多类都允许方便地使用该层次上的二进制数据来工作。
低层类
System.Net.Sockets命名空间包含一些相关类,允许直接发送TCP网络请求或在某个端口监听TCP网络请求。其中主要的类如表31-1所示。
表 31-1
类 | 用 途 |
Socket | 这个低层的类用于管理连接。WebRequest、TcpClient和UdpClient等类在内部使用这个类 |
NetworkStream | 这个类是从Stream 派生出来的,它表示来自网络的数据流 |
TcpClient | 允许创建和使用TCP连接 |
TcpListener | 允许监听传入的TCP连接请求 |
UdpClient | 用于为UDP客户创建连接(UDP是另一种TCP协议,但没有得到广泛的使用,主要用于本地网络) |
1. 使用TCP类
传输控制协议(TCP)类为连接和发送两个点之间的数据提供了简单的方法。端点是IP地址和端口号的组合。现有的协议很好地定义了端口号,例如,HTTP使用端口80,而SMTP使用端口25,Internet Assigned Number Authority(即IANA,http://www.iana.org/)把端口号赋予这些已知的服务。除非执行某个已知的服务,否则应选择1024以上的端口号。
TCP数据流构成了目前Internet上的主要传输流。TCP通常是首选的协议,因为它提供了有保证的传输、错误校正和缓存。TcpClient类封装了TCP连接,提供了许多属性来控制连接,包括缓存、缓存器的大小和超时。通过GetStream()方法请求NetworkStream对象时可以附带读写功能。
TcpListener类用Start()方法监听传入的TCP连接。当连接请求到达时,可以使用AcceptSocket()方法返回一个套接字,以与远程机器通信,或使用AcceptTcpClient()方法通过高层的TcpClient对象进行通信。阐明TcpListener 和 TcpClient类如何工作的最简单的方式是举一个示例。
2. TcpSend和TcpReceive示例
为了说明这两个类,需要建立两个应用程序。第一个应用程序是TcpSend客户应用程序,如图31-8所示。这个应用程序打开一个到服务器的TCP连接,并为它自己发送C#源代码。
图 31-8
再创建一个C# Windows应用程序,其中的窗体包含两个文本框(txtHost 和 txtPort),分别用于主机名和端口,该窗体还有一个按钮(btnSend),单击它可以启动连接。首先,确保包含相关的命名空间:
using System.Net;
using System.Net.Sockets;
using System.IO;
按钮的单击事件处理程序如下所示。
private void btnSend_Click(object sender, System.EventArgs e)
{
TcpClient tcpClient = new TcpClient(txtHost.Text, Int32.Parse(txtPort.Text));
NetworkStream ns = tcpClient.GetStream();
FileStream fs = File.Open("..//..//form1.cs", FileMode.Open);
int data = fs.ReadByte();
while(data != -1)
{
ns.WriteByte((byte)data);
data = fs.ReadByte();
}
fs.Close();
ns.Close();
tcpClient.Close();
}
这个示例用主机名和端口号创建了TcpClient。另外,如果有IPEndPoint类的一个实例,就可以把该实例传送给TcpClient构造函数。在得到NetworkStream类的一个实例后,打开源代码文件,开始读取字节。与许多二进制流一样,这里也需要将ReadByte()方法的返回值和-1相比较,以确定是否到达流的末尾。循环读取了所有的字节,并把它们发送给网络流后,就应关闭所有打开的文件、连接和流。
在连接的另一端,TcpReceive应用程序显示传输完成后接收到的文件,该应用程序如图31-9所示。
图 31-9
该窗体只包含一个RichTextBox控件txtDisplay。TcpReceive应用程序使用TcpListener等待进来的连接。为了避免应用程序界面的冻结,我们使用一个后台线程来等待,然后从连接中读取。因此还需要包含System.Threading命名空间:
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
在窗体的构造函数中,添加一个后台线程:
public Form1()
{
InitializeComponent();
Thread thread = new Thread(new ThreadStart(Listen));
thread.Start();
}
其他重要的代码如下所示。
public void Listen()
{
TcpListener tcpListener = new TcpListener(2112);
tcpListener.Start();
TcpClient tcpClient = tcpListener.AcceptTcpClient();
NetworkStream ns = tcpClient.GetStream();
StreamReader sr = new StreamReader(ns);
string result = sr.ReadToEnd();
Invoke(new UpdateDisplayDelegate(UpdateDisplay),
new object[] {result} );
tcpClient.Close();
tcpListener.Stop();
}
public void UpdateDisplay(string text)
{
txtDisplay.Text= text;
}
protected delegate void UpdateDisplayDelegate(string text);
用。注意这里把端口号 2112 硬编码到应用程序中,因此需要在客户应用程序中输入相同的端口号。
我们使用AccepTcpClient()返回的TcpClient对象打开一个新流,进行读取,与本章前面的示例类似,创建一个StreamReader,把进来的网络数据转换为字符串。在关闭客户机,停止监听程序前,更新窗体的文本框。我们不想从后台线程中直接访问文本框,所以使用窗体的Invoke()方法和一个委托,把得到的字符串作为object参数数组的第一个元素来传送。Invoke()方法可确保调用正确编组到线程中,以控制用户界面上的句柄。
3. TCP和UDP
本节要介绍的另一个协议是UDP(用户数据包协议)。UDP是一个功能较少的简单协议,但其开销也很小,开发人员常常在速度和性能要求比可靠性更高的应用程序中使用UDP,例如视频流应用程序。相反,TCP提供了许多功能来确保数据的传输,它还提供了错误校正、当数据丢失或数据包被损坏时重新传输它们的功能。最后,TCP可缓存进来和出去的数据,还保证在传输过程中,在把数据包传送给应用程序之前,重新编组杂乱的一系列数据包。即使有一些额外的开销,TCP仍是在Internet上使用最广泛的协议,因为它有非常高的可靠性。
4. UDP类
可以看出,与TcpClient 相比,UdpClient类提供了一个较小、较简单的界面。这反映出UDP协议相对简单的本质。TCP和UDP类都在后台使用套接字,但UdpClient类不包含返回网络流以读写数据的方法。相反,成员函数Send()把一个字节数组作为参数,Receive()函数则返回一个字节数组。另外,因为UDP是一个无连接的协议,所以可以指定把通信的端点,作为Send() 和 Receive()方法的一个参数,而不是在前面的构造函数或Connect()方法中指定。也可以在某个后续的发送或接收过程中修改端点。
下面的代码段使用UdpClient类给回应服务(echo service)发送消息。带有回应服务的服务器在端口7处接收TCP或UDP连接。回应服务值把发送给服务器的数据再发送回客户机。这个服务可用于诊断和测试,但许多系统管理员从安全的角度考虑,不启用回应服务。
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace Wrox.ProCSharp.InternetAccess.UdpExample
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
UdpClient udpClient = new UdpClient();
string sendMsg = "Hello Echo Server";
byte [] sendBytes = Encoding.ASCII.GetBytes(sendMsg);
udpClient.Send(sendBytes, sendBytes.Length, "SomeEchoServer.net", 7);
IPEndPoint endPoint = new IPEndPoint(0,0);
byte [] rcvBytes = udpClient.Receive(ref endPoint);
string rcvMessage = Encoding.ASCII.GetString(rcvBytes,
0,
rcvBytes.Length);
// should print out "Hello Echo Server"
Console.WriteLine(rcvMessage);
}
}
}
Encoding.ASCII类常常用于把字符串转换为字节数组,或把字节数组转换为字符串。还要注意IPEndPoint应按引用传送给Receive()方法。UDP不是一个面向连接的协议,所以对Receive()的每次调用都会从不同的端点读取数据,Receive()会用发送主机的IP地址和端口填充该参数。
UdpClient 和 TcpClient在最低层的类Socket上提供了一个抽象层。
5. Socket类
Socket类提供了网络编程的最高级控制。说明该类的最简单方式是用Socket类重新编写TcpReceive应用程序。更新后的Listen()方法如下所示:
public void Listen()
{
Socket listener = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Any, 2112));
listener.Listen(0);
Socket socket = listener.Accept();
Stream netStream = new NetworkStream(socket);
StreamReader reader = new StreamReader(netStream);
string result = reader.ReadToEnd();
Invoke(new UpdateDisplayDelegate(UpdateDisplay),
new object[] {result} );
socket.Close();
listener.Close();
}
Socket类需要再编写几行代码来完成相同的任务。对于初学者来说,构造函数的参数需要为使用TCP协议的流套接字指定IP寻址模式。这些参数只是可用于Socket类的许多组合中的一个,TcpClient类会配置这些设置。接着把监听器的套接字绑定到一个端口上,开始监听传入的连接。当传入一个连接时,就可以使用Accept()方法创建一个新的套接字,来处理该连接。最后为套接字创建一个StreamReader实例,来读取传入的数据,其方式与前面的大致相同。
Socket类也包含许多方法,用于异步接收、连接、发送和接收数据。使用这些方法和回调委托的方式与前面用WebRequest类请求异步页面的方式相同。如果确实需要了解套接字的内部情况,可以使用GetSocketOption() 和 SetSocketOption()方法,它们允许查看和配置各种选项,包括超时、生存期和其他低级选项