黑马程序员——Java基础---网络编程

 ------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

Java基础---网络编程

一、网络编程概述

1、计算机网络

      是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

2、网络编程

      就是用来实现网络互连的不同计算机上运行的程序间可以进行数据交换。

3、网络模型

      计算机网络之间以何种规则进行通信,就是网络模型研究问题。
      网络模型一般是指OSI(Open System Interconnection开放系统互连)参考模型或者TCP/IP参考模型。
      应用层:HTTP,传输层:TCP、UDP,网络层:IP,物理层,数据链路层
     网络模型7层概述:
      (1)   物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。 
      (2)   数据链路层:主要将从物理层接收的数据进行MAC地址(网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。 
      (3)   网络层:主要将从下层接收到的数据进行IP地址(例192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。 
      (4)   传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。 
      (5)   会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名) 
      (6)   表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。 
      (7)   应用层: 主要是一些终端的应用,比如说FTP(各种文件下载),WEB(IE浏览),QQ之类的(可以把它理解成我们在电脑屏幕上可以看到的东西.就是终端应用)。
PS:
    1、每个网卡的MAC地址都是全球唯一的。
    2、路由器实现将数据包发送到指定的地点。
    3、应用软件之间通信的过程就是层与层之间封包、解封包的过程。
    4、OSI参考模型虽然设计精细,但过于麻烦,效率不高,因此才产生了简化版的TCP/IP参考模型。
封包、解封包的过程:

二、网络编程三要素

      网络模型说完了,我们要进行通讯,需要哪些要素呢?
      比如说:我要跟你说话.
      第一个条件:我要先找到你 (IP)
      第二个条件:你得有接收数据的地方  耳朵 (端口)
      第三个条件:我跟你说话,你能接收到,咱按什么方式接收啊,我说英文你懂吗,说韩文你懂吗,不懂是吧,所以我还是说中文把.(协议)

1、IP地址:InetAddress

      网络中计算机的唯一标识,不易记忆,可用主机名。计算机只能识别二进制的数据,所以我们的IP地址应该是一个二进制的数据。为了方便表示IP地址,我们就把IP地址的每一个字节上的数据换算成十进制,然后用.分开来表示:"点分十进制"。
   (1)   所谓IP地址就是给每个连接在Internet上的主机分配的一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,比特换算成字节,就是4个字节。例如一个采用二进制形式的IP地址是“00001010000000000000000000000001”,这么长的地址,人们处理起来也太费劲了。为了方便人们的使用,IP地址经常被写成十进制的形式,中间使用符号“.”分开不同的字节。于是,上面的IP地址可以表示为“10.0.0.1”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多。
     (2)   IP地址的组成:IP地址 = 网络号码+主机地址
     (3)   IPV4数量已经不够分配,所以产生了IPV6。
     (4)   IP地址分类
     (5)   InetAddress类的使用
            此类表示互联网协议 (IP) 地址。
            ①   public static InetAddress getByName(String host):根据主机名或者IP地址的字符串表示得到IP地址对象
            ②   String  getHostName():获取此 IP 地址的主机名。
            ③   String  getHostAddress():返回 IP 地址字符串
代码示例:
 1 package cn.itcast_01;
 2 
 3 import java.net.InetAddress;
 4 import java.net.UnknownHostException;
 5 
 6 public class InetAddressDemo {
 7     public static void main(String[] args) throws UnknownHostException {
 8         // public static InetAddress getByName(String host)
 9         // InetAddress address = InetAddress.getByName("liuyi");
10         // InetAddress address = InetAddress.getByName("192.168.12.92");
11         InetAddress address = InetAddress.getByName("192.168.12.63");
12 
13         // 获取两个东西:主机名,IP地址
14         // public String getHostName()
15         String name = address.getHostName();
16         // public String getHostAddress()
17         String ip = address.getHostAddress();
18         System.out.println(name + "---" + ip);
19     }
20 }
运行结果:

2、端口号

     正在运行的程序的标识,用于标识进程的逻辑地址,不同进程的标识。有效端口:0~65535,其中0~1024系统使用或保留端口。
     端口分为:物理端口,网卡口;逻辑端口,我们指的就是逻辑端口。
     A:每个网络程序都会至少有一个逻辑端口
     B:用于标识进程的逻辑地址,不同进程的标识
     C:有效端口:0~65535,其中0~1024系统使用或保留端口。
     D:所谓防火墙,其功能就是将发送到某程序端口的数据屏蔽掉以及将从该程序端口发出的数据也屏蔽掉。

3、传输协议

      传输协议就是通讯的规则,常见协议:TCP,UDP。
(1)   UDP将数据源和目的封装成数据包中,不需要建立连接;每个数据报的大小在限制在64k;因无连接,是不可靠协议;不需要建立连接,速度快
(2)   TCP建立连接,形成传输数据的通道;在连接中进行大数据量传输;通过三次握手完成连接,是可靠协议;必须建立连接,效率会稍低
(3)   UDP和TCP的特点
       ①  UDP:面向无连接;不可靠;速度快;将数据封包传输,数据包最大64k。
            举例:聊天留言,在线视频,视频会议,发短信,邮局包裹。
       ②  TCP:面向连接;安全可靠效率稍低;通过三次握手确保连接的建立。
            举例:下载,打电话,QQ聊天(你在线吗,在线,就回应下,就开始聊天了)

4、域名解析

1、在浏览器中输入新浪的域名,DNS解析域名成IP,然后计算机再通过获取到的IP访问新浪服务器。
2、域名解析,最先走是本地的hosts(C:WINDOWSsystem32driversetchosts)文件,解析失败了,才去访问DNS服务器解析、获取IP地址。

三、Socket套接字

1、Socket套接字:

      网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。

2、Socket原理机制:

     (1)   通信的两端都有Socket。
     (2)   网络通信其实就是Socket间的通信。
     (3)   数据在两个Socket间通过IO传输。

3、Socket机制图解

四、UDP编程

1、UDP传输

     (1)  DatagramSocket与DatagramPacket
     (2)  建立发送端,接收端。
     (3)  建立数据包。
     (4)  调用Socket的发送接收方法。
     (5)  关闭Socket。
     (6)  发送端与接收端是两个独立的运行程序。

2、DatagramSocket

     (1)  此类表示用来发送和接收数据报包的套接字。 
     (2)  数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。 
     (3)  在 DatagramSocket 上总是启用 UDP 广播发送。为了接收广播包,应该将 DatagramSocket 绑定到通配符地址。在某些实现中,将 DatagramSocket 绑定到一个更加具体的地址时广播包也可以被接收。
     (4)   构造方法:
            ①  DatagramSocket(int port):创建数据报套接字并将其绑定到本地主机上的指定端口。 
            ②  DatagramSocket(int port, InetAddress laddr):创建数据报套接字,将其绑定到指定的本地地址。 
     (5)  UDP传输-发送端思路
           ①  建立udp的socket服务
           ②  将要发送的数据封装成数据包
           ③  通过udp的socket服务,将数据包发送出
           ④  关闭资源
 1 package cn.itcast_02;
 2 
 3 import java.io.IOException;
 4 import java.net.DatagramPacket;
 5 import java.net.DatagramSocket;
 6 import java.net.InetAddress;
 7 /*
 8  * UDP协议发送数据:
 9  * A:创建发送端Socket对象
10  * B:创建数据,并把数据打包
11  * C:调用Socket对象的发送方法发送数据包
12  * D:释放资源
13  */
14 public class SendDemo {
15     public static void main(String[] args) throws IOException {
16         // 创建发送端Socket对象
17         // DatagramSocket()
18         DatagramSocket ds = new DatagramSocket();
19 
20         // 创建数据,并把数据打包
21         // DatagramPacket(byte[] buf, int length, InetAddress address, int port)
22         // 创建数据
23         byte[] bys = "hello,udp,我来了".getBytes();
24         // 长度
25         int length = bys.length;
26         // IP地址对象
27         InetAddress address = InetAddress.getByName("192.168.12.92");
28         // 端口
29         int port = 10086;
30         DatagramPacket dp = new DatagramPacket(bys, length, address, port);
31 
32         // 调用Socket对象的发送方法发送数据包
33         // public void send(DatagramPacket p)
34         ds.send(dp);
35 
36         // 释放资源
37         ds.close();
38     }
39 }

3、DatagramPacket

(1)   此类表示数据报包。 
(2)   数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。 
(3)   构造方法
       ①  DatagramPacket(byte[] buf, int length) 
            构造 DatagramPacket,用来接收长度为 length 的数据包。
       ②  DatagramPacket(byte[] buf, int length, InetAddress address, int port) 
            构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
       ③  DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 
            构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。
(4)   UDP传输-接收端思路
       ①  建立udp的socket服务.
       ②  通过receive方法接收数据
       ③  将收到的数据存储到数据包对象中
       ④  通过数据包对象的功能来完成对接收到数据进行解析.
       ⑤  可以对资源进行关闭
 1 package cn.itcast_02;
 2 
 3 import java.io.IOException;
 4 import java.net.DatagramPacket;
 5 import java.net.DatagramSocket;
 6 import java.net.InetAddress;
 7 
 8 /*
 9  * UDP协议接收数据:
10  * A:创建接收端Socket对象
11  * B:创建一个数据包(接收容器)
12  * C:调用Socket对象的接收方法接收数据
13  * D:解析数据包,并显示在控制台
14  * E:释放资源
15  */
16 public class ReceiveDemo {
17     public static void main(String[] args) throws IOException {
18         // 创建接收端Socket对象
19         // DatagramSocket(int port)
20         DatagramSocket ds = new DatagramSocket(10086);
21 
22         // 创建一个数据包(接收容器)
23         // DatagramPacket(byte[] buf, int length)
24         byte[] bys = new byte[1024];
25         int length = bys.length;
26         DatagramPacket dp = new DatagramPacket(bys, length);
27 
28         // 调用Socket对象的接收方法接收数据
29         // public void receive(DatagramPacket p)
30         ds.receive(dp); // 阻塞式
31 
32         // 解析数据包,并显示在控制台
33         // 获取对方的ip
34         // public InetAddress getAddress()
35         InetAddress address = dp.getAddress();
36         String ip = address.getHostAddress();
37         // public byte[] getData():获取数据缓冲区
38         // public int getLength():获取数据的实际长度
39         byte[] bys2 = dp.getData();
40         int len = dp.getLength();
41         String s = new String(bys2, 0, len);
42         System.out.println(ip + "传递的数据是:" + s);
43 
44         // 释放资源
45         ds.close();
46     }
47 }
运行结果:

4、UDP案例

从键盘录入数据进行发送,如果输入的是886那么客户端就结束输入数据。
(1)发送端
package cn.itcast_04;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/*
 * 数据来自于键盘录入
 * 键盘录入数据要自己控制录入结束。
 */
public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端的Socket对象
        DatagramSocket ds = new DatagramSocket();

        // 封装键盘录入数据
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        while ((line = br.readLine()) != null) {
            if ("886".equals(line)) {
                break;
            }

            // 创建数据并打包
            byte[] bys = line.getBytes();
            // DatagramPacket dp = new DatagramPacket(bys, bys.length,
            // InetAddress.getByName("192.168.12.92"), 12345);
            DatagramPacket dp = new DatagramPacket(bys, bys.length,
                    InetAddress.getByName("192.168.12.255"), 12345);

            // 发送数据
            ds.send(dp);
        }

        // 释放资源
        ds.close();
    }
}
运行结果:
(2)接收端
 1 package cn.itcast_04;
 2 
 3 import java.io.IOException;
 4 import java.net.DatagramPacket;
 5 import java.net.DatagramSocket;
 6 
 7 /*
 8  * 多次启动接收端:
 9  *         java.net.BindException: Address already in use: Cannot bind
10  *         端口被占用。
11  */
12 public class ReceiveDemo {
13     public static void main(String[] args) throws IOException {
14         // 创建接收端的Socket对象
15         DatagramSocket ds = new DatagramSocket(12345);
16 
17         while (true) {
18             // 创建一个包裹
19             byte[] bys = new byte[1024];
20             DatagramPacket dp = new DatagramPacket(bys, bys.length);
21 
22             // 接收数据
23             ds.receive(dp);
24 
25             // 解析数据
26             String ip = dp.getAddress().getHostAddress();
27             String s = new String(dp.getData(), 0, dp.getLength());
28             System.out.println("from " + ip + " data is : " + s);
29         }
30 
31         // 释放资源
32         // 接收端应该一直开着等待接收数据,是不需要关闭
33         // ds.close();
34     }
35 }
运行结果:

