网络编程

学习目标:
网络无疑是当前计算机程序中最重要的应用平台。微软推出的这一套.NET框架,顾名思义就是要在网络上建立一个统一的操作平台。
本章将介绍网络编程的概念和TCP/IP协议;介绍了如何使用.NET Framework进行网络通信;本章还将介绍一些可用于网络编程的控件的使用方法。
11.1网络基础
 在介绍c#网络编程之前,首先介绍一下有关网络的基础知识。
11.1.1网络概述
计算机网络是整个计算机业发展最为迅速的一个领域,网络应用程序的需求也在日益扩大。所谓计算机网络就是通过通信线路互连起来的计算机的集合。首先它是计算机的一个群体,是由多台计算机组成的。其次这些计算机是通过一定的通信介质互连在一起的。计算机之间的互连是指它们彼此之间能够交换信息。互连通常有两种方法:计算机间通过双绞线、同轴电缆、电话线、光纤等有形通信介质互相连接,或通过激光、微波、地球卫星通信信道等无形介质互连。
在计算机网络产生之初,每个计算机厂商都有一套自己的网络体系结构的概念,它们之间互不相容。为此,国际标准化组织(ISO)在1979年建立了一个分委会来专门研究一种用于开放系统互联的体系结构(Open Systems Interconnection,简称OSI)。
OSI参考模型分为七层,分别是:物理层,数据链路层,网络层,传输层,会话层,表示层和应用层。
但是实际上,现在最普及的网络分层方法是TCP/IP协议中规定的五层方法,整个Internet就是以TCP/IP协议为基础建立起来的,下面就来介绍TCP/IP协议。
11.1.2 TCP/IP网络协议
协议时对等的网络实体间的通信的规则,可以简单理解为网络上各计算机彼此交流的一种“语言”。
TCP/IP参考模型是因特网(Internet)的基础,它分为五层,分别是物理层,数据链路层,网络层、传输层和应用程。通常说的TCP/IP是一组协议的总称,TCP/IP实际上一个协议族,包括100多个相互关联的协议,其中IP(Internet Protocol,网际协议)是网络层最主要的协议;TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)是传输层中最主要的协议。一般认为IP,TCP,UDP是最根本的三种协议,是其他协议的基础。
IP定义了数据按照数据报传输的格式和规则;TCP提供了可靠的、面向连接的协议;UDP是不可靠、无连接的协议。
TCP建立在IP之上(这也是TCP/IP的由来),定义了网络上程序到程序的数据传输格式和规则,提供了数据报传输确认、丢失重发、以及数据报按照发送次序重新装配的机制。TCP类似于打电话,在开始传输数据之前,必须要建立明确的连接。
UPD也建立在IP之上,但它是一种无连接的协议,两台计算机之间的传输类似于发送短信,消息从一台电脑发送到另一台电脑,两者之间没有明确的连接,UDP并不保证数据传输过程的完整性和正确性,所以说它是一种不可靠的协议,但相对于TCP来说,UDP有更好的传输效率。
TCP/IP使用协议栈来工作,栈是所有用来在两台机器间完成一个传输所需要的所有协议的一个集合。栈分为五个层次,每一层都能从相邻的层中接收或者发送数据,每一层都与许多协议相关联。TCP/IP协议族的最主要协议如表11-1所示:
                表11-1 TCP/IP协议族
 
    层次      主要协议   
应用层 HTTP、FTP、SMTP、DNS、Telnet……   
传输层 TCP、UDP、DVP…….   
网络层 IP、ICMP、ARP、RARP……   
链路层 Enternet、Arpanet、PDN……   
物理层 允许任何协议 
下面介绍一些常用的协议。
网络层协议管理离散的计算机间的数据传输,用户一般注意不到,如IP协议为用户和远程计算机提提供了信息包的传输方法,确保信息包能正确到达目的地;ARP协议(地址解析协议)的作用是将IP地址映射为物理地址;ICMP协议(Internet控制消息协议)是用来在两台计算机之间传输时处理错误和控制消息的,比如ping就是常用的由ICMP实现的网络工具,可以用来判断网络是否通畅。
应用层协议是建立在网络层协议之上,是专门为用户提供应用服务的,一般是可见的。如FTP协议(文件传输协议),可以实现从一个系统向另一个系统传输文件;HTTP协议(超文本传输协议)可以在Internet上进行信息传输时使用。SMTP协议(简单邮件传输协议)可以实现邮件传输的可靠性和高效性。


