018 网络编程基础

作者: 凤邪摩羯 | 来源:发表于2020-11-08 17:01 被阅读0次

    1 网络基础

    1.1 计算机网络分层

    image.png

    OSI七层网络模型(从下往上)

    • 物理层(Physical):设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的 环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等! 在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特
    • 数据链路层(Datalink):可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行 数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等! 另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是 长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。 每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~ 该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:
    • 网络层(Network):主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发 送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了 把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务! 该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!
    • 传输层(Transport):向上面的应用层提供通信服务,面向通信部分的最高层,同时也是 用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络 层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要 的协议就是:TCP传输控制协议UDP用户数据报协议,这也是本章节核心讲解的部分!
    • 会话层(Session):负责在网络中的两节点之间建立、维持和终止通信。建立通信链接, 保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时 决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!
    • 表示层(Presentation):对来自应用层的命令和数据进行解释,对各种语法赋予相应 的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、 数据格式转换和加密解密,压缩解压缩"等
    • 应用层(Application):OSI参考模型的最高层,为用户的应用程序提供网络服务。 它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。
    image.png

    TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。 TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层 (对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供 的网络来完成自己的需求。这4层分别为:

    • 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、 网络远程访问协议(Telnet)等。
    • 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、 用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中, 这一层负责传送数据,并且确定数据已被送达并接收。
    • 网络层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目 的主机(但不检查是否被正确接收),如网际协议(IP)。
    • 链路层:对实际的网络媒体的管理,定义如何使用实际网络 (如Ethernet、Serial Line等)来传送数据。

    1.2 IP地址&端口号

    image.png

    1.2.1 IP地址

    image.png

    1.2.2 端口号

    image.png

    端口号规定为16位,即允许一个IP主机有2的16次方65535个不同的端口。其中:

    • 0~1023:分配给系统的端口号

      我们不可以乱用 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23

    • 1024~49151:登记端口号,主要是让第三方应用使用

      但是必须在IANA(互联网数字分配机构)按照规定手续登记,

    • 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。

    在Socket使用时,可以用1024~65535的端口号

    1.2.3 C/S结构

    • 定义:即客户端/服务器结构,是软件系统体系结构

    • 作用:充分利用两端硬件环境的优势,将任务合理分配到Client端和Server端来实现,降低了系统的通讯开销。

      Socket正是使用这种结构建立连接的,一个套接字接客户端,一个套接字接服务器。

    如图:

    image.png

    可以看出,Socket的使用可以基于TCP或者UDP协议。

    1.3 TCP&UDP

    1.3.1 TCP

    • 定义:Transmission Control Protocol,即传输控制协议,是一种传输层通信协议

      基于TCP的应用层协议有FTP、Telnet、SMTP、HTTP、POP3与DNS。

    • 特点:面向连接、面向字节流、全双工通信、可靠

      • 面向连接:指的是要使用TCP传输数据,必须先建立TCP连接,传输完成后释放连接,就像打电话一样必须先拨号建立一条连接,打完后挂机释放连接。

      • 全双工通信:即一旦建立了TCP连接,通信双方可以在任何时候都能发送数据。

      • 可靠的:指的是通过TCP连接传送的数据,无差错,不丢失,不重复,并且按序到达。

      • 面向字节流:流,指的是流入到进程或从进程流出的字符序列。简单来说,虽然有时候要传输的数据流太大,TCP报文长度有限制,不能一次传输完,要把它分为好几个数据块,但是由于可靠性保证,接收方可以按顺序接收数据块然后重新组成分块之前的数据流,所以TCP看起来就像直接互相传输字节流一样,面向字节流。

    • TCP建立连接 必须进行三次握手:若A要与B进行连接,则必须

      • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认。即A发送信息给B

      • 第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认。即B收到连接信息后向A返回确认信息

      • 第三次握手:客户端收到服务器的(SYN+ACK)报文段,并向服务器发送ACK报文段。即A收到确认信息后再次向B返回确认连接信息

      此时,A告诉自己上层连接建立;B收到连接信息后告诉上层连接建立。

    image.png

    这样就完成TCP三次握手 = 一条TCP连接建立完成 = 可以开始发送数据

    1. 三次握手期间任何一次未收到对面回复都要重发。
    1. 最后一个确认报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态。

    为什么TCP建立连接需要三次握手?

    答:防止服务器端因为接收了早已失效的连接请求报文从而一直等待客户端请求,从而浪费资源

    • “已失效的连接请求报文段”的产生在这样一种情况下:Client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。
    • 这是一个早已失效的报文段。但Server收到此失效的连接请求报文段后,就误认为是Client再次发出的一个新的连接请求。
    • 于是就向Client发出确认报文段,同意建立连接。
    • 假设不采用“三次握手”:只要Server发出确认,新的连接就建立了。
    • 由于现在Client并没有发出建立连接的请求,因此不会向Server发送数据。
    • 但Server却以为新的运输连接已经建立,并一直等待Client发来数据。>- 这样,Server的资源就白白浪费掉了。

    以上答案只是表象,没有说到本质上去

    那么本质是 因为tcp是全双工,为保证传输的可靠性,需要给每次传输的数据段添加序号,那么初始的序列号就是tcp三次握手真正的意义所在,而为了确保交换双方的初始序号,最少需要三次才行

    采用“三次握手”的办法可以防止上述现象发生:

    • Client不会向Server的确认发出确认

    • Server由于收不到确认,就知道Client并没有要求建立连接

    • 所以Server不会等待Client发送数据,资源就没有被浪费


      image.png
    • TCP释放连接 TCP释放连接需要四次挥手过程,现在假设A主动释放连接:(数据传输结束后,通信的双方都可释放连接)

      • 第一次挥手:A发送释放信息到B;(发出去之后,A->B发送数据这条路径就断了)

      • 第二次挥手:B收到A的释放信息之后,回复确认释放的信息:我同意你的释放连接请求

      • 第三次挥手:B发送“请求释放连接“信息给A

      • 第四次挥手:A收到B发送的信息后向B发送确认释放信息:我同意你的释放连接请求

        B收到确认信息后就会正式关闭连接; A等待2MSL后依然没有收到回复,则证明B端已正常关闭,于是A关闭连接

    image.png

    为什么TCP释放连接需要四次挥手?

    为了保证双方都能通知对方“需要释放连接”,即在释放连接后都无法接收或发送消息给对方

    • 需要明确的是:TCP是全双工模式,这意味着是双向都可以发送、接收的

    • 释放连接的定义是:双方都无法接收或发送消息给对方,是双向的

    • 当主机1发出“释放连接请求”(FIN报文段)时,只是表示主机1已经没有数据要发送 / 数据已经全部发送完毕;

      但是,这个时候主机1还是可以接受来自主机2的数据。

    • 当主机2返回“确认释放连接”信息(ACK报文段)时,表示它已经知道主机1没有数据发送了 但此时主机2还是可以发送数据给主机1

    • 当主机2也发送了FIN报文段时,即告诉主机1我也没有数据要发送了 此时,主机1和2已经无法进行通信:主机1无法发送数据给主机2,主机2也无法发送数据给主机1,此时,TCP的连接才算释放

    三次握手&四次挥手面试题总结

    1. 三次握手是什么或者流程?四次挥手呢?答案前面分析就是。

    2. 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

      这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

    image.png
    image.png
    image.png
    image.png

    1.3.2 UPD

    image.png
    • 定义:User Datagram Protocol,即用户数据报协议,是一种传输层通信协议。

      基于UDP的应用层协议有TFTP、SNMP与DNS。

    • 特点:无连接的、不可靠的、面向报文、没有拥塞控制

      • 无连接的:和TCP要建立连接不同,UDP传输数据不需要建立连接,就像写信,在信封写上收信人名称、地址就可以交给邮局发送了,至于能不能送到,就要看邮局的送信能力和送信过程的困难程度了。

      • 不可靠的:因为UDP发出去的数据包发出去就不管了,不管它会不会到达,所以很可能会出现丢包现象,使传输的数据出错。

      • 面向报文:数据报文,就相当于一个数据包,应用层交给UDP多大的数据包,UDP就照样发送,不会像TCP那样拆分。

      • 没有拥塞控制:拥塞,是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象,就像交通堵塞一样。TCP建立连接后如果发送的数据因为信道质量的原因不能到达目的地,它会不断重发,有可能导致越来越塞,所以需要一个复杂的原理来控制拥塞。而UDP就没有这个烦恼,发出去就不管了。

    • 应用场景 很多的实时应用(如IP电话、实时视频会议、某些多人同时在线游戏等)要求源主机以很定的速率发送数据,并且允许在网络发生拥塞时候丢失一些数据,但是要求不能有太大的延时,UDP就刚好适合这种要求。

    1.3.3 Java中对于网络提供的几个关键类

    针对不同的网络通信层次,Java给我们提供的网络功能有四大类:

    • InetAddress: 用于标识网络上的硬件资源

    • URL: 统一资源定位符,通过URL可以直接读取或者写入网络上的数据

    • Socket和ServerSocket: 使用TCP协议实现网络通信的Socket相关的类

    • Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信

    2 Socket

    image.png

    2.1 什么是Socket?

    • 即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口(API) 用來描述IP地址和端口,是通信链的句柄,应用程序可以通过Socket向网络发送请求或者 应答网络请求!Socket是支持TCP/IP协议的网络通信的基本操作单元,是对网络通信过程 中端点的抽象表示,包含了进行网络通信所必须的五种信息

      1. 连接所使用的的协议

      2. 本地主机的IP地址

      3. 本地远程的协议端口

      4. 远程主机的IP地址

      5. 远地进程的协议端口

      1. 即通过Socket,我们才能在Andorid平台上通过 TCP/IP协议进行开发
      1. Socket不是一种协议,而是一个编程调用接口(API),属于传输层(主要解决数据如何在网络中传输)
      1. 成对出现,一对套接字

    2.2 Socket通信模型

    image.png

    2.3 Socket通信步骤

    • Step 1:创建ServerSocket和Socket

    • Step 2:打开连接到的Socket的输入/输出流

    • Step 3:按照协议对Socket进行读/写操作

    • Step 4:关闭输入输出流,以及Socket

    2.3.1 Socket服务端的编写

    • Step 1:创建ServerSocket对象,绑定监听的端口

    • Step 2:调用accept()方法监听客户端的请求

    • Step 3:连接建立后,通过输入流读取客户端发送的请求信息

    • Step 4:通过输出流向客户端发送响应信息

    • Step 5:关闭相关资源

     public static void main(String[] args) throws IOException {
     //1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
     ServerSocket serverSocket = new ServerSocket(12345);
     InetAddress address = InetAddress.getLocalHost();
     String ip = address.getHostAddress();
     Socket socket = null;
     //2.调用accept()等待客户端连接
     System.out.println("~~~服务端已就绪,等待客户端接入~,服务端ip地址: " + ip);
     socket = serverSocket.accept();
     //3.连接后获取输入流,读取客户端信息
     InputStream is=null;
     InputStreamReader isr=null;
     BufferedReader br=null;
     OutputStream os=null;
     PrintWriter pw=null;
     is = socket.getInputStream();     //获取输入流
     isr = new InputStreamReader(is,"UTF-8");
     br = new BufferedReader(isr);
     String info = null;
     while((info=br.readLine())!=null){//循环读取客户端的信息
     System.out.println("客户端发送过来的信息" + info);
     }
     socket.shutdownInput();//关闭输入流
     socket.close();
     }
    

    2.3.2 Socket客户端的编写

    • Step 1:创建Socket对象,指明需要链接的服务器的地址和端号

    • Step 2:链接建立后,通过输出流向服务器发送请求信息

    • Step 3:通过输出流获取服务器响应的信息

    • Step 4:关闭相关资源

    public static void main(String ... args) throws Exception{
     //1.创建客户端Socket,指定服务器地址和端口
     Socket socket = new Socket("127.0.0.1", 12345);
     //2.获取输出流,向服务器端发送信息
     OutputStream os = socket.getOutputStream();//字节输出流
     PrintWriter pw = new PrintWriter(os);//将输出流包装为打印流
     //获取客户端的IP地址
     InetAddress address = InetAddress.getLocalHost();
     String ip = address.getHostAddress();
     pw.write("客户端:~" + ip + "~ 接入服务器!!");
     pw.flush();
     socket.shutdownOutput();//关闭输出流
     socket.close();
     }
    

    3 HTTP

    image.png
    image.png
    image.png
    image.png
    image.png
    image.png

    3.1 Http版本区别

    3.1.1 HTTP 1.0

    • 请求与响应支持头域
    • 响应对象以一个响应状态行开始
    • 响应对象不只限于超文本
    • 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法
    • 支持长连接(但默认还是使用短连接),缓存机制,以及身份认证

    3.1.2 HTTP 1.1

    • Persistent Connection(keepalive连接)

    • chunked编码传输

    • 字节范围请求

    • Pipelining(请求流水线)

    • 请求消息和响应消息都应支持Host头域

    • 新增了一批Request method

    HTTP1.1增加了OPTIONS,PUT, DELETE, TRACE, CONNECT方法

    • 缓存处理

    HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。

    3.1.3 HTTP 2.0

    • 多路复用(二进制分帧)

    HTTP 2.0最大的特点: 不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

    • 头部压缩

    当一个客户端向相同服务器请求许多资源时,像来自同一个网页的图像,将会有大量的请求看上去几乎同样的,这就需要压缩技术对付这种几乎相同的信息。

    • 随时复位

    HTTP1.1一个缺点是当HTTP信息有一定长度大小数据传输时,你不能方便地随时停止它,中断TCP连接的代价是昂贵的。使用HTTP2的RST_STREAM将能方便停止一个信息传输,启动新的信息,在不中断连接的情况下提高带宽利用效率。

    • 服务器端推流: Server Push

    客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。

    • 优先权和依赖

    每个流都有自己的优先级别,会表明哪个流是最重要的,客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。

    3.2 Restful

    • REST(Resource Representational State Transfer)是Roy Thomas Fielding在他2000年的博士论文中提出的。如果一个架构符合REST原则,就称为RESTful架构

    • 本质:一种软件架构风格

    • 核心:面向资源

    • 解决问题:降低开发的复杂性;提高系统的可伸缩性

    • Restful资源层Resource:文本、图片、服务、音频等等

    • Restful表现层Representational

      • 文本:txt、html、xml、json、二进制

      • 图片:jpg、png

      • Case:book是一个资源,获取不同的格式

      • http协议的content-type和accept

    • Restful状态转化State Transfer

      • 幂等性:每次HTTP请求相同的参数,相同的URI,产生的结果是相同的

      • GET

      • POST

      • PUT

      • DELETE

    设计概念和准则

    • 网络上的所有事物都可以被抽象为资源。

    • 每一个资源都有唯一的资源标识,对资源的操作不会改变这些标识。

    • 所有的操作都是无状态的。

    资源:网络上的一个实体,或者说是网络上的一个具体信息。

    schema://host[:port]/path ?query-string

    • schema:指定底层使用的协议(例如:http,https,ftp)

    • host:服务器的IP地址或者域名

    • port:服务器端口,默认为80

    • path:访问资源的路径

    • query-string:发送给http服务器的数据

    • anchor:锚

    SOAP WebService

    WebService:是一种跨编程语言和操作系统平台的远程调用技术。

    WebService通过HTTP协议发送请求和接收结果时采用XML格式封装,并增加了一些特点的HTTP消息头,这些特定的HTTP消息头和XML内容格式就是SOAP协议。

    效率与易用性

    SOAP由于各种需求不断扩充其本身协议的内容,导致在SOAP处理方面的性能有所下降。同时在易用性方面以及学习成本上也有所增加。

    RESTful由于其面向资源接口设计以及操作抽象简化了开发者的不良设计,同时也最大限度的利用了http最初的应用协议设计理念。

    安全性

    RESTful对于资源型服务接口来说很合适,同时特别适合对效率要求很高,但是对于安全要求不高的场景。

    SOAP的成熟性可以给需要提供给多开发语言,对于安全性要求较高的接口设计带来便利。所以我觉得纯粹说什么设计模式将会占据主导地位没有什么意义,关键还是看应用场景。

    如何设计RESTful API

    • 资源路径(URI)

    • HTTP动词

    • 过滤信息

    • 状态码

    • 错误处理

    HTTP动词

    对于资源的操作(CRUD),由HTTP动词(谓词)表示。

    • GET:从服务器取出资源(一项或多项)

    • POST:在服务器新建一个资源

    • PUT:在服务器更新资源(客户端提供改变后的完整资源)

    • PATCH:在服务器更新资源(客户端提供改变的属性)

    • DELETE:从服务器删除资源

    请求类型 请求路径 功能
    GET /books 获取列表
    POST /book 创建一本书
    GET /books/id 通过id查询一本书列表
    PUT /book/id 通过id更新一本书
    DELETE /book/id 通过id删除一本书

    过滤信息

    • 如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

    举例:

    • ?offset=10:指定返回记录的开始位置

    • ?page=2&per_page=100:指定第几页,以及每页的记录数

    • ?sortby=name&order=asc:指定返回结果排序,以及排序顺序

    • ?animal_type_id=1:指定筛选条件

    3.2.1、URL 设计

    3.2.1.1 动词 + 宾语

    RESTful 的核心思想就是,客户端发出的数据操作指令都是"动词 + 宾语"的结构。比如,GET /articles这个命令,GET是动词,/articles是宾语。

    动词通常就是五种 HTTP 方法,对应 CRUD 操作。

    • GET:读取(Read)
    • POST:新建(Create)
    • PUT:更新(Update)
    • PATCH:更新(Update),通常是部分更新
    • DELETE:删除(Delete)

    根据 HTTP 规范,动词一律大写。

    3.2.1.2 动词的覆盖

    有些客户端只能使用GETPOST这两种方法。服务器必须接受POST模拟其他三个方法(PUTPATCHDELETE)。

    这时,客户端发出的 HTTP 请求,要加上X-HTTP-Method-Override属性,告诉服务器应该使用哪一个动词,覆盖POST方法。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n160" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">POST /api/Person/4 HTTP/1.1
    X-HTTP-Method-Override: PUT</pre>

    上面代码中,X-HTTP-Method-Override指定本次请求的方法是PUT,而不是POST

    3.2.1.3 宾语必须是名词

    宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。比如,/articles这个 URL 就是正确的,而下面的 URL 不是名词,所以都是错误的。

    • /getAllCars
    • /createNewCar
    • /deleteAllRedCars

    3.2.1.4 复数 URL

    既然 URL 是名词,那么应该使用复数,还是单数?

    这没有统一的规定,但是常见的操作是读取一个集合,比如GET /articles(读取所有文章),这里明显应该是复数。

    为了统一起见,建议都使用复数 URL,比如GET /articles/2要好于GET /article/2

    3.2.1.5 避免多级 URL

    常见的情况是,资源需要多级分类,因此很容易写出多级的 URL,比如获取某个作者的某一类文章。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n179" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">GET /authors/12/categories/2</pre>

    这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。

    更好的做法是,除了第一级,其他级别都用查询字符串表达。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n183" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">GET /authors/12?categories=2</pre>

    下面是另一个例子,查询已发布的文章。你可能会设计成下面的 URL。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n186" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">GET /articles/published</pre>

    查询字符串的写法明显更好。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n189" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">GET /articles?published=true</pre>

    3.2.2 状态码

    3.2.2.1 状态码必须精确

    客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。

    HTTP 状态码就是一个三位数,分成五个类别。

    • 1xx:相关信息
    • 2xx:操作成功
    • 3xx:重定向
    • 4xx:客户端错误
    • 5xx:服务器错误

    这五大类总共包含100多种状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

    API 不需要1xx状态码,下面介绍其他四类状态码的精确含义。

    3.2.2.2 状态码2XX

    200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。

    • GET: 200 OK
    • POST: 201 Created
    • PUT: 200 OK
    • PATCH: 200 OK
    • DELETE: 204 No Content

    上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。

    此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n225" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">HTTP/1.1 202 Accepted

    {
    "task": {
    "href": "/api/company/job-management/jobs/2130040",
    "id": "2130040"
    }
    }</pre>

    3.2.2.3 状态码

    API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。

    API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302307的含义一样,也是"暂时重定向",区别在于302307用于GET请求,而303用于POSTPUTDELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n230" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">HTTP/1.1 303 See Other
    Location: /api/orders/12345</pre>

    3.2.2.4 状态码4XX

    4xx状态码表示客户端错误,主要有下面几种。

    400 Bad Request:服务器不理解客户端的请求,未做任何处理。

    401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。

    403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。

    404 Not Found:所请求的资源不存在,或不可用。

    405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。

    410 Gone:所请求的资源已从这个地址转移,不再可用。

    415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。

    422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。

    429 Too Many Requests:客户端的请求次数超过限额。

    3.2.2.5 5xx 状态码

    5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

    500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。

    503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。

    3.2.3 服务器回应

    3.2.3.1 不要返回纯本文

    API 返回的数据格式,不应该是纯文本,而应该是一个 JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的Content-Type属性要设为application/json

    客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的ACCEPT属性也要设成application/json。下面是一个例子。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n251" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">GET /orders/2 HTTP/1.1
    Accept: application/json</pre>

    3.2.3.2 发生错误时,不要返回 200 状态码

    有一种不恰当的做法是,即使发生错误,也返回200状态码,把错误信息放在数据体里面,就像下面这样。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n255" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">HTTP/1.1 200 OK
    Content-Type: application/json

    {
    "status": "failure",
    "data": {
    "error": "Expected at least two items in list."
    }
    }</pre>

    上面代码中,解析数据体以后,才能得知操作失败。

    这张做法实际上取消了状态码,这是完全不可取的。正确的做法是,状态码反映发生的错误,具体的错误信息放在数据体里面返回。下面是一个例子。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n259" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">HTTP/1.1 400 Bad Request
    Content-Type: application/json

    {
    "error": "Invalid payoad.",
    "detail": {
    "surname": "This field is required."
    }
    }</pre>

    3.2.3.3 提供链接

    API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在回应中,给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。

    举例来说,GitHub 的 API 都在 api.github.com 这个域名。访问它,就可以得到其他 URL。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n264" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">{
    ...
    "feeds_url": "https://api.github.com/feeds",
    "followers_url": "https://api.github.com/user/followers",
    "following_url": "https://api.github.com/user/following{/target}",
    "gists_url": "https://api.github.com/gists{/gist_id}",
    "hub_url": "https://api.github.com/hub",
    ...
    }</pre>

    上面的回应中,挑一个 URL 访问,又可以得到别的 URL。对于用户来说,不需要记住 URL 设计,只要从 api.github.com 一步步查找就可以了。

    HATEOAS 的格式没有统一规定,上面例子中,GitHub 将它们与其他属性放在一起。更好的做法应该是,将相关链接与其他属性分开。

    <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="http" cid="n268" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 0px; margin-top: 0px; width: inherit;">HTTP/1.1 200 OK
    Content-Type: application/json

    {
    "status": "In progress",
    "links": {[
    { "rel":"cancel", "method": "delete", "href":"/api/status/12345" } ,
    { "rel":"edit", "method": "put", "href":"/api/status/12345" }
    ]}
    }</pre>

    相关文章

      网友评论

        本文标题:018 网络编程基础

        本文链接:https://www.haomeiwen.com/subject/ekapvktx.html