C#网络程序开发(第二版)
上QQ阅读APP看书,第一时间看更新

2.4 C#套接字与网络流

2.4.1 Socket类

套接字是支持TCP/IP网络通信的基本操作单元。在一个套接字既保存了本机的IP地址和端口,也保存了对方主机的IP地址和端口,同时还有双方通信的协议信息。C#的命名空间System.Net.Sockets提供了Socket类。一个Socket实例包含一个本地或者一个远程的套接字信息。

Socket可以像流(Stream)一样被视为数据通道,这个通道存在于服务器和客户端之间。数据的发送和接收均通过这个通道进行。所以在应用程序创建Socket对象后,就可以用Send/SendTo方法将数据发送到连接的Socket中,或者使用Receive/ReceiveFrom方法接收连接的Socket数据。图2-11显示了客户机(Client)和服务器(Server)进行通信的一般过程。

图2-11 Socket通信模型

Socket类为网络通信程序提供了丰富的方法和属性。System.Net.Sockets命名空间中常用的TcpClient类、TcpListener类和UdpClient类都是以该类为基础的。

2.4.2 套接字的类型与使用方法

1. Socket类的类型

套接字有3种不同类型:流套接字、数据报套接字和原始套接字。

(1)流套接字用来实现TCP通信,提供了面向连接的、可靠的、数据无错且无重复的数据传输服务,并且发送和接收的数据的顺序是相同的。

(2)数据报套接字用来实现UDP通信,提供了面向无连接的服务,它以独立的数据报形式发送数据(数据包的长度不能大于32KB),不提供正确性检查,也不保证各数据包的发送和接收顺序,所以可能会出现数据重发、丢失等情况。

(3)原始套接字用来实现IP数据包通信,用于直接访问协议的较低层,常用于侦听及分析数据包,广泛应用于高级网络编程,也是一种经常使用的黑客手段。

这3种类型的套接字均可以使用System.Net.Sockets命名空间中的Socket类来实现。Socket的构造函数为:

     public Socket(AddressFamily addressFamily,SocketType socketType,ProtocolType protocolType);

各参数的含义如下。

①addressFamily:指网络类型,使用AddressFamily枚举指定Socket使用的寻址方案,常见的有AddressFamily.InterNetwork(表示IPv4的地址)和AddressFamily.InterNetmorkV6(表示IPv6的地址)。

②socketType和protocolType:这两个枚举类型的参数必须对应,共同指明Socket使用哪种协议的哪种套接字。表2-5列出这两个参数的组合。

表2-5 套接字类型与协议对应关系

了解了构造函数的参数含义后,就可以创建套接字实例了,例如:

     Socket socket=new Socket(AddressFamily.InterNetwork,SocketType.stream,ProtocolType.Tcp)

表示创建基于TCP协议的IPv4流套接字。

2. Socket类的常用属性

表2-6列出套接字的一些常用的属性。

表2-6 套接字的常用属性

3. Socket类的常用方法

1)void Connect(IPEndPoint remoteIcp)

该方法客户机独有,通过远程设备的套接字建立与远程设备的连接。

2)int Send()/int Receive()

这两个方法在完成客户端的连接后,将数据发送到连接到的Socket上以及将数据从连接的Socket接收到缓冲区的指定位置。当Receive方法没有可读的数据时,将一直处于阻止状态。

3)void Bind(IPEndPoint localIcp)

该方法对应服务器程序而言,使用Socket与本地IP地址和端口号关联。

4)void Listen(int backlog)

该方法用于等待客户端发出连接请求,其中的backlog为用户的最大连接数,超过该参数值的其他客户不能与服务器进一步通信。

5)Socket Accept()

该方法创建新的Socket以处理连接请求。当程序执行到该方法时会处于阻塞状态,直到有新的客户机请求连接。该方法返回包含客户端信息的套接字句柄。

6)void ShutDown()