五、TCP编程

1、TCP传输

(1)   Socket和ServerSocket
(2)   建立客户端和服务器端
(3)   建立连接后,通过Socket中的IO流进行数据的传输
(4)   关闭socket
(5)   同样,客户端与服务器端是两个独立的应用程序。

2、Socket

     此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
(1)   构造方法
       Socket(String host, int port) :创建一个流套接字并将其连接到指定主机上的指定端口号。
       Socket(InetAddress address, int port) :创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
(2)  TCP传输-客户端思路
      ①  建立客户端的Socket服务,并明确要连接的服务器。
      ②  如果连接建立成功,就表明,已经建立了数据传输的通道.就可以在该通道通过IO进行数据的读取和写入.该通道称为Socket流,Socket流中既有读取流,也有写入流.
      ③  通过Socket对象的方法,可以获取这两个流
      ④  通过流的对象可以对数据进行传输
      ⑤  如果传输数据完毕,关闭资源
 1 package cn.itcast_06;
 2 
 3 import java.io.IOException;
 4 import java.io.OutputStream;
 5 import java.net.Socket;
 6 
 7 /*
 8  * TCP协议发送数据:
 9  * A:创建发送端的Socket对象
10  *         这一步如果成功,就说明连接已经建立成功了。
11  * B:获取输出流,写数据
12  * C:释放资源
13  * 
14  * 连接被拒绝。TCP协议一定要先看服务器。
15  * java.net.ConnectException: Connection refused: connect
16  */
17 public class ClientDemo {
18     public static void main(String[] args) throws IOException {
19         // 创建发送端的Socket对象
20         // Socket(InetAddress address, int port)
21         // Socket(String host, int port)
22         // Socket s = new Socket(InetAddress.getByName("192.168.12.92"), 8888);
23         Socket s = new Socket("192.168.12.92", 8888);
24 
25         // 获取输出流,写数据
26         // public OutputStream getOutputStream()
27         OutputStream os = s.getOutputStream();
28         os.write("hello,tcp,我来了".getBytes());
29 
30         // 释放资源
31         s.close();
32     }
33 }