11.2  .NET网络编程组件
11.2.1  .NET中的网络组件
.NET类库为我们提供了很多网络组件以方便网络程序的编写,其中最重要的网络组件主要分布在以下几个命名空间中:
System.Net
System.Net命名空间为当今很多流行的网络协议提供了一个统一的简单的接口,其中WebClient提供了简单访问Internet资源的方法;WebRequest和WebResponse可以让你使用网络上的资源而不用考虑各种协议内部的细节,此外,这个命名空间里还包含IPAddress等用户描述网络地址信息的类。
(2)System.Net.Socket
这个命名空间包含的类通常与较低级别的操作有关。其中Socket类是一个非常基础且强大的类,可以完成几乎所有的TCP/IP的操作,当然我们也可以在Socket的基础上构建TCPClinent、TCPListener等TCP、UDP实现。
(3)System.Web
System.Web包含大部分的实现浏览器/服务器通讯结构的类和接口。它的功能主要集中在于HTTP协议和Web有关的操作上。
(4)System.Web.Service
其中的组件将用于web服务,以HTTP协议和XML的格式为其他应用程序提供一定的基于Web的信息服务。
11.2.2  网络通信中的流
流的概念已经存在很长时间了。流是一个用于传输数据的对象,数据传输一般有两个方向:如果数据从外部源传输到程序中,就是读取流;如果数据从程序传输到外部源,就是写入流。外部源常常是一个文件,但它还可以是网络上资源或者一个指定的管道,也可以是内存中的一片区域。
在这些情况下,.NET提供了一个System.IO.Stream处理对于所有流数据的操作,另外为了使用方便,又提供了System.IO.MemoryStream来读取内存中的数据,使用System.Net.Socket.NetworkStream处理网络数据。
对于流的基本操作如表11-2所示:
 
名称 说明   
CanRead 是否可读   
CanSeek 是否可以直接访问流中的某个位置   
CanWrite 是否可写   
Length 流的长度   
Position 当前访问的位置   
BeginRead 异步读   
BeginWrite 异步写   
Close 关闭流   
EndRead 结束异步读   
EndWrite 结束异步写   
Flush 把缓冲区的操作一次完成   
Read 读   
ReadByte 读字节   
Seek 定位   
Write 写   
WriteByte 写字节 

NetworkStream实现了System.IO.Stream的机制,可以直接使用它进行网络数据的读写,它提供以下功能:
一个统一的从网络中读取数据的方法。
与其他的.NET流兼容,这样可以很方便移植程序。例如你有一个从FileStream中读取XML数据的程序,只要稍加修改就可以变为从网络中读取XML数据的程序。
提供两种操作方式,同步处理和异步处理,其中异步处理提供了在数据到达时处理的功能,避免在程序中等待数据的传输。
但是NetworkStream的操作对象是网络中的资源,所以它不支持随机数据访问,也就是它的CanSeek永远是False,使用它的Position属性或者调用Seek方法都会返回异常。
11.2.3 网络中的编码和解码
网络中的数据传输经常会遇到编码和解码的操作。因为网络是一个公共的混合体,多种平台、多种语言开发的程序都在网络上传输数据,所以网络上存在很多的编码方式:ASCII,UTF7和UTF8等。
这种现状就需要一个可以进行编码方式转换的工具让我们方便的将内码转换为我们系统可以接收的方式,以正确的获取数据。.NET中的System.Text.Encoding类提供类这样的功能。
Encoding类提供类字符串、Unicode字符集和相应编码的字节数组之间的相互转换。有了这些Encoding的方法我们就可以进行内码转换并读取数据了,Encoding类的主要成员如表11-3所示。
                   表11-3 Encoding的主要成员
 
名称 说明   
ASCII 获取 ASCII(7 位)字符集的编码   
BigEndianUnicode 获取采用 Big-Endian 字节顺序的 Unicode 格式的编码   
CodePage 获取此编码的代码页标识符   
Default 获取系统的当前 ANSI 代码页的编码   
EncodingName 获取编码的可读说明   
Unicode 获取采用 Little-Endian 字节顺序的 Unicode 格式的编码   
UFT7 获取 UTF-7 格式的编码   
UTF8 获取 UTF-8格式的编码   
Convert 将字节数组从一种编码转换为另一种   
GetEncoding 获得一种特殊的编码   
GetBytes 将指定的string或字符数组的全部或部分内容编码为字节数组。   
GetChars 将字节数组解码为字符数组   
GetEncoder 为此编码返回一个编码器   
GetDecoder 为此编码返回一个解码器   
GetString 将指定字节数组解码为字符串 
在网络间传输的数据都是以字节数组的形式存在,如果要把从网络中获得的字节数组转化为ASCII字符串,代码如下:
假定Buffer为保存网络间传输的数据的字节数组
String content=System.Text.Encoding.ASCII.GetString(Buffer);
如果要将某个ASCII字符串在网络间传送,则需要一个相反的过程,要将该字符串转化为ASCII字节数组,代码如下:
String content=”123456”;
Byte[] sendData=System.Text.Encoding.ASCII.GetByte(content);
11.3访问Internet
   在网络环境下,我们最感兴趣的两个命名空间是System.Net和System.Net.Sockets。System.Net通常与较高层的操作有关,比如下载文件,使用HTTP协议进行Web请求等,而System.Net.Sockets通常与较底层的操作有关,如果要直接使用套接字或者TCP/IP一类的协议,这个命名空间是非常有用的。本节我们将介绍使用.NET访问Internet的一些方法。
