
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 编码与解码程序运行结果