HTTP协议使用

作者:陈广
日期:2018-2-21


我们这一系列文章主要针对的还是Web开发,所以Http协议是绕不过去的坎,这篇文章就讲述一些Http协议的基本概念,让大家对浏览器和服务器之间的交互有一个大至了解。现在有更为安全的Https,将来有机会再介绍。

为帮助大家理解本节所讲内容,会使用一个简单的例子。在Visual Studio 2017中新建.NET Core项目会有一个Web API 选项,它会创建一个最简单的Web API 应用程序,但它创建的程序实在过于简单,我的例子在它的基础上稍作更改,力求使用最简单的代码较完整展示Http的几种常用方法。

HTTP协议概述

浏览器和服务器的交互是通过HTTP协议执行的,而GET和POST也是HTTP协议中的两种方法。HTTP全称为Hyper Text Transfer Protocol,中文翻译为超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务器之间的请求-应答协议。

HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,增,改,删。

GET交互

GET交互方式是从服务器上获取数据,而并非修改数据,所以GET交互方式是安全的。另外,对同一个URL的多个请求,得到的结果是相同的。下面写一个简单例子来演示GET请求。

在本地硬盘新建一个httpTest 文件夹,鼠标右击此文件夹,弹出菜单选择Open with Code,从而使用vscode打开此文件夹。打开终端输入如下命令创建一个空白项目:

dotnet new empty

为方便解析浏览器所发送的URL,我们需要使用.NET Core自带的路由,而路由已经集成在MVC中间件之中,所以接下来引入MVC中间件。这里涉及到中间件及路由知识,两者在.NET Core文档中都有详细介绍,我已翻译完成。有不理解的地方可以去看一眼,不看直接做例子也没问题,心里有个底再去看会更好些。

加入MVC中间件

打开Startup.cs 文件,更改Startup 类代码如下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();
}

其中,services.AddMvc() 注册了MVC中间件,而app.UseMvc() 则启动中间件。

添加一个Controller

Cotroller是控制器的意思,MVC中的“C”便代表了Controller,“M”代表了数据库事务;“V”代表浏览器显示的内容;“C”则负责接收request并返回response,也就是说跟浏览器如何打交道是Controller的事。为使程序简单,我们不打算使用数据库,而直接使用一个List<string> 来存放数据。并省略了“M”,把数据直接放在Controller中。将来再一步步地复杂、分层化。

接下来在httpTest 文件夹下新建一个Controllers 文件夹,并在Controllers 文件夹下新建一个ValuesController.cs 文件。这些操作都可以很方便地在vscode的资源管理器中完成,我就不再赘述了。

ValuesController.cs 文件中添加如下代码:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace httpTest.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        List<string> names = new List<string>() { "张三", "李四" };
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return names.ToArray();
        }
    }
}

在VS2017中可以添加一个控制器类,这样系统会自动生成一部分代码,减少工作量。我找了下,好象vscode没有自带这类模板,需要使用脚手架,脚手架安装起来比较麻烦,以后有机会再介绍吧。这次直接拷贝我的代码就行了。

F5 运行程序,会自动打开浏览器,并在地址栏的地址后面添加/api/values ,按回车发现浏览器显示

["张三","李四"]

如下图所示:

本例,我们使用了一个List<string> 来代替数据库存放数据,并在里面放了两条数据“张三”和“李四”作为初始数据。

特性 [Route("api/[controller]")] 为类指定路由,它为我们访问特定的Controller指定了一个URL。其中api/ 为固定字段,是访问ValuesController 所需要写的固定内容,你可以把其中的api 改成其它单词;[controller] 则表示xxxController.cs 中的xxx。由于此类的文件名为 ValuesController.cs ,所以xxx 所对应的就是Values 。那么,访问此控制器的URL就应当为api/values

特性 [HttpGet] 将一个方法指定为GET访问方法,也就是说当访问URL为api/values,访问方式为GET时,将返回Get() 方法所返回的数据。

根据id返回相应数据

刚才我们演示的是如何返回集合中的所有数据,那么可不可以返回指定id的数据呢?我们更改程序,在ValuesController 类中添加如下代码:

[HttpGet("{id}")]
public string Get(int id)
{
    return names[id];
}

运行程序,在浏览器中输入URL

http://localhost:5000/api/values/0

返回张三
在浏览器中输入URL

http://localhost:5000/api/values/1

返回李四
也就是说,这次我们在URL中的values 之后指定一个索引号,返回了此索引号所对应的单个数据。

特性[HttpGet("{id}")] 表示访问此带参的Get() 方法需要在URL中添加{id}。由于之前我们已经分析了[HttpGet] 代表的是api/values。所以[HttpGet("{id}")] 代表的就应该是api/values/{id}

状态码

