C#网络编程

Session

作者:陈广 日期:2018-12-5


Session,在计算机中,尤其网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。

这段定义听着怎么感觉那么像 Cookie 呢?没错,Session 和 Cookie 类似,只是 Cookie 是客户端机制,而 Session 是服务器机制,而且,一般情况下,Session 是和 Cookie 配合使用的。

Session 的运行机制

Session 与 Cookie 是如何配合的呢?下面大概介绍一下?

  • 当浏览器访问服务器时,服务器首先检查客户端请求 Cookie 中是否包含一个 Session Id。
    • 如果已包含则说明以前已经为此客户端创建过 Session,服务器就按照 Session Id 把这个 Session 检索出来使用(检索不到,会新建一个)。
    • 如果客户端请求不包含 Session Id,则为此客户端创建一个 Session 并且生成一个与此 Session 相关联的 Session Id,Session Id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串。这个 Session Id 将被在本次响应中通过 Cookie 的方式返回给客户端保存。
  • 浏览器重新访问服务器时,通过 Cookie 带上此 Session Id。这样,服务器就知道是谁在进行访问了。

图 1: Session 运行机制

Session 是如何工作的

理解一个知识点的最好办法就是运行一个例子,查看它是如何工作的。下面我们就通过例子来查看 Session 的运行流程。

准备项目

新建一个名为 Session 的文件夹,在右键菜单上选择【Open with Code】打开此文件夹。按下【Ctrl + ~】快捷键打开终端,输入如下命令:

dotnet new empty

创建项目完成后,首先关掉 HTTPS,打开 Properties 文件夹下的 launchSettings.json 文件,将sslPort项的值更改为0以关闭 HTTPS。将applicationUrl项的值更改如下:

"applicationUrl": "http://localhost:5000",

ASP.NET Core 已经内置了完善的 Session 服务,我们只需添加相应的服务就可以调用了。更改 Startup.cs 文件代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Session
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSession(); 
            services.AddMvc();
        }

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

            app.UseSession();
            app.UseMvcWithDefaultRoute();
        }
    }
}

要使用 Session,请先在ConfigureServices方法中使用services.AddSession(),然后在Configure方法中使用app.UseSession()。这里需要注意的是app.UseSession()一定要放在app.UseMvc()之前调用,否则会引发InvalidOperationException异常。

添加控制器

接下来添加控制器。在项目根目录中添加一个 Controllers 文件夹,并在其中新建一个 HomeController.cs 文件,输入代码如下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Session.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {   //设置一个 Session
            HttpContext.Session.SetInt32("Item1", 2018);
            return View();
        }

        public IActionResult Second()
        {   //获取指定键的 Session 值
            int? num = HttpContext.Session.GetInt32("Item1");
            return View(num);
        }

        public IActionResult Three()
        {   //获取指定键的 Session 值
            int? num=HttpContext.Session.GetInt32("Item1");
            return View(num);
        }
    }
}

此程序结构和上篇讲 Cookie 文章的程序类似,也是三个 action 方法对应三个视图。不同之处就是这里是设置并读取 Session 的值。

添加视图

在项目根目录下新建 Views 文件夹。

_ViewImports.cshtml

在 Views 文件夹下新建一个 _ViewImports.cshtml 文件,并输入如下代码以加入标签助手引用:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Index.cshtml

在 Views 文件夹下新建 Home 文件夹。然后在 Home 文件夹下新建 Index.cshtml 文件,并输入如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <a asp-action="Second">跳转至第二个页面</a>
</body>
</html>

此视图为根视图,里面只存在一个跳转至第二个页面的链接。它所对应的 action 方法为Index。

Second.cshtml

在 Views/Home 文件夹下新建 Second.cshtml 文件,输入如下代码:

@model int

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h2>这是第二个页面</h2>
    服务器读取的 Session 值:
    <h3>@Model</h3>
    <a asp-action="Three">跳转到第三个页面</a>
</body>
</html>

此视图为第二个页面所对应的视图文件,第一句@model语句表明此视图将接受由 action 方法传递过来的int对象,并在后面通过Model对象调用。action 方法的传递方式可通过查看 HomeController.cs 文件下的Second()方法的返回语句:

Three.cshtml

在 Views/Home 文件夹下新建 Three.cshtml 文件,输入如下代码:

@model int

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h2>这是第三个页面</h2>
    服务器读取的 Session 值:
    <h3>@Model</h3>
</body>
</html>

此视图为第三个页面。

运行程序并解析

运行程序,在浏览器中按【F12】打开开发者工具。查看响应标头,可以看到服务器在响应中加入了一个 Cookie,键为.AspNetCore.Session,如下图所示。

图 2: 查看响应 Cookie

图中 Set-Cookie 部分全文如下:

   Set-Cookie: .AspNetCore.Session=CfDJ8O%2FaneK7Ow1Fm53GRHD%2F5J6iNuagtIrXNUIM4aAvEnR%2FhWchlgkeweEveUgDg8aPvAXGjGcd4xFleYzpi043Olf2ylc4Q0tdX8lKcemR7NNgV2XIts%2BqLEIqeGb5hTrZgzDMSEkZq%2BYTlnrfC61BTG0dkgYyZ15K5NKbTaXR1Zmy; path=/; samesite=lax; httponly

.AspNetCore.Session的值已经过 ASP.NET Core 加密,我们无法得知,它应该就是我们之前所说的 Session Id。

接下来点击页面【跳转到第二个页面】链接,然后查看【请求标头】,我们看到请求 header 中已发送了 Cookie 项,键值依然为.AspNetCore.Session,这正是刚才服务器发送过来的 Cookie。如下图所示:

图 3: 查看请求 Cookie

其中 Cookie 值全文如下:

   Cookie: .AspNetCore.Session=CfDJ8O%2FaneK7Ow1Fm53GRHD%2F5J6g0%2FZaeJFvUf5bHsP6WQReV0s%2F6%2Bxl7cGRSiMXJS4kX0wMnU8NJi3q6bFwQmAQsbFAhhYPtN0BUAFHdSjC4c840X8KCT3gz8OmKNxtV4xHK%2FbMHiGQF0LD1d5JsgQPn2cg2LvUuR8klg%2FivvRlU6DX

我们看到,返回去的值已有了改变,但从页面中服务器读出来的 Session 值来看,里面存放的值是没有改变的。

接下来继续点击页面上的【跳转到第三个页面】链接,查看开发者工具,发现跳转还是携带了相同的 Cookie,并且服务器读取了 Session 值并返回页面显示。

由这个例子我们可以推断出。当浏览器第一次请求服务器时,服务器通过HttpContext.Session.SetInt32设置了 Session 值,并向浏览器通过 Set-Cookie 返回一个 Session Id,此 Id 由 .AspNetCore.Session 指定。在浏览器的一系列后续请求中,浏览器会通过 Cookie 中将此 Id 一起发送给服务器。然后服务器通过此 Id 找到相应的 Session,并读取其中的值返回给客户端显示。

配置 Session 状态

在配置会话状态服务时,我们可以设置各种属性:

  1. Cookie.Name :用来覆盖缺省的Cookie名称。
  2. Cookie.HttpOnly :设置Cookie是否可以通过JavaScript访问。缺省值是true,也就是说在客户端不能通过脚本访问。
  3. Cookie.Path :表示 Cookie 所属的路径,请参考上一篇文章 Cookie 属性。
  4. Cookie.Domain :确定用于创建 Cookie 的域。默认情况下不提供。
  5. Cookie.SecurePolicy :确定 Cookie 是否只应在 HTTPS 请求上传输。
  6. IdleTimeout :IdleTimeout 指示在丢弃其内容之前 Session 可以空闲多久。每个 Session 访问都会重置超时。注意,这只适用于会话的内容,而不是 Cookie。
  7. IOTimeout :从存储区加载 Session 或将其提交回存储区所允许的最大时间。注意,这可能只适用于异步操作。可以使用InfiniteTimeSpan禁用此超时。

下面更改示例,以演示如何设置 Session 属性。更改 Startup.cs 文件代码如下:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Session
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSession(options =>
            {
                options.Cookie.Name = ".Iotxfd.Session"; //更改 Cookie 名称
                options.IdleTimeout = TimeSpan.FromSeconds(10); //10秒后 Session 失效
            });
            services.AddMvc();
        }

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

            app.UseSession();
            app.UseMvcWithDefaultRoute();
        }
    }
}

注意,新增了一个命名空间,更改了ConfigureServices方法内的代码。这里,我们将 Cookie 名称进行了更改,并设置了超时时间。