该方法在通信完成后负责将连接释放,并关闭socket对象。表2-7列出了ShutDown方法可以使用的值。

表2-7 Socket.ShutDown值

7)void Close()

该方法关闭远程主机连接,并释放所有与Socket关联的资源。关闭后,Connected属性将设置为false。对于面向连接的协议,先调用Shutdown方法,再调用Close方法,以确保在已连接的套接字关闭之前,已发送和接收该套接字上的所有数据。

4. 面向连接的套接字

面向连接的套接字使用TCP建立两个IP地址端点间的通信。根据连接启动的方式及本地Socket要连接的目标,套接字间的连接包括服务器监听、客户端请求、连接确认3个步骤。建立连接后的套接字双方可以进行数据传输。其编程步骤如图2-12所示。

图2-12 面向连接的套接字编程流程

例2-3】 编写控制台程序,利用同步的面向连接Socket实现客户端和服务器的消息通信。

(1)编写服务器端程序,Program类中代码如下:

(2)编写客户端程序,Program类中代码如下:

5. 无连接的套接字

无连接的套接字使用UDP协议,不需要像面向连接的套接字那样发送连接信息,即没有使用Connect方法进行连接的步骤,发送进程直接使用SendTo方法进行数据发送;但是如果一个进程是等待远程设备的信息,则套接字必须用Bind方法绑定到一个本地“IP地址/端口”上,完成绑定后才能使用ReceiveFrom方法接收数据。其编程步骤如图2-13所示。

图2-13 无连接的套接字编程流程

例2-4】 编写控制台程序,利用无连接Socket实现接收方和发送方的消息通信。

(1)编写接收方程序,Program类中代码如下:

(2)编写发送方程序,Program类中代码如下:

2.4.3 网络流

当通过网络传输数据,或对文件数据进行操作时,需要将数据转化为数据流的形式。数据流(stream)是对串行传输的数据(以字节为单位)的一种抽象表示,数据源可以是文件、外部设备、主存、网络套接字等。数据流分为文件流、内存流和网络流。网络流用于在网络上传输数据。使用网络流时,数据在网络的各个位置之间以连续的字节形式传输。为了处理这种网络流,C#在System.Net.Sockets命名空间中提供了NetworkStream类用于收发网络数据。

NetworkStream类相当于在网络数据的源端和目的端之间架起了一个数据桥梁,使得读取和写入数据只针对这个通道进行。但NetworkStream类只支持面向连接的套接字。

对于NetworkStream流,写入操作是从源端内存缓冲区到网络上的数据传输,读取操作是从网络上到目的端内存缓冲区的数据传输,如图2-14所示。

图2-14 NetworkStream流的数据传输

表2-8列出了NetworkStream类的常用属性和方法。

表2-8 NetworkStream类的常用属性和方法

下面介绍如何使用NetworkStream收发网络数据。

1. 获取NetworkStream实例

在构造一个NetworkStream实例后,就可以用它来收发网络数据。

(1)利用TcpClient获取网络流对象。例如:

     TcpClient tcpClient=new TcpClient();
     tcpClient.Connect("www.cqut.edu.cn",5188);
     NetworkStream myNteworkStream=tcpClient.GetStream();

(2)利用Socket获取网络流对象。例如:

     NetworkStream myNetworkStream=new NetworkStream(mySocket);//mySocket为获取的Socket对象

2. 利用NetworkStream实例收发数据

图2-15显示了利用网络流收发数据的流程。其中,Write方法负责将字节数组从进程缓冲区发送到本机的TCP发送缓冲区,然后TCP/IP协议栈再通过网络适配器将数据真正发送到网络上,最终到达接收方的TCP接收缓冲区。

图2-15 NetworkStream流收发数据的流程

由于Write方法为同步方法,所以在发送成功或者返回异常前都将处于阻塞状态,直到发送成功或者返回异常。

