在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 40 章 ASP.NET Core(下)),不对的地方欢迎指出与交流。 章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。 本章节译文分为上下篇,上篇见:C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(上) --------------------------------------------- 通过HTTP协议,客户端向服务器发出请求。该请求通过响应回答。 该请求包括头部,并且在许多情况下,包括到服务器的主体信息。服务器根据客户端的需要通过主体信息定义不同的结果。来看看可以从客户端读取什么信息。 要将HTML格式的输出返回到客户端,GetDiv方法创建一个div元素,其中包含传递的参数key和value(代码文件WebSampleApp/RequestAndResponseSample.cs)的span元素: public static string GetDiv(string key, string value) =>
$"<div><span>{key}:</span><span>{value}</span></div>";
因为在以下示例中需要这些HTML div和span标签来包围字符串,所以创建扩展方法来覆盖该功能(代码文件WebSampleApp/HtmlExtensions.cs): public static class HtmlExtensions
{
public static string Div(this string value) =>
$"<div>{value}</div>";
public static string Span(this string value) =>
$"<span>{value}</span>";
}
方法 GetRequestInformation 使用 HttpRequest 去对象访问Scheme,Host,Path,QueryString,Method和Protocol属性(代码文件WebSampleApp/RequestAndResponseSample.cs): public static string GetRequestInformation(HttpRequest request)
{
var sb = new StringBuilder();
sb.Append(GetDiv("scheme", request.Scheme));
sb.Append(GetDiv("host", request.Host.HasValue ? request.Host.Value :
"no host"));
sb.Append(GetDiv("path", request.Path));
sb.Append(GetDiv("query string", request.QueryString.HasValue ?
request.QueryString.Value :"no query string"));
sb.Append(GetDiv("method", request.Method));
sb.Append(GetDiv("protocol", request.Protocol));
return sb.ToString();
}
Startup类的Configure方法更改为调用GetRequestInformation方法,并通过HttpContext的Request属性传递HttpRequest。 结果写入Response对象(代码文件WebSampleApp/Startup.cs): app.Run(async (context) =>
{
await context.Response.WriteAsync(RequestAndResponseSample.GetRequestInformation(context.Request));
});
从Visual Studio启动程序将产生以下信息: scheme:http
host:localhost:5000
path: /
query string: no query string
method: GET
protocol: HTTP/1.1
添加一个路径到路径值的请求结果,例如 http://localhost:5000/Index,设置如下: scheme:http
host:localhost:5000
path: /Index
query string: no query string
method: GET
protocol: HTTP/1.1
添加查询字符串,如 http://localhost:5000/Add?x=3&y=5, 查询字符串访问 QueryString,如下所示: query string: ?x=3&y=5
下一个代码片段中,使用HttpRequest的Path属性来创建轻量级自定义路由。 根据客户端设置的路径,调用不同的方法(代码文件WebSampleApp/Startup.cs): app.Run(async (context) =>
{
string result = string.Empty;
switch (context.Request.Path.Value.ToLower())
{
case"/header":
result = RequestAndResponseSample.GetHeaderInformation(context.Request);
break;
case"/add":
result = RequestAndResponseSample.QueryString(context.Request);
break;
case"/content":
result = RequestAndResponseSample.Content(context.Request);
break;
case"/encoded":
result = RequestAndResponseSample.ContentEncoded(context.Request);
break;
case"/form":
result = RequestAndResponseSample.GetForm(context.Request);
break;
case"/writecookie":
result = RequestAndResponseSample.WriteCookie(context.Response);
break;
case"/readcookie":
result = RequestAndResponseSample.ReadCookie(context.Request);
break;
case"/json":
result = RequestAndResponseSample.GetJson(context.Response);
break;
default:
result = RequestAndResponseSample.GetRequestInformation(context.Request);
break;
}
await context.Response.WriteAsync(result);
});
以下部分将实现不同的方法来显示请求头信息,查询字符串等。 请求头信息来看看客户端在HTTP头信息中发送的信息。 为了访问HTTP头信息,HttpRequest对象定义Headers属性。 这是IHeaderDictionary类型,它包含一个头的名称和值的字符串数组的字典。 使用此信息,先前创建的GetDiv方法用于为客户端写入div元素(代码文件WebSampleApp/RequestAndResponseSample.cs): public static string GetHeaderInformation(HttpRequest request)
{
var sb = new StringBuilder();
IHeaderDictionary headers = request.Headers;
foreach (var header in request.Headers)
{
sb.Append(GetDiv(header.Key, string.Join(";", header.Value)));
}
return sb.ToString();
}
结果取决于所使用的浏览器。 我们来比较一下他们中的几个。 以下是来自Windows 10触摸设备上的Internet Explorer 11: Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,image/jxr,*.*
Accept-Encoding: gzip, deflate
Accept-Language: en-Us,en;q=0.8,de-AT;q=0.6,de-DE;q=0.4,de;q=0.2
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch;
rv:11.0)
like Gecko
Google Chrome 47.0版显示此信息,包括来自AppleWebKit,Chrome和Safari的版本号: Connection: keep-alive
Accept:
text/html,application/xhtml,application/xml;q=0.9,image/webp,*.*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-Us;en;q=0.8
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome 47.0.2526.80 Safari/537.36
Microsoft Edge提供了此信息,包括来自AppleWebKit,Chrome,Safari和Edge的版本号: Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,image/jxr,*.*
Accept-Encoding: gzip, deflate
Accept-Language: en-Us,en;q=0.8,de-AT;q=0.6,de-DE;q=0.4,de;q=0.2
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML,
从这个头信息中可以得出什么结论? Connection头是HTTP 1.1协议的增强。有了这个,客户端可以请求保持连接打开。通常使用HTML,客户端发出多个请求,例如以获取图像,CSS和JavaScript文件。服务器可能会满足请求,也可能会忽略该请求以防负载过高,最好是关闭连接。 Accept头定义了浏览器接受的mime格式。该列表按优先格式排序。根据该信息,可能会决定根据客户端的需要以不同的格式返回数据。 IE更适应HTML格式,然后是XHTML和JXR。 Google Chrome则是不同的列表,它更喜欢这些格式:HTML,XHTML,XML和WEBP。利用这些信息中的一些,还定义了量词。用于输出的浏览器在此列表的末尾都有*。*,以接受返回的所有数据。 Accept-Language头信息显示用户已配置的语言。该信息可以返回本地化信息。本地化在第28章“本地化”中讨论。 注意 很久以前,服务器保留了很长的浏览器功能列表。这些列表用于了解浏览器可以使用的功能。要识别浏览器,可以使用来自浏览器的用于映射功能的代理字符串。随着时间的推移,浏览器提供错误的信息,甚至允许用户配置想要的浏览器名称,以便得到一些更多的功能(因为浏览器列表通常没有在服务器上更新)。过去Internet Explorer(IE)通常需要与所有其他浏览器不同的编程。 Microsoft Edge与IE非常不同,并且有更多与其他供应商的浏览器相同的功能。这就是为什么Microsoft Edge在User-Agent字符串中显示Mozilla,AppleWebKit,Chrome,Safari和Edge。最好不要使用此User-Agent字符串来获取可用的功能列表。相反,请检查需要编程的特定功能。 目前为止,通过浏览器发送的头信息是发送到非常简单的网站的信息。通常会有更多的细节,如Cookie,身份验证信息,以及自定义信息。要查看发送到服务器和从服务器发送的所有信息(包括标题信息),可以使用浏览器的开发人员工具并启动网络会话,不仅可以看到发送到服务器的所有请求,而且还会看到头,主体,参数,Cookie和计时信息,如图40.11所示。
图40.11 查询字符串可以使用Add方法分析查询字符串。该方法需要 x 和 y 参数,如果这些参数是数字则相加,并以div标记返回计算结果。上一节中显示的方法 GetRequestInformation 演示了如何使用 HttpRequest 对象的 QueryString 属性访问完整的查询字符串。要访问查询字符串的部分,可以使用Query属性。以下代码片段使用Get方法访问 x 和 y 的值。如果在查询字符串中找不到相应的键,此方法将返回null(代码文件WebSampleApp/RequestAndResponseSample.cs): public static string QueryString(HttpRequest request)
{
var sb = new StringBuilder();
string xtext = request.Query["x"];
string ytext = request.Query["y"];
if (xtext == null ∥ ytext == null)
{
return"x and y must be set";
}
int x, y;
if (!int.TryParse(xtext, out x))
{
return $"Error parsing {xtext}";
}
if (!int.TryParse(ytext, out y))
{
return $"Error parsing {ytext}";
}
return $"{x} + {y} = {x + y}".Div();
}
从Query字符串返回的 IQueryCollection 还允许使用Keys属性访问所有键,它还提供了一个 ContainsKey 方法来检查指定的键是否可用。 使用URL http://localhost:5000/add?x=39&y=3 在浏览器中显示此结果: 39 + 3 = 42
编码返回用户输入的数据可能很危险。我们可以用Content方法做到这一点。以下方法直接返回通过查询数据字符串传递的数据(代码文件WebSampleApp/RequestAndResponseSample.cs): public static string Content(HttpRequest request) => request.Query["data"];
使用URL http://localhost:5000/content?data=sample 调用此方法,只返回字符串"sample"。使用相同的方法,用户还可以传递HTML内容,如 http://localhost:5000/content?data=<h1>Heading 1</h1> 是什么结果?图40.12显示了h1元素由浏览器解释,文本以标题格式显示。在某些情况下,用户希望允许这样做 - 例如,当用户(可能不是匿名用户)正在为网站编写文章时。
图40.12 在不检查用户输入的情况下,用户也可以传递诸如 http://localhost:5000/content?data=<script>alert(“hacker”);</script> 。可以使用JavaScript警报功能弹出消息框。将用户重定向到其他网站也很容易。当此用户输入存储在站点中时,一个用户可以输入这样的脚本,并且打开该页面的所有其他用户被相应地重定向。 返回用户输入的数据应始终编码。要结果有没有编码,可以使用 HtmlEncoder 类进行HTML编码,如以下代码段中所示(代码文件WebSampleApp/RequestResponseSample.cs): public static string ContentEncoded(HttpRequest request) =>
HtmlEncoder.Default.Encode(request.Query["data"]);
注意 使用 HtmlEncoder 需要NuGet包 System.Text.Encodings.Web。 运行应用程序,使用 http://localhost:5000/encoded?data=<script>alert(“hacker”);</script> 传递具有编码的相同JavaScript代码,客户端只看到JavaScript代码在浏览器中,它没有被解释(见图40.13)。
图40.13 发送的编码字符串类似于以下示例 - 字符引用小于号(<),大于号(>)和引号(“): <script>alert("hacker");</script>
表单数据不要用查询字符串将数据从用户传递到服务器,而是使用表单HTML元素。示例使用HTTP POST请求,而不是GET。使用POST请求时用户数据与请求的正文一起传递,而不是以查询字符串方式传递。 使用表单数据定义有两个请求。首先,表单通过GET请求发送到客户端,然后用户填写表单并使用POST请求提交数据。通过传递/ form路径调用的方法依次调用GetForm或ShowForm方法,具体取决于HTTP方法类型(代码文件WebSampleApp/RequestResponseSample.cs): public static string GetForm(HttpRequest request)
{
string result = string.Empty;
switch (request.Method)
{
case"GET":
result = GetForm();
break;
case"POST":
result = ShowForm(request);
break;
default:
break;
}
return result;
}
该表单创建 text1的输入元素和 Submit 按钮创建。 单击 Submit 按钮使用方法参数定义的HTTP方法调用表单的 action 方法: private static string GetForm() =>
"<form method=\"post\" action=\"form\">" +
"<input type=\"text\" name=\"text1\" />" +
"<input type=\"submit\" value=\"Submit\" />" +
"</form>";
为了读取表单数据,HttpRequest类定义了一个Form属性。 该属性返回一个IFormCollection对象,其中包含发送到服务器的表单中的所有数据: private static string ShowForm(HttpRequest request)
{
var sb = new StringBuilder();
if (request.HasFormContentType)
{
IFormCollection coll = request.Form;
foreach (var key in coll.Keys)
{
sb.Append(GetDiv(key, HtmlEncoder.Default.Encode(coll[key])));
}
return sb.ToString();
}
else return"no form".Div();
}
使用/form 链接,GET请求接收到表单(参见图40.14)。单击提交按钮时,表单与POST请求一起发送,可以看到表单数据的text1 内容(参见图40.15)。
图40.14
图40.15 Cookies要记住多个请求之间的用户数据,可以使用Cookie。将Cookie添加到 HttpResponse 对象将HTTP头中的cookie从服务器发送到客户端。默认情况下,Cookie是临时的(不存储在客户端上),如果URL是来自Cookie的同一个域,则浏览器将其发送回服务器。可以设置路径来限制浏览器返回Cookie的时间。在这种情况下,只有当它来自同一个域并且使用路径/cookies时才返回Cookie。设置Expires属性时,cookie是一个持久性cookie,因此存储在客户端上。超时后cookie将被移除。然而也无法保证Cookie不被提前删除(代码文件WebSampleApp/RequestResponseSample.cs): public static string WriteCookie(HttpResponse response)
{
response.Cookies.Append("color","red",
new CookieOptions
{
Path ="/cookies",
Expires = DateTime.Now.AddDays(1)
});
return"cookie written".Div();
}
通过读取 HttpRequest 对象可以再次读取cookie。 Cookie属性包含浏览器返回的所有Cookie: public static string ReadCookie(HttpRequest request)
{
var sb = new StringBuilder();
IRequestCookieCollection cookies = request.Cookies;
foreach (var key in cookies.Keys)
{
sb.Append(GetDiv(key, cookies[key]));
}
return sb.ToString();
}
测试Cookie,也可以使用浏览器的开发人员工具。 这些工具显示有关发送和接收的Cookie的所有信息。 发送JSON服务器返回超过HTML代码,也返回许多不同类型的数据格式,如CSS文件,图像和视频。 客户端知道它在响应头中的MIME类型的帮助下接收什么类型的数据。 方法 GetJson 从具有 Title,Publisher和Author 属性的匿名对象创建JSON字符串。 要使用JSON序列化对象,需要添加NuGet包NewtonSoft.Json,并导入命名空间NewtonSoft.Json。 JSON格式的MIME类型是application/json。 这是通过HttpResponse的ContentType属性设置的(代码文件WebSampleApp/RequestResponseSample.cs): public static string GetJson(HttpResponse response)
{
var b = new
{
Title ="Professional C# 6",
Publisher ="Wrox Press",
Author ="Christian Nagel"
};
string json = JsonConvert.SerializeObject(b);
response.ContentType ="application/json";
return json;
}
注意 要使用JsonConvert类,需要添加NuGet包Newtonsoft.Json。 以下是返回给客户端的数据。 {"Title":"Professional C# 6","Publisher":"Wrox Press",
"Author":"Christian Nagel"}
注意 第42章“ASP.NET Web API”中介绍了发送和接收JSON。 依赖注入依赖注入深深集成在ASP.NET Core中。此设计模式提供松耦合,因为服务仅用于接口。实现接口的具体类型是注入的。使用ASP.NET内置依赖注入机制,注入通过具有注入接口类型的参数的构造函数进行。 依赖注入分离服务契约和服务实现。该服务可以在不知道具体实现的情况下使用 - 只需要一个合同。这允许在单个位置替换所有使用服务的服务(例如日志记录)。 让我们通过创建自定义服务来更详细地了解依赖注入。 定义服务首先,声明示例服务的合同。通过接口定义合同可以将服务实现与其使用分离 - 例如,使用不同的实现进行单元测试(代码文件WebSampleApp/Services/ISampleService.cs): public interface ISampleService
{
IEnumerable<string> GetSampleStrings();
}
类DefaultSampleService实现接口ISampleService(代码文件WebSampleApp/Services/DefaultSampleService.cs): public class DefaultSampleService : ISampleService
{
private List<string> _strings = new List<string> {"one","two","three" };
public IEnumerable<string> GetSampleStrings() => _strings;
}
注册服务使用 AddTransient 方法(这是程序集 Microsoft.Extensions.DependencyInjection.Abstractions 在命名空间Microsoft.Extensions.DependencyInjection 中定义的 IServiceCollection 的扩展方法),DefaultSampleService 类型映射到ISampleService。 使用ISampleService接口时,DefaultSampleService类型将被实例化(代码文件WebSampleApp/Startup.cs): public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISampleService, DefaultSampleService>();
// etc.
}
内置依赖注入服务定义了几个生存期类型。AddTransient 方法每次注入服务时都会重新实例化服务。 使用AddSingleton方法,服务只被实例化一次。每次注入都使用相同的实例: services.AddSingleton <ISampleService,DefaultSampleService>();
AddInstance 方法需要实例化一个服务并将实例传递给此方法。这样就定义了服务的生命周期: var sampleService = new DefaultSampleService();
services.AddInstance<ISampleService>(sampleService);
第四种服务的生存期基于当前上下文。ASP.NET MVC 当前上下文基于HTTP请求。只要调用相同请求的操作,不同注入使用相同的实例。使用新请求,将创建一个新实例。为了定义基于上下文的生命周期,AddScoped 方法将服务契约映射到服务: services.AddScoped<ISampleService>();
注入服务服务注册后,可以注入它。在目录Controllers中创建名为HomeController的控制器类型。内置依赖注入框架会使用构造函数注入,因此定义了接收 ISampleService 接口的构造函数。方法Index接收 HttpContext 并且可以使用它来读取请求信息,并返回一个 HTTP 状态值。在实现中,ISampleService 用于从服务获取字符串。控制器添加一些HTML元素将字符串放入列表(代码文件WebSampleApp/Controllers/HomeController.cs): public class HomeController
{
private readonly ISampleService _service;
public HomeController(ISampleService service)
{
_service = service;
}
public async Task<int> Index(HttpContext context)
{
var sb = new StringBuilder();
sb.Append("<ul>");
sb.Append(string.Join("", _service.GetSampleStrings().Select(
s => $"<li>{s}</li>").ToArray()));
sb.Append("</ul>");
await context.Response.WriteAsync(sb.ToString());
return 200;
}
}
注意 此示例控制器直接返回HTML代码。 实际上最好将功能与用户界面分开,并从不同的 类 - 视图 创建HTML代码。 这种分离最好使用一个框架:ASP.NET MVC。 这个框架在第41章中解释。 调用控制器要通过依赖注入来实例化控制器,HomeController 类是用 IServiceCollection 服务注册的。 这一次不使用接口,因此只需要使用 AddTransient 方法调用具体实现服务类型(代码文件WebSampleApp/Startup.cs): public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISampleService, DefaultSampleService>();
services.AddTransient<HomeController>();
// etc.
}
包含路由信息的 Configure 方法现在已更改以检查 /home 路径。 如果表达式返回 true,HomeController 通过依赖注入通过调用注册的应用程序服务上的 GetService 方法来实例化。 IApplicationBuilder 接口定义了一个ApplicationServices 属性,返回实现 IServiceProvider 的对象。 这里可以访问已注册的所有服务。 使用这个控制器,通过传递 HttpContext 来调用Index方法。 状态代码将写入应答对象: public void Configure(IApplicationBuilder app, ILoggerFactory
loggerFactory)
{
app.Run(async (context) =>
{
// etc.
if (context.Request.Path.Value.ToLower() =="/home")
{
HomeController controller =
app.ApplicationServices.GetService<HomeController>();
int statusCode = await controller.Index(context);
context.Response.StatusCode = statusCode;
return;
}
});
// etc.
}
图40.16显示了运行 home 地址URL的应用程序时无序列表的输出
图40.16 路由使用映射前面的代码片段中,当URL的路径是 “/home”时,调用HomeController类。 没有去留意查询字符串或子文件夹。 当然,可以通过只检查字符串的一个子集来做到这一点。 但是,有一个更好的方法。 ASP.NET支持使用IApplicationBuilder 的扩展的子应用程序:Map方法。以下代码片段定义了到 /home2 路径的映射,并运行HomeController的Invoke方法(代码文件WebSampleApp/Startup.cs): public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
// etc.
app.Map("/home2", homeApp =>
{
homeApp.Run(async context =>
{
HomeController controller =
app.ApplicationServices.GetService<HomeController>();
int statusCode = await controller.Index(context);
context.Response.StatusCode = statusCode;
});
});
// etc.
}
不仅可以使用Map方法,也可以使用MapWhen。 使用以下代码段,MapWhen 管理的映射在路径以 /configuration 开头时适用。 剩余的路径写入到剩余的变量,可以用于方法调用的不同: PathString remaining;
app.MapWhen(context =>
context.Request.Path.StartsWithSegments("/configuration", out remaining),
configApp =>
{
configApp.Run(async context =>
{
// etc.
}
});
可以访问 HttpContext 的任何其他信息,例如客户端的主机信息,而不仅仅使用该路径(context.Request.Host)或已认证的用户(context.User.Identity.IsAuthenticated)。 使用中间件ASP.NET Core可以轻松创建在调用控制器之前调用的模块。它可以用于添加头信息,验证令牌,构建缓存,创建日志跟踪等。一个中间件模块在另一个之后被链接,直到所有连接的中间件类型被调用。 可以使用Visual Studio项目模板中间件类创建中间件类。使用此中间件类型,可以创建接收对下一个中间件类型的引用的构造函数。 RequestDelegate是一个委托,它接收一个HttpContext作为参数并返回一个Task。这正是Invoke方法的签名。在此方法中,您可以访问请求和响应信息。类型HeaderMiddleware向HttpContext的响应添加一个样本头。作为最后一个操作,Invoke方法调用下一个中间件模块(代码文件WebSampleApp/Middleware/HeaderMiddleware.cs): public class HeaderMiddleware
{
private readonly RequestDelegate _next;
public HeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
httpContext.Response.Headers.Add("sampleheader",
new string[] {"addheadermiddleware"});
return _next(httpContext);
}
}
为了方便配置中间件类型,扩展方法 UseHeaderMiddleware 扩展了接口 IApplicationBuilder,它调用方法UseMiddleware : public static class HeaderMiddlewareExtensions
{
public static IApplicationBuilder UseHeaderMiddleware(
this IApplicationBuilder builder) =>
builder.UseMiddleware<HeaderMiddleware>();
}
另一种中间件类型是 Heading1Middleware。 这种类型类似于以前的中间件类型,它只将 heading 1 写入响应(代码文件WebSampleApp/Middleware/Heading1Middleware.cs): public class Heading1Middleware
{
private readonly RequestDelegate _next;
public Heading1Middleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
await httpContext.Response.WriteAsync("<h1>From Middleware</h1>");
await _next(httpContext);
}
}
public static class Heading1MiddlewareExtensions
{
public static IApplicationBuilder UseHeading1Middleware(
this IApplicationBuilder builder) =>
builder.UseMiddleware<Heading1Middleware>();
}
现在轮到Startup类和Cofigure 方法工作,配置所有中间件类型。 扩展方法已经准备好调用(代码文件WebSampleApp/Startup.cs): public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
// etc.
app.UseHeaderMiddleware();
app.UseHeading1Middleware();
// etc.
}
运行应用程序时,将看到返回到客户端的标题(使用浏览器的开发人员工具),并且标题会显示在每个页面中,无论先前创建的链接是什么(参见图40.17)。
图40.17 会话状态使用中间件实现的服务是会话状态。会话状态允许服务器临时记住来自客户端的数据。会话状态本身被实现为中间件。 会话状态在用户首次从服务器请求页面时启动。当用户在服务器上保持打开页面时,会话继续保持直到超时(通常为10分钟)发生。为了在用户导航到新页面时保持服务器上的状态,可以将状态写入会话。当达到超时时,会话数据将被移除。 为了识别会话,第一次请求会创建有会话标识符的临时cookie。每次请求服务器时 cookie 从客户端返回,直到浏览器关 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论