ASP.NET Core Basic: Http协议与编写Http协议程序(客户端和服务端),浏览器与Javascript的Ajax请求,自定义数据传输
前言:
写了一段时间的web程序,但是一直用的是dotnet core的框架应用,由于最近编写程序的需求,需要一些更灵活的程序传输。但是目前在net core的官方提供的框架上并没有找到更好的方案,因此今天决定从传输协议到基础传输的数据结构入手,整理清楚整个传输体系的关系。其实大学的网络课程有很清晰的描述,但是实践太少,今天不得不又复习了一次。由于以前做过javaweb的工作,对框架这种东西一直有着抵触心理(尤其是java框架泛滥),因为有时候为了学习使用别人编写的一个框架而浪费自己大量的时间,往往是得不偿失的。所以考虑到与其毫无头绪的在官方文档中查找解决方案而浪费大量的时间,不如着手从根本上解决问题。
概念:
http协议,即web的传输协议,协议包括传输过程,传输的数据结构
http程序,这里指的是用程序代码直接编写服务器和客户端,就像编写自己的tcp传输程序一样,不依托任何其他工具。
自定义数据传输,即在协议程序或者net core中如何自由控制传输和处理数据
浏览器与脚本程序Ajax,即浏览器与http协议以及脚本访问之间的关系。浏览器本身是一个http的客户端程序,对于返回的数据,浏览器会先根据header中的信息确定body中的数据是什么数据(比如根据contentype),如果是文本流,那么就直接显示,如果是文件流,那么就以下载文件的方式保存文件。对于ajax,这要说明一下。ajax可以看作是浏览器程序下的子客户端程序,这个程序可以由用户自己编写请求以及对结果的处理。这里常用的就是异步加载。
不过要说明一下,html中的图片或者视频的src下的内容也是异步加载的,不过这是由浏览器自己完成的。
关于自定义处理流的好处,便是对于大型文件或者是大型视频的加载,比如对于一个两个G的视频,不可能一直保持连接下载,这对服务器是个巨大的考验,因此可以使用流的方式,在客户端播放视频的过程中,每次加载视频文件的一部分,然后写入到缓存流或者数据提的对应位置,从而实现跳跃播放。整体过程,可以是,将服务端的大型视频文件在服务端保存的时候切割成多个可以单次传输的视频文件,然后客户端跳跃播放的时候可以加载视频文件的某一段对应的视频小文件,加载完成后用上面的方式进行拼接,或者定点播放。这里便是一个网络概念即dash,即视频串流技术。
Http协议与编写Http协议程序(客户端和服务端)
Http协议属于应用层协议,但是Http底层所使用的协议是Tcp协议,并且是无状态,即数据传输结束后立即结束连接,也就是客户端发起连接-》客户端发送请求数据-》服务端返回数据-》断开连接。Http协议的传输数据主要包含三个部分:请求行,首部行(Header),实体主体(Body);关于请求行,客户端发送到服务端的请求数据,与服务器返回到客户端的数据,这两者之间略有区别。这里重点讨论Header和Body,Header本质上就是个字符串的键值对字典,Body则是流数据。客户端可以根据Header携带的信息决定如何处理Body里面的数据比如body中的数据是html脚本代码还是下载的文件,或者可以不管Header是什么数据,直接设置传输body数据和处理body中的数据。这里注意,一个键对应的值,概念上是一个字符串数组,也就是说一个header键对应多个值。
下面是一个自定义的用于传输Http协议数据的客户端和服务端程序。这里不自己编写底层的Tcp传输程序,而是使用C#提供的Http传输程序集using System.Net.Http;,接口版本是0.0.1
客户端(充当浏览器的作用,示例中访问www.baidu.com,并输出从body中获取的字符串数据):
static async Task Main(string[] args) { Program program = new Program(); await program.GetDataSimpleAsync(); } private async Task GetDataSimpleAsync() { using (var client=new HttpClient()) { HttpResponseMessage response = await client.GetAsync("http://www.baidu.com"); //HttpResponseMessage response = await client.GetAsync("https://localhost:5001"); if (response.IsSuccessStatusCode) { Console.WriteLine($"Response Status Code:{(int)response.StatusCode} "+$"{response.ReasonPhrase}"); string responseBodyAsText = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Received payload of {responseBodyAsText.Length} characters"); Console.WriteLine(); Console.WriteLine(responseBodyAsText); } } }
客户端输出结果:
服务端程序(这里向Body中写入“Hello world”字符串):
static void Main(string[] args) { HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8085/");//设置服务端的访问链接地址 listener.Start(); HttpListenerContext context = listener.GetContext();//程序会在此处发生阻塞,直到有新的访问到达,因此可以在此处建立起循环,一直接受新的请求 HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; string responseString = string.Format("<HTML><BODY>{0}</BODY></HTML>", "Hello World");//生成http代码 byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString); response.ContentLength64 = buffer.Length; System.IO.Stream output = response.OutputStream;//从此处可知,http数据的传输本质是流,因此也可以传输文件 output.Write(buffer, 0, buffer.Length); output.Close(); listener.Stop(); }
浏览器访问输出:
net core自定义数据传输
net core便是服务端的http的数据程序,因此也可以直接设置传输的数据流。也就是说如果要返回文件数据,可以不需要UseStaticFile中间件,而是直接将任意的文件流直接写入到响应的Body中
这里有三个示例:第一个,将从客户端获取到发来的请求数据(Request),做成html字符串,写入响应的Body返回给客户端显示出来;第二个,将一个图片的数据写入到响应,在浏览器上显示出来;第三个,和第二个差不多,区别是图片以文件的形式下载,关于如何设置下载默认文件名,可查阅http的header设置文档。
程序如下
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<CustomController>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.Run(async (context) => { CustomController customController = app.ApplicationServices.GetService<CustomController>(); await customController.DealRequest(context);//使用自定义的程序处理Context }); } }
自定义的处理程序
public class CustomController { IWebHostEnvironment _env; public CustomController(IWebHostEnvironment env) { this._env = env; } public async Task DealRequest(HttpContext context) { string path = context.Request.Path.Value.ToLower(); if (path == "/testhttp" || context.Request.Path.Value.ToLower() == "/") { await ShowRequestInfo(context); }else if (path == "/image")//请求图片 { await GetImage(context); }else if(path== "/imagefile")//请求图片文件 { await GetImageFile(context); } //else if (path == "/imageview")//图片视图 //{ //} } //将Http的请求消息的信息写入到返回消息中,以在页面上显示 private async Task ShowRequestInfo(HttpContext context) { await context.Response.WriteAsync( HttpTools.GetRequestInformation(context.Request) + "<br /><br /><br />" + HttpTools.GetRequestHeaderInformation(context.Request) + "<br /><br /><br />" + HttpTools.GetRequestBodyInformation(context.Request)); } //获取图片流 private async Task GetImage(HttpContext context) { string filePath = Path.Combine(_env.ContentRootPath, "file.png"); FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read); await file.CopyToAsync(context.Response.Body); //await context.Response.Body.WriteAsync(new byte[] {1,2,3,4 });//向Body写入自定义的byte数据 } //获取图片文件 private async Task GetImageFile(HttpContext context) { context.Response.ContentType = "application/octet-stream";//通过设置ContentType为application/octet-stream,表示Body的数据是文件流 string filePath = Path.Combine(_env.ContentRootPath, "file.png"); FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read); await file.CopyToAsync(context.Response.Body); } }
辅助程序
public class HttpTools { public static string GetTag(string tag,string value) { return $"<{tag}>{value}</{tag}>"; } public static string GetDiv(string key,string value) { return $"<div><span>{key}:</span><span>{value}:</span></div>"; } public static string Div(string value) { return $"<div>{value}</div>"; } public static string Span(string value) { return $"<span>{value}</span>"; } //获取常用信息 public static string GetRequestInformation(HttpRequest request) { var sb = new StringBuilder(); sb.Append(Div("RequestInformations----------------------------------------------------")); sb.Append(GetDiv("Uri", GetAbsoluteUri(request))); 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(); } //获取绝对路径 public static string GetAbsoluteUri(HttpRequest request) { return new StringBuilder() .Append(request.Scheme) .Append("://") .Append(request.Host) .Append(request.PathBase) .Append(request.Path) .Append(request.QueryString) .ToString(); } //获取头信息 public static string GetRequestHeaderInformation(HttpRequest request) { var sb = new StringBuilder(); sb.Append(Div("RequestHeaderInformations----------------------------------------------------")); foreach (var header in request.Headers) { sb.Append(GetDiv(header.Key, string.Join(";", header.Value))); } return sb.ToString(); } //获取Body信息 public static string GetRequestBodyInformation(HttpRequest request) { var sb = new StringBuilder(); sb.Append(Div("RequestBodyInformations----------------------------------------------------")); sb.Append(Div(request.Body.ToString())); return sb.ToString(); } public static string GetResponseHeaderInformation(HttpResponse response) { var sb = new StringBuilder(); sb.Append(Div("ResponseHeaderInformations----------------------------------------------------")); foreach (var header in response.Headers) { sb.Append(GetDiv(header.Key, string.Join(";", header.Value))); } return sb.ToString(); } }
输出结果1
输出结果2
输出结果3
输入https://localhost:5001/imagefile后