http status code 是reponse的一部分,它提供了这些信息:请求是否成功,失败的原因。web api 能涉及到的status codes主要是这些:
200:OK
201:Created,创建了新的资源
204:无内容 No Content,例如删除成功
400:Bad Request,指的是客户端的请求错误.
401:未授权 Unauthorized
403:禁止操作 Forbidden。验证成功,但是没法访问相应的资源
404:Not Found
409:有冲突 Conflict
500:Internal Server Error,服务器发生了错误

这里我们在网页里见得最多的可能就是404了。之前的程序我们如果在URL中输入的是http://localhost:5000/api/values/3,会导致服务端崩溃,这是因为集合里只有两条记录,你输入的索引超出范围自然会引发异常。此时如果我要返回404给浏览器,该如何操作呢?

返回带状态码的对象

首先要明确,返回string 肯定是不行的,浏览器接收字符串只会直接显示出来。.NET Core中的标准操作是返回一个IActionResult,而如果要返回一个对象,则需包装在IActionResult 的子类ObjectResult 内。ObjectResult 内有一个StatusCode属性,它可以存放状态码;而ObjectResultValue属性则可以存放所返回的对象。当然,默认情况下,StatusCode的值为200。下面将带参Get(int id)方法更改如下:

[HttpGet("{id}")]
public IActionResult Get(int id)
{
    if (id >= 0 && id < names.Count)
    {
        return new ObjectResult(names[id]);
    }
    else
    {
        return NotFound();
    }
}

运行程序,在打开win10自带的edge浏览器,按F12打开开发人员工具,切换到网络标签所在窗口,在地址栏输入如下地址(注意端口号可能会不一样):

http://localhost:5000/api/values/1

刷新后显示如下图所示结果:

可以看到,网络栏内的方法GET结果中显示状态码为200。也就是说ObjectResult的默认状态码为200。并非所有浏览器都自带开发人员工具,如果你机子上没有edge浏览器,可以装一个Postman来查看返回状态码,稍后我们会使用Postman

使用NotFound()返回404

接下来看看查找id不存在的情况。在地址栏输入如下地址:

http://localhost:5000/api/values/2

结果如下图所示:

这一次,返回了404状态码。而在我们的代码中,返回404是由NotFound()方法完成的。NotFound()方法内部创建一个NotFoundResult并返回。而NotFoundResult 也是IActionResult 的子类。

使用Ok()返回对象

实际上.NET Core还包装了Ok()方法来返回一个带状态码200的对象,它的使用更为方便,只需将对象作为Ok()方法参数传递进去即可。更改上述代码如下:

[HttpGet("{id}")]
public IActionResult Get(int id)
{
    if (id >= 0 && id < names.Count)
    {
        return Ok(names[id]); //此处为更改代码。
    }
    else
    {
        return NotFound();
    }
}

运行后可以发现效果和new ObjectResult是一样的。Ok()方法返回一个OkObjectResult,而OkObjectResult继承自ObjectResult

POST交互

  1. POST交互是可以修改服务器数据的一种方式,涉及到信息的修改,就会有安全问题。就像数据库的更新,Update一个数据库表时。
  2. 一般的POST交互是必须要用到表单的,但是表单提交的默认方法是GET,如果改为POST方式,就需要修改表单提交时的Method。
  3. POST方式将表单内各个字段和内容放置在HTML HEADER中一起传送到Action属性所指定的URL地址,用户是看不到这个过程的。
  4. POST方式传输的数据安全性较高,因为数据传输不是显示的。这里的安全不是绝对的,如果了解HTTP协议漏洞,通过拦截发送的数据包,同样可以修改交互数据。

Post一个数据

由于Controller是每次接收请求都会创建新实例,所以我们创建的List<string>集合是无法持久保存的。因此把此集合改为静态,以方便多次访问还能保存数据。整个ValuesController类更改代码如下:

[Route("api/[controller]")]
public class ValuesController : Controller
{   //此集合类添加static修饰符
    static List<string> names = new List<string>() { "张三", "李四" };
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return names.ToArray();
    }
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        if (id >= 0 && id < names.Count)
        {
            return Ok(names[id]);
        }
        else
        {
            return NotFound();
        }
    }
    //此处为新添加的Post代码
    [HttpPost]
    public void Post([FromBody] string value)
    {
        names.Add(value);
    }
}

为方便测试,此处我们使用Postman来添加数据,ASP.NET Core文档中使用的也是这个软件。请到以下网址下载并安装:
https://www.getpostman.com/

接下来运行我们刚更改好的程序,打开Postman 并进行如下设置:

设置结果如下图所示:

接下来点击蓝色Send按钮。当然,需要再Get一次才能看到添加结果。

接下来,把HTTP方法改为Get,再次点击Send按钮。结果如下图所示:

可以看到,字符串“王五”已经添加进集合了。

我们来看下这段新添加的代码:

[HttpPost]
public void Post([FromBody] string value)
{
    names.Add(value);
}

特性[HttpPost]表示将下面的Post方法指定为Post访问方法,路由还是按照类上方特性[Route("api/[controller]")],即api/values