下面的代码给出使用NetworkStream发送数据的一个示例。

     if(myNetworkStream.Canwrite)
     {
         byte[]myWriteBuffer=Encoding.ASCII.GetBytes("Are you receiving this message?");
         myNetworkStream.Write(myWriteBuffer,0,myWriteBuffer.Length);
     }
     else
         Console.WriteLine("Sorry.You cannot write to this NetworkStream.");

接收方通过调用Read方法将数据从接收缓冲区读入到进程缓冲区,完成读取操作。

下面的代码给出使用NetworkStream读取数据的一个示例。

     if(myNetworkStream.CanRead)
     {
         byte[]myReadBuffer=new byte[1024];
         String myCompleteMessage="";
          int numberOfBytesRead=0;
         //准备接收的信息有可能大于1024,所以用循环
         do{
           numberOfBytesRead=myNetworkStream.Read(myReadBuffer,0,myReadBuffer.Length);
           myCompleteMessage=String.Concat(myCompleteMessage,Encoding.ASCII.GetString(myReadBuffer,
     0,numberOfBytesRead));
          }while(myNetworkStream.DataAvailable);
     }

使用NetworkStream实例时,需要注意以下几点:

(1)通过DataAvailable属性,可以查看在缓冲区中是否有数据等待读出。

(2)网络流没有当前位置的概念,因此它不支持对数据流的查找和随机访问。

(3)网络数据传输完成后,必须用Close方法关闭NetworkStream实例。

2.4.4 网络数据编码与解码

在网络通信中,很多时候通信双方传达的是字符信息。但是字符信息不能直接在网络中传递,而是需要转换成一个字节序列后才能在网络中传输。将字符序列转换为字节序列的过程称为编码;反之即为解码。

1. 常见字符编码方式

常见的字符编码方式有以下3种:

1)ASCII字符集

ASCII字符集是美国信息交换标准委员会(American Standards Committee for Information Interchange)的缩写,在20世纪80年代由美国英语通信所设计。每个ASCII码由7位构成,整个ASCII字符集由128个字符组成,包括大小写字母、数字0~9、标点符号、非打印字符(换行符、制表符等4个)以及控制字符(退格、响铃等)。

2)非ASCII字符集

由于ASCII字符针对英语设计,当处理汉字等其他字符时,这种编码就不适用了。为解决这个问题,不同国家制订了自己的编码标准。我国一般使用国标码,常用的有GB 2312和GB 18030—2000编码,其中,GB 18030编码汉字更多,是我国计算机系统必须遵循的基础性编码标准之一。

在GB 2312编码中,汉字都采用双字节编码。为了与系统中基本的ASCII字符集区分开,所有汉字编码的每个字节的第一位都是1。例如,“啊”字的编码为0xB0A1。GB 18030是对GB 2312的扩展,其编码长度由2个字节变为1~4个字节。

3)Unicode字符集

由于每个国家都有自己的编码方式,要想打开一个文本文件,就必须知道其编码方式,否则就会出现乱码。为了让国际信息交流更加方便,国际组织制定了Unicode字符集。它为各种语言中的每一个字符规定了统一且唯一的字符,并且只需要两个字节,便可以表示地球上绝大部分地区的文字。

C#的默认字符都是Unicode码,一个英文字母和一个汉字一样,都占两个字节。Unicode码虽然能够表示大部分国家的文字,但是其占有空间比ASCII码大一倍,这对于能用ASCII码表示的字符显得有些浪费。因此,又出现了一些中间格式的字符集,它们被称为通用转换格式,即UTF(Universal Transformation Format)。目前比较流行的是UTF-8、UTF-16、UTF-32。

UTF-8是Internet上使用最广泛的一种UTF格式。它是Unicode的一种变长字符编码,一般用1~4个字节编码一个Unicode字符,即将一个Unicode字符编为1~4个字节组成的UTF-8格式,根据不同的符号变化字节长度。UTF-8是与字节顺序无关的,它的字节顺序在所有系统中都是一样的,故此种编码可以使排序变得容易。