3、ServerSocket

     此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
(1)   构造方法
       ServerSocket(int port) :创建绑定到特定端口的服务器套接字。
(2)   TCP传输-服务器端思路
       ①   建立服务器端的socket服务,需要一个端口
       ②   服务端没有直接流的操作,而是通过accept方法获取客户端对象,在通过获取到的客户端对象的流和客户端进行通信
       ③   通过客户端的获取流对象的方法,读取数据或者写入数据
       ④   如果服务完成,需要关闭客户端,然后关闭服务器,但是,一般会关闭客户端,不会关闭服务器,因为服务端是一直提供服务的
 1 package cn.itcast_06;
 2 
 3 import java.io.IOException;
 4 import java.io.InputStream;
 5 import java.net.ServerSocket;
 6 import java.net.Socket;
 7 
 8 /*
 9  * TCP协议接收数据:
10  * A:创建接收端的Socket对象
11  * B:监听客户端连接。返回一个对应的Socket对象
12  * C:获取输入流,读取数据显示在控制台
13  * D:释放资源
14  */
15 public class ServerDemo {
16     public static void main(String[] args) throws IOException {
17         // 创建接收端的Socket对象
18         // ServerSocket(int port)
19         ServerSocket ss = new ServerSocket(8888);
20 
21         // 监听客户端连接。返回一个对应的Socket对象
22         // public Socket accept()
23         Socket s = ss.accept(); // 侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。
24 
25         // 获取输入流,读取数据显示在控制台
26         InputStream is = s.getInputStream();
27 
28         byte[] bys = new byte[1024];
29         int len = is.read(bys); // 阻塞式方法
30         String str = new String(bys, 0, len);
31 
32         String ip = s.getInetAddress().getHostAddress();
33 
34         System.out.println(ip + "---" + str);
35 
36         // 释放资源
37         s.close();
38         // ss.close(); //这个不应该关闭
39     }
40 }