运行程序,使用开发者工具查看 Cookie,可以看到 Cookie 的名称已被更改为'.Iotxfd.Session',如下图所示:

图 4: 查看响应 Cookie

第一个页面打开 10 秒内跳转到第二个页面还可以读取 Session 值 2018。再等 10 以后点击第三个页面则读取出的 Session 值就变为 0 了,说明 Session 过了 10 秒后就失效了。

实验做完后,将改码改回原样,继续下面的实验。

使用 Session 读取复杂对象

我们在写程序时,通过代码提示可以看到 Session 只有三个设置值的方法:SetSetInt32SetString。也就是说 Session 只能存储字节数组、整数和字符串。如果要保存其他数据类型或自定义类该怎么办泥?微软给出的解决方案是使用 JSON。在 C# 中操作 JSON 可以使用 Newtonsoft 程序包,Newtonsoft 异常强大,可以直接将复杂对象序列化为 JSON 格式,之后可以将此序列化后的 JSON 字符串反序列化为 C# 可以使用的对象。那么,我们在写 Session 时就可以通过 Newtonsoft 将复杂对象序列化为字符串进行存储,在读 Session 时将读取的字符串反序列化为 C# 对象。这个过程需要使用扩展方法来实现,以使我们可以直接在HttpContext.Session属性中调用。

下面演示如何在 Session 中存储日期。更改 HomeController.cs 文件如下:

using System;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Session.Controllers
{
    //扩展方法
    public static class SessionExtensions
    {
        public static void Set<T>(this ISession session, string key, T value)
        {   //将对象 T 写入 Session
            session.SetString(key, JsonConvert.SerializeObject(value));
        }

        public static T Get<T>(this ISession session, string key)
        {   //从 Session 中读对象 T
            var value = session.GetString(key);

            return value == null ? default(T) :
                JsonConvert.DeserializeObject<T>(value);
        }
    }

    public class HomeController : Controller
    {
        public IActionResult Index()
        {   //设置一个 Session
            HttpContext.Session.Set<DateTime>("Item", DateTime.Now);
            return View();
        }

        public IActionResult Second()
        {
            DateTime time = HttpContext.Session.Get<DateTime>("Item");
            return View(time);
        }

        public IActionResult Three()
        {
            DateTime time = HttpContext.Session.Get<DateTime>("Item");
            return View(time);
        }
    }
}

注意:如果是 .NET Core 2.0 及以前版本,需要引入 Newtonsoft NuGet 程序包。2.1 版本之后,.NET Core 已经内置了 Newtonsoft。具说 .NET Core 3.0 将移除内置 Newtonsoft,改用高性能内置 JSON APIs,使用Span<T>实现,变化太快了。

接下来将 Second.cshtml 的第一句代码:

@model int

改为

@model DateTime

接下来将 Three.cshtml 的第一句代码:

@model int

改为

@model DateTime

运行程序,我们看到,现在 Session 已经可以存储时间了,如下图所示。

图 5: 使用 Session 存储时间

简易购物车

为了加深对 Session 的理解,下面我们来做一个常见的购物车应用。

准备项目

新建一个名为 ShoppingCart 的文件夹,在右键菜单上选择【Open with Code】打开此文件夹。按下【Ctrl + ~】快捷键打开终端,输入如下命令:

dotnet new empty

创建项目完成后,首先关掉 HTTPS,打开 Properties 文件夹下的 launchSettings.json 文件,将sslPort项的值更改为0以关闭 HTTPS。将applicationUrl项的值更改如下:

"applicationUrl": "http://localhost:5000",

下面添加 Session 服务,更改 Startup.cs 文件代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Session
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSession(); 
            services.AddMvc();
        }

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

            app.UseSession();
            app.UseMvcWithDefaultRoute();
        }
    }
}

添加控制器

接下来添加控制器。在项目根目录中添加一个 Controllers 文件夹,并在其中新建一个 HomeController.cs 文件,输入代码如下:

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

namespace ShoppingCart.Controllers
{
    public static class SessionExtensions
    {
        public static void Set<T>(this ISession session, string key, T value)
        {   //将对象 T 写入 Session
            session.SetString(key, JsonConvert.SerializeObject(value));
        }

