0
点赞
收藏
分享

微信扫一扫

C#GJBC-较底层的协议


 


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()方法,它们允许查看和配置各种选项,包括超时、生存期和其他低级选项

举报

相关推荐

0 条评论