UTF-16将每个码位表示为一个由1~2个16位整数组成的序列。

UTF-32将每个码位表示为一个32位整数。

2. C#中的编码与解码类

1)Encoding类

Encoding类位于System.Text命名空间中,主要用于在不同的编码和Unicode之间进行转换。表2-9中列出了Encoding类常见的属性和方法。

表2-9 Encoding类常见的属性和方法

利用Encoding类的Convert方法可将字节数组从一种编码转换为另一种编码。方法原型为:

     Public static byte[]Convert(Encoding srcEncoding,Encoding dstEncoding,byte[]bytes)

各参数含义如下。

srcEncoding:表示源编码格式。

dstEncoding:表示目标编码格式。

Bytes:待转换的字节数组。

返回值为包含转换结果的Byte类型的数组。

将Unicode字符串转换为UTF8字符串时,可以参考以下步骤。

(1)利用Encoding的UTF8和Unicode属性获取UTF8格式的编码实例utf8和Unicode编码实例unicode,例如:

     string unicodeString="unicode字符串pi(\u03a0)";
     Encoding Unicode=Encoding.Unicode;
     Encoding utf8=Ecoding.UTF8;

(2)利用unicode实例的GetBytes方法将Unicode字符编码为Unicode字节数组:

     byte[]unicodeBytes=unicode.GetBytes(unicodeString);

(3)利用Encoding的Convert方法将Unicode字节数组转换为UTF8字节数组:

     byte[]utf8Bytes=Encoding.Convet(Encoding.Unicode,Encoding.UTF8,unicodeBytes);

(4)最后利用实例utf8的GetString方法将UTF8字节数组解码为UTF8字符串:

     string utf8String=utf8.GetString(utf8Bytes);

2)Encoder类和Decoder类

在网络传输和文件操作中,如果数据量比较大,需要将其划分为较小的块。对于跨块传输的情况,直接使用Encoding类的GetBytes方法编写程序比较麻烦,而Encoder和Decoder由于维护了数据块结尾信息,则可以轻松地实现跨块字符序列的正确编码和解码,因此它们在网络传输和文件操作中很有用。

Encoder和Decoder类位于System.Text命名空间下,Encoder可以将一组字符串转换为一个字节序列,而Decoder则将已编码的字节序列解码为字符序列。Encoder编码的步骤为:

(1)获取Encoder实例。利用它对字符编码首先要获取Encoder类的实例,由于Encoder的构造函数为protected,不能直接创建该类的实例,必须通过Encoding提供的GetEncoder方法创建实例,例如:

     //获取ASCII编码的Encoder实例
     Encoder ASCiiEncoder=Encoding.ASCII.GetEncoder();
     //获取Unicode编码的Encoder实例
     Encoder unicodeEncoder=Encoding.Unicode.GetEncoder();

(2)GetBytes方法。获取Encoder实例后,利用它的GetBytes方法将一组字符编码转换为字节序列。

方法原型:

该方法将编码后的字节数组存储在参数bytes中,返回结果为写入bytes的实际字节数。如果设置flush为false,则编码器会将数据块末尾的尾部字节存储在内部缓冲区中,为下次编码操作中使用这些字节做准备。

(3)GetByteCount方法。该方法计算对字符序列进行编码后所产生的精确字节数,以确定GetBytes方法中byte类型数组实例的长度。

方法原型:

Decoder类解码的步骤为:首先通过Encoding的GetDecoder方法创建Decoder实例,然后用实例的GetChars方法将字节序列解码为一组字符。

GetChars方法用于将一个字节序列解码为一组字符,并从指定的索引位置开始存储这组字符。

方法原型:

该方法返回chars写入的实际字符数。

例2-5】 利用Encoder和Decoder类实现编码和解码。

程序运行结果如图2-16所示。

图2-16 编码与解码程序运行结果