        public static T Get<T>(this ISession session, string key)
        {   //从 Session 中读对象 T
            var value = session.GetString(key);

            return value == null ? default(T) :
                JsonConvert.DeserializeObject<T>(value);
        }
    }

    public class HomeController : Controller
    {
        static List<string> products = new List<string>()
        {   //商品数据
            "手机","电脑","苹果","饼干","鼠标",
            "钢笔","裤子","相机","香蕉","雨伞"
        };

        public IActionResult Index()
        {   //从 Session 中获取购物车商品
            List<string> cart = HttpContext.Session.Get<List<string>>("Cart");
            if (cart != null)
            {   //将购物车数据通过 ViewData 传递给页面
                ViewData["Cart"]=cart.ToArray();
            }
            return View(products.ToArray());
        }

        public IActionResult AddToCart(int id)
        {
            List<string> cart = HttpContext.Session.Get<List<string>>("Cart");
            if (cart != null)
            {   //从 Session 中获取购物车商品
                if (!cart.Contains(products[id]))
                {   //如果商品不在购物车中,则加进去
                    cart.Add(products[id]);
                    HttpContext.Session.Set<List<string>>("Cart", cart);
                }
            }
            else
            {   //如果还未创建购物车,则创建它
                cart = new List<string>();
                cart.Add(products[id]);
                HttpContext.Session.Set<List<string>>("Cart", cart);
            }
            return RedirectToAction(nameof(Index)); //重定向至首页
        }
    }
}

为了省事,Session 的扩展方法直接放在控制器里了,程序的模型层也省了,直接使用一个List存放商品数据。这样做只是为了专注于 Session,尽量简单,排除所有干扰。这些并不是好的习惯,实际开发中千万不要这样做。

Index action 方法中,由于View方法参数已经用于传递商品数据,所以只能通过ViewData传递购物车数据。AddToCart action 方法用于在购物车中添加商品。我们可以看到,购物车里的商品实际上是存放在 Session 中的,购物车处理完毕后会重定向至首页以实时更新购物车内容。

添加视图

在项目根目录下新建 Views 文件夹。

Index.cshtml

在 Views 文件夹下新建 Home 文件夹。然后在 Home 文件夹下新建 Index.cshtml 文件,并输入如下代码:

@model string[]

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>IOT小分队购物网站</h1>
    <ul>
        @for (int i = 0; i < Model.Length; i++)
        {
            <li>
                @{
                    string s=$"/Home/AddToCart/{i}";
                }
                <a href=@s>@Model[i]</a>
            </li>
        }
    </ul>
    <hr>
    <h2>购物车:</h2>
    <ul>
        @if(ViewData["Cart"] != null)
        {
            @foreach(var item in ViewData["Cart"] as string[])
            {
                <li>
                    @item
                </li>
            }
        }
    </ul>
</body>
</html>

此页面分为两部分,上部分为商品列表,通过 action 方法中返回的模型数据获取。下半部分为购物车,通过ViewData获取。

其实,将商品加入购物车应使用 POST 方法提交数据,为了省事,我直接将商品索引使用 GET 方法通过 URL 传递给服务器了。要怪只能怪 Razor 太强大,这样写都可以,几句代码搞定。

运行程序

运行程序,点击商品,可以看到,所点击的商品会实时在下方购物车中列出,如图3所示:

图 5: 运行程序

呵呵,这应当是史上最穷购物车了,什么功能都木有。你要功能比较完善的购物车也有,请在本网站找自由男写的《Pro ASP.NET Core MVC 2(第7版)》这本书,里面的运动商店项目有购物车功能,足够复杂。

虽然程序功能简单,但它还是可以帮助我们做一些实验以进一步了解 Session 机制的。

  • 使用同一浏览器的两个窗口同时访问网站,我们会发现它们使用的是相同的 Session。
  • 浏览器关闭后再重新访问网站,我们会发现会使用新的 Session。
  • 使用不同的浏览器同时访问网站,我们会发现它们使用的是不同的 Session。

关于购物车

对于大的购物网站来说,比如京东,购物车功能是存放在 Cookie 里面的。这么多人访问,使用 Session 来存放并不现实。京东在不登录的情况下在购物车中加入商品,即使等到第二天,商品仍然存在。清空浏览器 Cookie,购物车商品就空了,这佐证了以上说法。

;

© 2018 - IOT小分队文章发布系统 v0.3