11.3.1 URI
URI一般是用来描述Internet中的网页或者资源的位置。注意术语URL(统一资源定位符)在新的技术规范中已不再使用,现在使用的是URI(统一资源标识符)。URI与URL的含义大致相同,但URI更通用。
在System命名空间下,Uri和UriBuilder两个类都用于表示URI。UriBulider允许把给定的字符串当做URI的组成部分,从而建立一个URI,而Uri类允许分析、组合和比较URI。
11.3.2 文件下载
在这里,我们使用System.Net下包含的HttpWebRequest和HttpWebResponse对象来实现文件的下载功能。
HttpWebRequest对象是从WebRequest对象继承而来,负责对基于HTTP协议的网络资源的请求和访问,HttpWebRequest 类对 WebRequest 中定义的属性和方法提供支持,也对使用户能够直接与使用 HTTP 的服务器交互的附加属性和方法提供支持。
不要使用 HttpWebRequest 构造函数。使用 WebRequest.Create 方法初始化新的 HttpWebRequest 对象。如果统一资源标识符 (URI) 的方案是 http:// 或 https://,则 Create 返回 HttpWebRequest 对象。
如果HttpWenRequest对象请求的资源可用,则返回HttpWebResponse对象。HttpWebResponse里包含请求资源的相关属性,并可用HttpWebResponse对象对请求资源进行操作(下载)。
下表显示可以通过 HttpWebResponse 类的属性使用的公共 HTTP 标头。
 
标头  属性    
Content-Encoding  编码方式   
Content-Length  资源长度   
Content-Type  资源类型 
此外,Systen.Net命名空间下也提供了WebClient对象用来专属下载Internet资源,该对象提供了OpenRead()方法直接以流的形式返回所请求资源。
下面是使用以上几个对象下载网络中文件的示例:


   11.4C#套接字
11.4.1 接字基本概念
  套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
  套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套接字有两种不同的类型:流套接字和数据报套接字。

11.4.2套接字字工作原理
  要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。
  根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
  所谓服务器监听,是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
  所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
  所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
11.4.3 Scket类
 .NetFrameWork为Socket通讯提供了System.Net.Socket命名空间,在这个命名空间里面有以下几个常用的重要类分别是:
  ·Socket类 这个低层的类用于管理连接,WebRequest,TcpClient和UdpClient在内部使用这个类。
  ·NetworkStream类 这个类是从Stream派生出来的,它表示来自网络的数据流
  ·TcpClient类 允许创建和使用TCP连接
  ·TcpListener类 允许监听传入的TCP连接请求
  ·UdpClient类 用于UDP客户创建连接(UDP是另外一种TCP协议,但没有得到广泛的使用,主要用于本地网络)。
其中Socket类是包含在System.Net.Sockets名字空间中的一个非常重要的类。一个Socket实例包含了一个本地以及一个远程的终结点,该终结点包含了该Socket实例的一些相关信息。
Socket 类支持两种基本模式:同步和异步。其区别在于:在同步模式中,按块传输,对执行网络操作的函数(如 Send 和 Receive)的调用一直等到所有内容传送操作完成后才将控制返回给调用程序。在异步模式中,是按位传输,需要指定发送的开始和结束。同步模式是最常用的模式。
下面我们重点讨论同步模式的Socket编程。首先,同步模式的Socket编程的基本过程如下:
1. 创建一个Socket实例对象。
2. 将上述实例对象连接到一个具体的终结点(EndPoint)。
3. 连接完毕,就可以和服务器进行通讯:接收并发送信息。
4. 通讯完毕,用ShutDown()方法来禁用Socket。
5. 最后用Close()方法来关闭Socket。
知道了以上基本过程,我们就开始进一步实现连接并通讯了。在使用之前,需要首先创建Socket对象的实例,这可以通过Socket类的构造方法来实现:
 