4、TCP传输案例

(1)   客户端键盘录入,服务器输出到控制台
客户端:
 1 package cn.itcast_08;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.BufferedWriter;
 5 import java.io.IOException;
 6 import java.io.InputStreamReader;
 7 import java.io.OutputStreamWriter;
 8 import java.net.Socket;
 9 
10 /*
11  * 客户端键盘录入,服务器输出到控制台
12  */
13 public class ClientDemo {
14     public static void main(String[] args) throws IOException {
15         // 创建客户端Socket对象
16         Socket s = new Socket("192.168.12.92", 22222);
17 
18         // 键盘录入数据
19         BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
20         // 把通道内的流给包装一下
21         BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
22                 s.getOutputStream()));
23 
24         String line = null;
25         while ((line = br.readLine()) != null) {
26             // 键盘录入数据要自定义结束标记
27             if ("886".equals(line)) {
28                 break;
29             }
30             bw.write(line);
31             bw.newLine();
32             bw.flush();
33         }
34 
35         // 释放资源
36         // bw.close();
37         // br.close();
38         s.close();
39     }
40 }
运行结果:
服务器端:
 1 package cn.itcast_08;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.net.ServerSocket;
 7 import java.net.Socket;
 8 
 9 public class ServerDemo {
10     public static void main(String[] args) throws IOException {
11         // 创建服务器Socket对象
12         ServerSocket ss = new ServerSocket(22222);
13 
14         // 监听客户端连接
15         Socket s = ss.accept();
16 
17         // 包装通道内容的流
18         BufferedReader br = new BufferedReader(new InputStreamReader(
19                 s.getInputStream()));
20         String line = null;
21         while ((line = br.readLine()) != null) {
22             System.out.println(line);
23         }
24 
25         // br.close();
26         s.close();
27         // ss.close();
28     }
29 }
运行结果:
(2)   上传图片案例
客户端:
 1 package cn.itcast_13;
 2 
 3 import java.io.BufferedInputStream;
 4 import java.io.BufferedOutputStream;
 5 import java.io.FileInputStream;
 6 import java.io.IOException;
 7 import java.io.InputStream;
 8 import java.net.Socket;
 9 