特性[FromBody]表示请求的body里面包含着方法需要的实体数据,我们可以从它所标识的value里获取所需数据。MVC接收json数据,所以在Postman里我们需要把上传的数据设置为json格式。在Post()方法中,我们只是简单地将数据添加进集合内。

Post后返回结果

对于POST,成功添加后,建议的返回Status Code 是 201 (Created),还可返回新添加的数据,此功能可使用CreatedAtRoute()实现;如果失败,可以返回400(Bad Request),可通过BadRequest()方法实现,它和Ok()NotFound()类似。接下来更改ValuesController类代码如下:

[Route("api/[controller]")]
public class ValuesController : Controller
{
    static List<string> names = new List<string>() { "张三", "李四" };
    //此处给HttpGet添加一个名称,方便下面的CreatedAtRoute方法调用
    [HttpGet(Name="GetAll")] 
    public IEnumerable<string> Get()
    {
        return names.ToArray();
    }
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        if (id >= 0 && id < names.Count)
        {
            return Ok(names[id]);
        }
        else
        {
            return NotFound();
        }
    }
    [HttpPost]
    public IActionResult Post([FromBody] string value)
    {   //此处代码更改
        if(value==null)
        {
            return BadRequest();
        }
        names.Add(value);
        return CreatedAtRoute("GetAll",names.ToArray());
    }
}

运行程序,按照刚才的设置操作Postman。现在每Post一次,我们都可以即时看到添加的结果。下图是Post两次之后的结果,可以看到,返回的Status Code 是 201:

CreatedAtRoute这个内置的Helper Method可以返回一个带有地址Header的Response,这个Location Header将会包含一个URI,通过这个URI可以我们可以找到返回的数据。但是这个Action必须有一个路由的名字才可以引用它,所以在Get()方法上的Route这个attribute里面加上Name="GetAll",然后在CreatedAtRoute方法第一个参数写上这个名字就可以了,尽管进行了引用,但是Post方法走完的时候并不会调用GetProduct方法,它应该只是提取里面的URI。CreatedAtRoute第二个参数是集合内的所有数据。我们可以在Postman中找到这个URI:单击Postman下方的Headers标签,查看Location项,如下图所示:

HTTP Post标准响应方式

我们也可以仅仅返回新添加的数据和找到它的URI,而不是所有数据,这也是HTTP Post的标准响应方式。ValuesController类更改代码如下:

 [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        static List<string> names = new List<string>() { "张三", "李四" };
        [HttpGet(Name = "GetAll")]
        public IEnumerable<string> Get()
        {
            return names.ToArray();
        }
		//此处添加方法名称方便下面调用		
        [HttpGet("{id}", Name = "GetById")]
        public IActionResult Get(int id)
        {
            if (id >= 0 && id < names.Count)
            {
                return Ok(names[id]);
            }
            else
            {
                return NotFound();
            }
        }
        [HttpPost]
        public IActionResult Post([FromBody] string value)
        {
            if (value == null)
            {
                return BadRequest();
            }
            names.Add(value);
            //此处更改 
            return CreatedAtRoute("GetById", new { id = names.Count - 1 }, value);
        }
    }

同上添加数据“王五”,下图是返回的数据:

下图是返回的URI:

可以把URI直接放入浏览器地址栏访问,即可获取新添加的数据。

这一次CreatedAtRoute使用的是带参Get()方法,我们给它起了个名字GetById,并作为CreatedAtRoute方法的第一个参数,第二个参数对应着带参Get()方法的参数列表,使用匿名类即可。最后一个参数返回我们刚收到的数据。

PUT交互

按照HTTP规范,Put请求 需要一个类似id这样的参数, 用于查找现有的数据以进行修改。更新成功后,应当返回204 (No Content)。PUT请求需要客户端发送整个更新后的实体,而不仅仅是增量。要支持部分更新,使用HTTP PATCH(本文不作介绍)。

ValuesController类中添加如下代码:

[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] string value)
{
    if (value == null || id < 0 || id >= names.Count)
    {
        return BadRequest();
    }
    names[id] = value;
    return NoContent();
}

运行程序,下面我们将“张三”改为“张三丰”。打开Postman,进行如下设置:

DELETE交互

DELETE和PUT基本一样,它做删除操作,也需要id,操作成功后也返回204 No Content

ValuesController类中添加如下代码:

[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    if(id < 0 || id >= names.Count)
    {
        return BadRequest();
    }
    names.RemoveAt(id);
    return NoContent();
}

运行程序,在Postman中设置HTTP方法为DELETE,输入网址http://localhost:5000/api/values/0,注意端口号可能不同。运行结果如下图:

返回204,说明删除成功。Get所有数据结果如下图所示:

一篇长文,花了几天时间,总算写完,有点哆嗦,也不够完善,不过面向基础不是太好的初学者这是必要的。看完这一篇文章再去看.NET Core文档,会比较容易理解些。