public Socket(AddressFamily addressFamily,SocketType socketType,ProtocolType protocolType); 
其中:
addressFamily 参数指定Socket使用的寻址方案,比如AddressFamily.InterNetwork表明为IP版本4的地址;
socketType参数指定Socket的类型,比如SocketType.Stream表明连接是基于流套接字的,而SocketType.Dgram表示连接是基于数据报套接字的。
protocolType参数指定Socket使用的协议,比如ProtocolType.Tcp表明连接协议是运用TCP协议的,而Protocol.Udp则表明连接协议是运用UDP协议的。
在创建了Socket实例后,我们就可以通过一个远程主机的终结点和它取得连接,运用的方法就是Connect()方法:
 
public Connect (EndPoint ep);  
该方法只可以被运用在客户端。进行连接后,我们可以运用套接字的Connected属性来验证连接是否成功。如果返回的值为true,则表示连接成功,否则就是失败。
下面的代码就显示了如何创建Socket实例并通过终结点与之取得连接的过程:
 
IPHostEntry IPHost = Dns.Resolve("http://www.sohu.com/");
string []aliases = IPHost.Aliases;
IPAddress[] addr = IPHost.AddressList;
EndPoint ep = new IPEndPoint(addr[0],80);
Socket sock = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
sock.Connect(ep);
if(sock.Connected)
Console.WriteLine("OK"); 
一旦连接成功,我们就可以运用Send()和Receive()方法来进行通讯。
Send()方法的函数原型如下:
 
public int Send (byte[] buffer, int size, SocketFlags flags); 
其中:
参数buffer包含了要发送的数据,
参数size表示要发送数据的大小,
而参数flags则可以是以下一些值:SocketFlags.None、SocketFlags.DontRoute、SocketFlags.OutOfBnd。
该方法返回的是一个System.Int32类型的值,它表明了已发送数据的大小。同时,该方法还有以下几种已被重载了的函数实现:
public int Send (byte[] buffer);
public int Send (byte[] buffer, SocketFlags flags);
public int Send (byte[] buffer,int offset, int size, SocketFlags flags);
介绍完Send()方法,下面是Receive()方法,其函数原型如下:
 
public int Receive(byte[] buffer, int size, SocketFlags flags); 
其中的参数和Send()方法的参数类似,在这里就不再赘述。
同样,该方法还有以下一些已被重载了的函数实现:
public int Receive (byte[] buffer);
public int Receive (byte[] buffer, SocketFlags flags);
public int Receive (byte[] buffer,int offset, int size, SocketFlags flags);
在通讯完成后,我们就通过ShutDown()方法来禁用Socket,函数原型如下:
 
public void ShutDown(SocketShutdown how); 
其中参数how表明了禁用的类型,SoketShutdown.Send表明关闭用于发送的套接字;SoketShutdown.Receive表明关闭用于接收的套接字;而SoketShutdown.Both则表明发送和接收的套接字同时被关闭。
应该注意的是在调用Close()方法以前必须调用ShutDown()方法以确保在Socket关闭之前已发送或接收所有挂起的数据。一旦ShutDown()调用完毕,就调用Close()方法来关闭Socket,其函数原型如下:
 
public void Close(); 
该方法强制关闭一个Socket连接并释放所有托管资源和非托管资源。该方法在内部其实是调用了方法Dispose(),该函数是受保护类型的,其函数原型如下:
 
protected virtual void Dispose(bool disposing); 
其中,参数disposing为true或是false,如果为true,则同时释放托管资源和非托管资源;如果为false,则仅释放非托管资源。因为Close()方法调用Dispose()方法时的参数是true,所以它释放了所有托管资源和非托管资源。
如果以上操作中出现异常,我们可以使用SocketException来抛出Socket相关出错信息;当网络发生错误时,Socket 和Dns 引发SocketException;使用SocketException.ErrorCode 可以获取特定的错误代码, Message 可以获得错误消息。
主要代码如下:
 
try
{
//执行代码
}
catch(SocketException e)
{
 Console.WriteLine(“{0} error code : {1}” , e.Message , e.ErrorCode);
return (e.ErrorCode);
}
return 0; 
这样,一个Socket从创建到连接到通讯最后的关闭的过程就完成了。

【例11.6】下面看一个完整的实例,client(客户端)向server(服务器端)发送一段测试字符串,server接收并显示出来,给予client成功响应。

客户端主要代码:
 