10 public class UploadClient {
11     public static void main(String[] args) throws IOException {
12         // 创建客户端Socket对象
13         Socket s = new Socket("192.168.12.92", 19191);
14 
15         // 封装图片文件
16         BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
17                 "林青霞.jpg"));
18         // 封装通道内的流
19         BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
20 
21         byte[] bys = new byte[1024];
22         int len = 0;
23         while ((len = bis.read(bys)) != -1) {
24             bos.write(bys, 0, len);
25             bos.flush();
26         }
27         
28         s.shutdownOutput();
29 
30         // 读取反馈
31         InputStream is = s.getInputStream();
32         byte[] bys2 = new byte[1024];
33         int len2 = is.read(bys2);
34         String client = new String(bys2, 0, len2);
35         System.out.println(client);
36 
37         // 释放资源
38         bis.close();
39         s.close();
40     }
41 }
服务器端:
 1 package cn.itcast_13;
 2 
 3 import java.io.BufferedInputStream;
 4 import java.io.BufferedOutputStream;
 5 import java.io.FileOutputStream;
 6 import java.io.IOException;
 7 import java.io.OutputStream;
 8 import java.net.ServerSocket;
 9 import java.net.Socket;
10 
11 public class UploadServer {
12     public static void main(String[] args) throws IOException {
13         // 创建服务器Socket对象
14         ServerSocket ss = new ServerSocket(19191);
15 
16         // 监听客户端连接
17         Socket s = ss.accept();
18 
19         // 封装通道内流
20         BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
21         // 封装图片文件
22         BufferedOutputStream bos = new BufferedOutputStream(
23                 new FileOutputStream("mn.jpg"));
24 
25         byte[] bys = new byte[1024];
26         int len = 0;
27         while ((len = bis.read(bys)) != -1) {
28             bos.write(bys, 0, len);
29             bos.flush();
30         }
31 
32         // 给一个反馈
33         OutputStream os = s.getOutputStream();
34         os.write("图片上传成功".getBytes());
35 
36         bos.close();
37         s.close();
38     }
39 }

运行结果:

5、TCP传输容易出现的问题

(1)   客户端连接上服务端,两端都在等待,没有任何数据传输。
(2)   通过例程分析:
       因为read方法或者readLine方法是阻塞式。
(3)   解决办法:
       自定义结束标记
       使用shutdownInput,shutdownOutput方法。

六、客户端和服务器端原理

1、常见的客户端、服务器端

     最常见的客户端:浏览器,IE/chrome。
     最常见的服务端:服务器,Tomcat。

2、常见网络结构

3、URL&URLConnection

     URI:统一资源标示符。
     URL:统一资源定位符,也就是说根据URL能够定位到网络上的某个资源,它是指向互联网“资源”的指针。
     每个URL都是URI,但不一定每个URI都是URL。这是因为URI还包括一个子类,即统一资源名称
   (URN),它命名资源但不指定如何定位资源。
原文地址:https://www.cnblogs.com/AllenIverson/p/4768901.html