进程、线程与网络协议(二)
C#网络应用高级编程学习
1.2 IP 地址与端口
IP(Internet Protocol)是 Internet 网络设备之间传输数据的一种协议。本节所讲的端口是逻辑意义上的端口,即 TCP/IP 协议中的端口。这一节我们主要学习 IPAddress、IPHostEntry、IPEndPoint 等 System.Net 命名空间中的几个类,为以后的应用打下基础。
1.2.1TCP/IP 网络协议
网络协议是网络上所有设备(网络服务器、计算机及交换机、路由器、防火墙等)之间通信规则的集合,它定义了通信时信息必须采用的格式以及这些格式的含义。网络协议使网络上各种设备能够相互交换信息。
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是一组
网络通信协议的总称,它规范了网络上的所有通信设备,尤其是一个主机与另一个主机之间的数据交换格式以及传送方式。对普通用户来说,并不需要了解网络协议的整个结构,仅需了解IP 的地址格式,即可与世界各地进行网络通信。
IP 地址就是给每个连接在因特网上的主机(或路由器)分配一个在全世界范围内惟一的标识符。一个 IP 地址主要由两部分组成:一部分是用于标识该地址所从属的网络号,另一部分用于指明该网络上某个特定主机的主机号。网络号由因特网权力机构分配,主机地址由各个网络的管理员统一分配。因此,网络地址的惟一性与网络内主机地址的惟一性确保了 IP 地址的全球惟一性。
目前,大多数 IP 编址方案仍采用 IPv4 编址方案,即使用 32 位的二进制地址进行识别,我们常见的形式是将 32 位的 IP 地址分成 4 个字节,然后把 4 个字节分别用十进制表示,中间用圆点分开,这种方法叫做点分十进制表示法。
使用 IP 地址的点分十进制表示法,网络类的范围划分如下:
A 类:0.x.x.x—127.x.x.x
(32 位二进制最高位为 0)
B 类:128.x.x.x—191.x.x.x
(32 位二进制最高 2 位为 10)
C 类:192.x.x.x—223.x.x.x
(32 位二进制最高 3 位为 110)
D 类:224.x.x.x—239.x.x.x
(32 位二进制最高 4 位为 1110)
E 类:240.x.x.x—255.x.x.x
(32 位二进制最高 5 位为 11110)
其中,D 类地址用于组播,E 类地址保留用于扩展或实验用。
在这些网络类中,每类网络又可以进一步分成不同的网络,或者叫子网。每个子网必须用一个公共的网址把它与该类网络中的其他子网分开。为了识别 IP 地址的网络部分,又为特定的子网定义了子网掩码。子网掩码用于区分哪些位作为网络地址部分,哪些位作为主机地址部分。把所有的网络位用 1 来标识,主机位用 0 来标识,就得到了子网掩码。
从表面上看,好像知道了服务器的 IP 地址,客户端就能够和服务器相互通信。其实,真正相互完成通信功能的不是两台计算机,而是两台计算机上的进程。IP 地址仅仅能够具体标识到某台主机,而不能标识某台计算机上的进程。如果要标识具体的进程,需要引入新的地址空间,这就是端口(Port)。
在网络技术中,端口大致有两种意思:一是物理意义上的端口,比如,ADSL Modem、集线器、交换机、路由器上连接其他网络设备的接口,如 RJ-45 端口、SC 端口等等。
二是逻辑意义上的端口,一般是指 TCP/IP 协议中的端口,端口号的范围从 0到 65535,比如用于浏览网页服务的 80 端口,用于 FTP 服务的 21 端口等等。我们这里介绍的是逻辑意义上的端口。定义端口是为了解决与多个应用程序同时进行通信的问题;它主要扩充了 IP 地址的概念。假设一台计算机正在同时运行多个应用程序,并通过网络接收到了一个数据包,这时就可以利用一个独有的端口号(该端口号在建立连接时确定)来标识目标进程。因此,如果客户端 A 要与服务器 B 相互通信,客户端 A 不仅要知道服务器 B 的 IP 地址,而且要知道服务器 B 提供具体服务的端口号。由于使用 16 位二进制表示端口地址,因此可用端口地址的范围是 0~65535。
1.2.2IPAddress 类与 Dns 类
在 System.Net 命名空间中,IPAddress 类提供了对 IP 地址的转换、处理等功能。该类提供的 Parse 方法可将 IP 地址字符串转换为 IPAddress 实例。例如:
IPAddress ip = IPAddress.Parse("192.168.1.1");
IPAddress 类还提供了 7 个只读字段,分别代表程序中使用的特殊 IP 地址:
Any 表示本地系统可用的任何 IPv4 地址
Broadcast 表示本地 IPv4 网络广播地址
IPv6Any Socket.Bind 方法用此字段指出本地系统可用的 IPv6 地址
IPv6Loopback 表示系统的 IPv6 回送地址
IPv6None 表示系统上没有可用的 IPv6 网络接口
Loopback 表示系统的 IPv4 回送地址
None 表示系统上没有可用的 IPv4 网络接口
System.Net 命名空间下,还有一个 Dns 类,该类提供了一系列静态的方法,用于获取提供本地或远程域名等功能。常用方法有:
1) GetHostAddresses 方法
获取指定主机的 IP 地址,返回一个 IPAddress 类型的数组。函数原形为:
public static IPAddress[] GetHostAddresses(string hostNameOrAddress);
例如:
IPAddress[] ip=Dns.GetHostAddresses("www.cctv.com");
listBox1.Items.AddRange(ip);
2) GetHostName 方法
获取本机主机名。例如:
string hostname = Dns.GetHostName();
1.2.3IPHostEntry 类
IPHostEntry 类的实例对象中包含了 Internet 主机的相关信息。常用属性有两个:一个是AddressList 属性,另一个是 HostName 属性。AddressList 属性的作用是获取或设置与主机关联的 IP 地址列表,是一个 IPAddress 类型的数组,包含了指定主机的所有 IP 地址;HostName 属性则包含了服务器的主机名。在 Dns 类中,有一个专门获取 IPHostEntry 对象的方法,通过 IPHostEntry 对象,可以获
取本地或远程主机的相关 IP 地址。例如:
listBox1.Items.Add("搜狐新闻所用的服务器 IP 地址有:");
IPAddress[] ip = Dns.GetHostEntry("news.sohu.com").AddressList;
listBox1.Items.AddRange(ip);
listBox1.Items.Add("本机 IP 地址为:");
ip = Dns.GetHostEntry(Dns.GetHostName()).AddressList;
listBox1.Items.AddRange(ip);
1.2.4IPEndPoint 类
要与远程主机进行通信,仅有 IP 地址是不够的。在 Internet 中,TCP/IP 使用一个网络地18址和一个服务端口号来唯一标识设备和服务。网络地址标识网络上的设备;端口号标识该设备上的特定服务。网络地址和服务端口的组合称为端点。在 C#中,使用 IPEndPoint 类表示这个端点,该类包含了应用程序连接到主机上的服务所需的 IP 地址和端口信息。IPEndPoint 类常用的构造函数为:
public IPEndPoint(IPAddress, int);
其中第一个参数指定 IP 地址,第二个参数指定端口号。
【例1-4】IPAddress类、Dns类、IPHostEntry类和IPEndPoint类的使用方法。
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 9 using System.Windows.Forms; 10 using System.Net; 11 namespace NetProgramDemo 12 { 13 public partial class Form4 : Form 14 { 15 public Form4() 16 { 17 InitializeComponent(); 18 } 19 20 private void button1_Click(object sender, EventArgs e) 21 { 22 listBox1.Items.Clear(); 23 string name = Dns.GetHostName(); 24 listBox1.Items.Add("本机主机名:" + name); 25 IPHostEntry me = Dns.GetHostEntry(name); 26 listBox1.Items.Add("本机所有 IP 地址:"); 27 foreach (IPAddress ip in me.AddressList) 28 { 29 listBox1.Items.Add(ip); 30 } 31 IPAddress localip = IPAddress.Parse("127.0.0.1"); 32 IPEndPoint iep = new IPEndPoint(localip, 80); 33 listBox1.Items.Add("The IPEndPoint is: " + iep.ToString()); 34 listBox1.Items.Add("The Address is: " + iep.Address); 35 listBox1.Items.Add("The AddressFamily is: " + iep.AddressFamily); 36 listBox1.Items.Add("The max port number is: " + IPEndPoint.MaxPort); 37 listBox1.Items.Add("The min port number is: " + IPEndPoint.MinPort); 38 } 39 40 private void button2_Click(object sender, EventArgs e) 41 { 42 listBox1.Items.Clear(); 43 IPHostEntry remoteHost = Dns.GetHostEntry("www.cctv.com"); 44 IPAddress[] remoteIP = remoteHost.AddressList; 45 listBox1.Items.Add("中央电视台:"); 46 foreach (IPAddress ip in remoteIP) 47 { 48 IPEndPoint iep = new IPEndPoint(ip, 80); 49 listBox1.Items.Add(iep); 50 } 51 } 52 } 53 }
效果图:
1.3 套接字
套接字是支持 TCP/IP 协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用 Internet 协议进行通信。
套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套接字有两种不同的类型:流套接字和数据报套接字。
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为 ServerSocket。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。客户端请求是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为
此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后再向服务器端套接字提出连接请求。连接确认是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的信息发给客户端,一旦客户端确认了此信息,连接即可建立。而服务器端套接字继续处于监听状态,继续接收其他客户端
套接字的连接请求。使用套接字处理数据有两种基本模式:同步套接字和异步套接字。
1. 同步套接字
同步套接字的特点是在通过 Socket 进行连接、接收、发送操作时,客户机或服务器在接收到对方响应前会处于阻塞状态,即一直等到接收到对方请求时才继续执行下面的语句。可见,同步套接字适用于数据处理不太多的场合。当程序执行的任务很多时,长时间的等待可能会让用户无法忍受。
2. 异步套接字
在通过 Socket 进行连接、接收、发送操作时,客户机或服务器不会处于阻塞方式,而是利用 callback 机制进行连接、接收和发送处理,这样就可以在调用发送或接收的方法后直接返回,并继续执行下面的程序。可见,异步套接字特别适用于进行大量数据处理的场合。
使用同步套接字进行编程相对比较简单,而异步套接字则比较复杂。
1.3.1Socket 类
Socket 类包含在 System.Net.Sockets 命名空间中。一个 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协议,而 ProtocolType.Udp 则表明连接协议是 UDP 协议。
Socket 构造函数的三个参数中,对于网络上的 IP 通信来说,AddressFamily 总是使用AddressFamily.InterNetwork 枚举值。而 SocketType 参数则与 ProtocolType 参数配合使用,不允许其他的匹配形式,也不允许混淆匹配。
熟悉了构造函数的参数含义,我们就可以创建套接字的实例:
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
套接字被创建后,我们就可以利用 Socket 类提供的一些属性方便的设置或检索信息。表
在实际应用中,还可以通过调用 Socket 对象的 SetSocketOption 方法设置套接字的各种选
项,它有四种重载的形式:
public void SetSocketOption(SocketOptionLevel ol, SocketOptionName on, boolean value)
public void SetSocketOption(SocketOptionLevel ol, SocketOptionName on, byte[] value)
public void SetSocketOption(SocketOptionLevel ol, SocketOptionName on, int value)
public void SetSocketOption(SocketOptionLevel ol, SocketOptionName on, object value)
其中,ol 定义套接字选项的类型,可选类型有:IP、IPv6、Socket、Tcp、Udp。
on 指定套接字选项的值,表 1-3 列出了套接字常用的选项值。21
Value 参数指定所使用的套接字选项名 SocketOptionName 的值。例如:
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 1000);
该语句设置套接字发送超时时间为 1000 毫秒。
1.3.2 面向连接的套接字
IP 连 接 领 域 有 两 种 通 信 类 型 : 面 向 连 接 的 ( connection-oriented ) 和 无 连 接 的(connectionless)。在面向连接的套接字中,使用 TCP 协议来建立两个 IP 地址端点之间的会
话。一旦建立了这种连接,就可以在设备之间可靠的传输数据。为了建立面向连接的套接字,服务器和客户端必须分别进行编程,
对于服务器端程序,建立的套接字必须绑定到用于 TCP 通信的本地 IP 地址和端口上。
Bind方法用于完成绑定工作:Bind(IPEndPoint address)Address 为 IPEndPoint 的实例,该实例包括一个本地 IP 地址和一个端口号。在套接字绑定到本地之后,就用 Listen 方法等待客户机发出的连接尝试:
Listen(int backlog)Backlog 参数指出系统等待用户程序服务排队的连接数,超过连接数的任何客户都不能与服务器进行通信。在 Listen 方法执行之后,服务器已经做好了接收任何引进连接的准备,这是用 Accept 方法来完成的,当有新客户进行连接时,该方法就返回一个新的套接字描述符。
下面是完成上述步骤的服务器端部分代码的例子:
IPHostEntry local = Dns.GetHostByName(Dns.GetHostName());
IPEndPoint iep = new IPEndPoint(local.AddressList[0], 80);
Socket localSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
localSocket.Bind(iep);
locatSocket.Listen(10);
Socket clientSocket = localSocket.Accept();
程序执行到 Accept 方法时会处于阻塞状态,直到有客户机请求连接,一旦有客户机连接到服务器,clientSocket 对象将包含该客户机的所有连接信息。而 localSocket 对象仍然绑定到原来的 IPEndPoint 对象,并可以通过增加循环语句继续用 Accept 方法接收新的客户端连接。如果没有继续调用 Accept 方法,服务器就不会再响应任何新的客户机连接。在接受客户机连接之后,客户机和服务器就可以开始传递数据了。表 1-4 和 1-5 分别列出
了可用的 Receive 方法和 Send 方法的格式以及 SocketFlag 的值。
对于客户端程序,客户机也必须把一个地址绑定到创建的 Socket 对象,不过它不使用 Bind方法,而是使用 Connect 方法:
IPAddress remoteHost = IPAddress.Parse("192.168.0.1");
PEndPoint iep = new IPEndPoint(remoteHost, 80);
Socket localSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
localSocket.Connect(iep);
程序运行后,客户端在与服务器建立连接之前,系统不会执行 Connect 方法下面的语句,而是处于阻塞方式。一旦客户端与服务器建立连接,客户机就可以像服务器收发数据使用的方法一样,使用 Send 和 Receive 方法进行通信。注意通信完成后,必须先用 Shutdown 方法停止会话,然后关闭 Socket 实例。
下面是关闭连接的典型用法:
sock.Shutdown(SocketShutdown.Both);
sock.Close();
该方法允许 Socket 对象一直等待,直到将内部缓冲区的数据发送完为止。
1.3.3 无连接的套接字
UDP 协议使用无连接的套接字,无连接的套接字不需要在网络设备之间发送连接信息。因此,很难确定谁是服务器谁是客户机。如果一个设备最初是在等待远程设备的信息,则套接字就必须用 Bind 方法绑定到一个本地地址/端口对上。完成绑定之后,该设备就可以利用套接字接收数据了。由于发送设备没有建立到接收设备地址的连接,所以收发数据均不需要 Connect
由于不存在固定的连接,所以可以直接使用 SendTo 方法和 ReceiveFrom 方法发送和接收数据,在两个设备之间的通信结束之后,可以像 TCP 中使用的方法一样,对套接字使用Shutdown 和 Close 方法。
再次提醒注意,需要接收数据时,必须使用 Bind 方法将套接字绑定到一个本地地址/端口对上之后才能使用 ReceiveFrom 方法接收数据,如果只发送而不接收,则不需要使用 Bind方法。
实际上,为了简化复杂的网络编程,.NET Framework 除了提供可以灵活控制的套接字类
以外,还在此基础上提供了对套接字封装后的基于不同协议的更易于使用的类。
1.4 网络流
流(stream)是对串行传输的数据的一种抽象表示,底层的设备可以是文件、外部设备、主存、网络套接字等等。
流有三种基本的操作:写入、读取和查找。
如果数据从内存缓冲区传输到外部源,这样的流叫作“写入流”。
如果数据从外部源传输到内存缓冲区,这样的流叫作“读取流”。
在网络上传输数据时,使用的是网络流(NetworkStream)。网络流的意思是数据在网络的各个位置之间是以连续的形式传输的。为了处理这种流,C#在 System.Net.Sockets 命名空间中提供了一个专门的 NetworkStream 类,用于通过网络套接字发送和接收数据。NetworkStream 类支持对网络数据的同步或异步访问,它可以被视为在数据来源端和接收端之间架设了一个数据通道,这样我们读取和写入数据就可以针对这个通道来进行。对于 NetworkStream 流,写入操作是指从来源端内存缓冲区到网络上的数据传输;读取操作是从网络上到接收端内存缓冲区(如字节数组)的数据传输。
构造 NetworkStream 对象的常用形式为:
Socket socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
NetWorkStream networkStream=new NetworkStream(socket);
一旦构造了一个 NetworkStream 对象,就不需要使用 Socket 对象了。也就是说,在关闭25
网 络连接之前就一直使用 NetworkStream 对 象发送和接收网络数据。
在这个表中,比较常用的一个属性就是 DataAvailable,通过这个属性,可以迅速查看在缓冲区中是否有数据等待读出。
注意:网络流没有当前位置的概念,因此不支持查找和对数据流的随机访问,相应属 性CanSeek 始 终 返 回 false , 而 读 取 Position 属 性 和 调 用 Seek 方 法 时 , 都 将 引 发
NotSupportedException 异常。
网络数据传输完成后,不要忘记用 Close 方法关闭 NetworkStream 对象。