static void Main()
        {
            try
            {
                int port = 2000;
                string host = "127.0.0.1";
                IPAddress ip = IPAddress.Parse(host);
                IPEndPoint ipe = new IPEndPoint(ip, port);
//把ip和端口转化为IPEndPoint实例
Socket c = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//创建一个Socket类
                Console.WriteLine("Conneting...");
                c.Connect(ipe);  //连接到服务器
                string sendStr = "hello!This is a socket test";
                byte[] bs = Encoding.ASCII.GetBytes(sendStr);
                Console.WriteLine("Send Message");
                c.Send(bs, bs.Length, 0);//发送测试信息
                string recvStr = "";
                byte[] recvBytes = new byte[1024];
                int bytes;
                bytes = c.Receive(recvBytes, recvBytes.Length, 0);
//从服务器端接受返回信息
                recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes);
                Console.WriteLine("Client Get Message:{0}", recvStr);
//显示服务器返回信息
                c.Close();
            }
            catch (ArgumentNullException e)
            {
                Console.WriteLine("ArgumentNullException: {0}", e);
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            Console.WriteLine("Press Enter to Exit");
            Console.ReadLine();
        }
 
服务器端主要代码:
 
static void Main()
        {
            try
            {
                int port = 2000;
                string host = "127.0.0.1";
                IPAddress ip = IPAddress.Parse(host);
                IPEndPoint ipe = new IPEndPoint(ip, port);
                Socket s = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//创建一个Socket类
                s.Bind(ipe);//绑定端口
                s.Listen(0);//开始监听
                Console.WriteLine("Wait for connect");
                Socket temp = s.Accept();//为新建连接创建新的Socket。
                Console.WriteLine("Get a connect");
                string recvStr = "";
                byte[] recvBytes = new byte[1024];
                int bytes;
                bytes = temp.Receive(recvBytes, recvBytes.Length, 0);//从客户端接受信息
                recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes);
                Console.WriteLine("Server Get Message:{0}", recvStr);
//把客户端传来的信息显示出来
                string sendStr = "Ok!Client Send Message Sucessful!";
                byte[] bs = Encoding.ASCII.GetBytes(sendStr);
                temp.Send(bs, bs.Length, 0);//返回客户端成功信息
                temp.Close();
                s.Close();
            }
            catch (ArgumentNullException e)
            {
                Console.WriteLine("ArgumentNullException: {0}", e);
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            Console.WriteLine("Press Enter to Exit");
            Console.ReadLine();
        }
 

11.4.4 Socket的同步编程和异步编程
上一节中的Accept,Receive,Send方法都有相应的BeginAccept,EndAccept等方法与之对应。这是因为Socket允许同步或异步的方式进行操作,对于Socket得每一个操作也就有同步和异步两个版本。
所谓同步方式,就是发送方发送数据包以后,不等待接受方相应,就这届发送下一个数据包;异步方式就是当发送方发送一个数据包以后,一直等到接收方相应后,才接着发送下一个数据包。
所谓同步Socket是指客户机或服务器上执行Socket连接,接收或发送操作时,客户机或服务器会中止工作,处于暂停状态。这种方式编程比较简单,对于简单的网络程序来说也确实够用,但是对于网络操作比较繁重的时候同步操作这种机制显然就不何时了。这是就需要使用异步通信的机制。
对于服务器端,在同步操作的过程中,当一个Socket处于Listen状态时,它可以使用Accept方法来接收远方的连接请求。而在连接请求到来之前,程序是一直处于中止状态的,当请求到来后,Accept方法将会返回一个与客户端连接的Socket。
同步Socket在监听时处于暂停状态,这样无疑减小了网络的负载能力,因为程序中有一个同步Socket在监听时,程序就处于暂停,无法再建立一个同步的监听 Socket或者进行其他操作。对于网络操作负荷比较大的程序来说,使用同步得Socket无法满足要求,这时就要使用异步的Socket操作了。异步的Socket可以在监听的同时进行其他的操作。
这里我们以服务器端的异步Socket为例,演示使用异步Socket在服务器端建立连接的操作,异步数据发送和接收这里不再累述。
对于服务器端的异步Socket来说,它需要在一个方法开始接受网络连接请求;一个回调函数处理连接请求建立连接。
Socket中使用BeginAccept方法接收新的连接请求。BeginAccept方法有两个参数,第一个是AsyncCallback类型的委托,第二个是object类型用来传递状态,一般用来传递Socket变量

posted @ 2022-12-03 09:16  星火燎猿*  阅读(95)  评论(0编辑  收藏  举报