目標:使用.net core最新的3.0版本,藉助httpclient和本機的host功能變數名稱代理,實現網路請求轉發和內容獲取,最終顯示到目標客戶端! 背景:本人在core領域是個新手,對core的使用不多,因此在實現的過程中遇到了很多坑,在這邊博客中,逐一介紹下。下麵進入正文 正文: 1-啟用http ...
目標:使用.net core最新的3.0版本,藉助httpclient和本機的host功能變數名稱代理,實現網路請求轉發和內容獲取,最終顯示到目標客戶端!
背景:本人在core領域是個新手,對core的使用不多,因此在實現的過程中遇到了很多坑,在這邊博客中,逐一介紹下。下麵進入正文
正文:
1-啟用httpClient註入:
參考文檔:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.0
services.AddHttpClient("configured-inner-handler").ConfigurePrimaryHttpMessageHandler(() => { return new HttpClientHandler() { AllowAutoRedirect = false, UseDefaultCredentials = true, Proxy = new MyProxy(new Uri("你的代理Host")) }; });
這裡添加了httpClient的服務,且設置了一些其他選項:代理等
2-添加和配置接受請求的中間件:
參考文檔:1: 官網-中間件 2: ASP.NET到ASP.NET Core Http模塊的遷移
a-創建中間件:
public class DomainMappingMiddleware : BaseMiddleware { public ConfigSetting ConfigSetting { get; set; } public ILogger<DomainMappingMiddleware> Logger { get; set; } public HttpClient HttpClient = null; private static object _Obj = new object(); public DomainMappingMiddleware(RequestDelegate next, IConfiguration configuration, IMemoryCache memoryCache, ConfigSetting configSetting, ILogger<DomainMappingMiddleware> logger, IHttpClientFactory clientFactory) : base(next, configuration, memoryCache) { this.ConfigSetting = configSetting; this.Logger = logger; this.HttpClient = clientFactory.CreateClient("domainServiceClient"); } public async Task Invoke(HttpContext httpContext) { string requestUrl = null; string requestHost = null; string dateFlag = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss:fff"); requestUrl = httpContext.Request.GetDisplayUrl(); bool isExistDomain = false; bool isLocalWebsite = this.ConfigSetting.GetValue("IsLocalDomainService") == "true"; if (httpContext.Request.Query.ContainsKey("returnurl")) { requestUrl = httpContext.Request.Query["returnurl"].ToString(); requestUrl = HttpUtility.UrlDecode(requestUrl); isLocalWebsite = false; } Match match = Regex.Match(requestUrl, this.ConfigSetting.GetValue("DomainHostRegex")); if (match.Success) { isExistDomain = true; requestHost = match.Value; } #if DEBUG requestUrl = "http://139.199.128.86:444/?returnurl=https%3A%2F%2F3w.huanqiu.com%2Fa%2Fc36dc8%2F9CaKrnKnonm"; #endif if (isExistDomain) { this.Logger.LogInformation($"{dateFlag}_記錄請求地址:{requestUrl},是否存在當前域:{isExistDomain},是否是本地環境:{isLocalWebsite}"); bool isFile = false; //1-設置響應的內容類型 MediaTypeHeaderValue mediaType = null; if (requestUrl.Contains(".js")) { mediaType = new MediaTypeHeaderValue("application/x-javascript"); //mediaType.Encoding = System.Text.Encoding.UTF8; } else if (requestUrl.Contains(".css")) { mediaType = new MediaTypeHeaderValue("text/css"); //mediaType.Encoding = System.Text.Encoding.UTF8; } else if (requestUrl.Contains(".png")) { mediaType = new MediaTypeHeaderValue("image/png"); isFile = true; } else if (requestUrl.Contains(".jpg")) { mediaType = new MediaTypeHeaderValue("image/jpeg"); isFile = true; } else if (requestUrl.Contains(".ico")) { mediaType = new MediaTypeHeaderValue("image/x-icon"); isFile = true; } else if (requestUrl.Contains(".gif")) { mediaType = new MediaTypeHeaderValue("image/gif"); isFile = true; } else if (requestUrl.Contains("/api/") && !requestUrl.Contains("/views")) { mediaType = new MediaTypeHeaderValue("application/json"); } else { mediaType = new MediaTypeHeaderValue("text/html"); mediaType.Encoding = System.Text.Encoding.UTF8; } //2-獲取響應結果 if (isLocalWebsite) { //本地伺服器將請求轉發到遠程伺服器 requestUrl = this.ConfigSetting.GetValue("MyDomainAgentHost") + "?returnurl=" + HttpUtility.UrlEncode(requestUrl); } if (isFile == false) { string result = await this.HttpClient.MyGet(requestUrl); if (httpContext.Response.HasStarted == false) { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_長度{result.Length}"); //請求結果展示在客戶端,需要重新請求本地伺服器,因此需要將https轉為http result = result.Replace("https://", "http://"); //替換"/a.ico" 為:"http://www.baidu.com/a.ico" result = Regex.Replace(result, "\"\\/(?=[a-zA-Z0-9]+)", $"\"{requestHost}/"); //替換"//www.baidu.com/a.ico" 為:"http://www.baidu.com/a.ico" result = Regex.Replace(result, "\"\\/\\/(?=[a-zA-Z0-9]+)", "\"http://"); //必須有請求結果才能給內容類型賦值;如果請求過程出了異常再賦值,會報錯:The response headers cannot be modified because the response has already started. httpContext.Response.ContentType = mediaType.ToString(); await httpContext.Response.WriteAsync(result ?? ""); } else { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_圖片位元組流長度{result.Length}_Response已啟動"); } } else { byte[] result = await this.HttpClient.MyGetFile(requestUrl); if (httpContext.Response.HasStarted == false) { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_圖片位元組流長度{result.Length}"); httpContext.Response.ContentType = mediaType.ToString(); await httpContext.Response.Body.WriteAsync(result, 0, result.Length); } else { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_圖片位元組流長度{result.Length}_Response已啟動"); } } } } }View Code
繼承類:
/// <summary> /// 中間件基類 /// </summary> public abstract class BaseMiddleware { /// <summary> /// 等同於ASP.NET裡面的WebCache(HttpRuntime.Cache) /// </summary> protected IMemoryCache MemoryCache { get; set; } /// <summary> /// 獲取配置文件裡面的配置內容 /// </summary> protected IConfiguration Configuration { get; set; } public BaseMiddleware(RequestDelegate next, params object[] @params) { foreach (var item in @params) { if (item is IMemoryCache) { this.MemoryCache = (IMemoryCache)item; } else if (item is IConfiguration) { this.Configuration = (IConfiguration)item; } } } }View Code
httpClient擴展類:
public static class HttpClientSingleston { public async static Task<string> MyGet(this HttpClient httpClient, string url) { string result = null; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url)) { using (var response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { using (Stream stream = await response.Content.ReadAsStreamAsync()) { using (StreamReader streamReader = new StreamReader(stream, Encoding.UTF8)) { result = await streamReader.ReadToEndAsync(); } } } } } return result ?? ""; } public async static Task<byte[]> MyGetFile(this HttpClient httpClient, string url) { byte[] result = null; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url)) { using (var response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { result = await response.Content.ReadAsByteArrayAsync(); } } } return result ?? new byte[0]; } }View Code
b-註冊中間件:
在Startup.cs的Configure方法中:
app.UseMiddleware<DomainMappingMiddleware>();
小結:該中間件負責接受請求,並處理請求(由於項目是用來專門處理網路網頁和圖片的,因此沒有對請求的Url篩選過濾,實際使用時需要註意);該中間件即負責處理請求的轉發,又負責處理網路圖片和內容的獲取;
轉發的目的,當然是為了規避網路IP的限制,當你想訪問某一網站卻發現被禁止訪問的時候,而這時候你又有一臺可以正常訪問的伺服器且你和你的伺服器能正常連接的時候,那麼你就可以用這個方式了,做一個簡單的代理伺服器做中轉,來間接訪問我們想看的網站,是不是很神奇? 哈哈,我覺得是的,因為沒這麼乾過。
踩過的坑有:
bug0-HTTP Error 500.0 - ANCM In-Process Handler Load Failure
bug1-The response headers cannot be modified because the response has already started.
bug2-An unhandled exception was thrown by the application. IFeatureCollection has been disposed
bug3-An unhandled exception was thrown by the application. The SSL connection could not be established, see inner exception.
bug4-this request has no response data
bug5-獲取的網路圖片返回字元串亂碼
bug6-瀏覽器顯示網頁各種資源請求錯誤:IIS7.5 500 Internal Server Error
bug7-response如何添加響應頭?
bug8-如何設置在core中設置伺服器允許跨域請求?
bug9-如何在Core中使用NLog日誌記錄請求信息和錯誤?
逐一解答:
bug0:一般會在第一次在IIS上調試core項目會遇到,一般是因為電腦未安裝AspNetCoreModuleV2對IIS支持Core的模塊導致,還需要檢查項目的應用程式池的.Net Framework版本是否是選擇的無托管模式。
參考其他道友文章:https://www.cnblogs.com/leoxjy/p/10282148.html
bug1:這是因為response發送響應消息後,又修改了response的頭部的值拋出的異常,我上面列舉的代碼已經處理了該問題,該問題導致了我的大部分坑的產生,也是我遇到的最大的主要問題。這個錯誤描述很清楚,但是我從始至終的寫法並沒有在response寫入消息後,又修改response的頭部,且為了修改該問題,使用了很多輔助手段:
在發送消息前使用:if (httpContext.Response.HasStarted == false) 做判斷後再發送,結果是錯誤少了一些,但是還是有的,後來懷疑是多線程可能導致的問題,我又加上了了lock鎖,使用lock鎖和response的狀態一起判斷使用,最後是堵住了該錯誤,但是我想要的內容並沒有出現,且瀏覽器端顯示了很多bug6錯誤。
最後是在解決bug2的時候,終於在google上搜索到正確的答案:Disposed IFeatureCollection for subsequent API requests 通過左邊的文檔找到了關鍵的開髮指南: ASP.NET核心指南
通過指南發現我的一個嚴重錯誤:
a-將httpContext及其屬性(request,response等)存到了中間件的屬性中使用!!! X
b-將httpContext及其屬性(request,response等)存到了中間件的屬性中使用!!! XX
c-將httpContext及其屬性(request,response等)存到了中間件的屬性中使用!!! XXX
這個我自己挖的深坑導致我很多的錯誤!
不讓這樣用的原因主要是以為Core的特性,沒錯,就是註入,其中中間件是一個註入進來的單例模式的類,在啟動後會初始化一次構造函數,但是之後的請求就不會再執行了,因此如果把context放到單例的屬性中,結果可想而知,單例的屬性在多線程下,數據不亂才改,response在發送消息後不被再次修改才怪!!
bug2:同bug1.
bug3:不記得怎麼處理的了,可能和許可權和https請求有關,遇到在修改解決方案吧,大家也可以百度和谷歌,是能搜到的,能不能解決問題,大家去試吧。
bug4:是請求沒有響應的意思,這裡是我在獲取內容的時候使用的非同步方法,沒有使用await等待結果導致的。一般使用httpClient獲取影響內容要加上:await httpClient.SendAsync(request) ,等待結果後再做下一步處理。
bug5:獲取響應的圖片亂碼是困擾我的另一個主要問題:
初步的實現方式是:請求圖片地址,獲取響應字元,直接返回給客戶端,這肯定不行。因為你需要在response的內容類型上加上對應的類型值:
mediaType = new MediaTypeHeaderValue("image/jpeg");
httpContext.Response.ContentType = mediaType.ToString();
await httpContext.Response.WriteAsync(result ?? "")
藍後,上面雖然加了響應的內容類型依然不行,因為圖片是一種特殊的數據流,不能簡單實用字元串傳輸的方式,位元組數據在轉換的過程中可能丟失。後來在領導的項目中看到了以下發送圖片響應的方法:
//直接輸出文件 await response.SendFileAsync(physicalFileInfo);
嘗試後發現,我只能將response的響應內容讀取中字元串,怎麼直接轉成圖片文件呢? 難道我要先存下來,再通過這種方式發送出去,哎呀!物理空間有限啊,不能這麼乾,必須另想他發,百度和google搜索後都沒有找到解決方案,終於想了好久,突然發現Response對象的Body屬性是一個Stream類型,是可以直接出入位元組數據的,於是最終的解決方案出爐啦:
本解決方案獨一無二,百度谷歌獨家一份,看到就是賺到哈!!!
一段神奇的代碼產生了:await httpContext.Response.Body.WriteAsync(result, 0, result.Length);
public async static Task<byte[]> MyGetFile(this HttpClient httpClient, string url) { byte[] result = null; using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url)) { using (var response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { result = await response.Content.ReadAsByteArrayAsync(); } } } return result ?? new byte[0]; }
byte[] result = await this.HttpClient.MyGetFile(requestUrl); if (httpContext.Response.HasStarted == false) { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_圖片位元組流長度{result.Length}"); MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("image/gif"); httpContext.Response.ContentType = mediaType.ToString(); await httpContext.Response.Body.WriteAsync(result, 0, result.Length); } else { this.Logger.LogInformation($"{dateFlag}_請求結束_{requestUrl}_圖片位元組流長度{result.Length}_Response已啟動"); }
bug6:同bug1.
bug7:官網文檔給瞭解決方案,總之就是,你不要在response寫入消息後再修改response就好了。 參照官方文檔: 發送HttpContext.Response.Headers
bug8:直接上代碼吧:
在Setup.cs的ConfigService方法中添加:
services.AddCors(options => { options.AddPolicy("AllowSameDomain", builder => { //允許任何來源的主機訪問 builder.AllowAnyOrigin() .AllowAnyHeader(); }); });
在Setup.cs的Configure方法中添加:
app.UseCors();
bug9:使用NLog日誌的代碼如下:
在Program.cs其中類的方法CreateHostBuilder添加以下加粗代碼:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }).ConfigureLogging(logging => { //https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-3 logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Information); }).UseNLog();
添加Nlog的配置文件:nlog.config
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="Warn" internalLogFile="internal-nlog.txt"> <!--define various log targets--> <targets> <!--write logs to file--> <target xsi:type="File" name="allfile" fileName="${basedir}/logs/${shortdate}.log" layout="${longdate}|${logger}|${uppercase:${level}}${newline}${message} ${exception}${newline}" /> <target xsi:type="Console" name="console" layout= "${longdate}|${logger}|${uppercase:${level}}${newline}${message} ${exception}${newline}"/> </targets> <rules> <!--All logs, including from Microsoft--> <!--<logger name="*" minlevel="Trace" writeTo="allfile" />--> <!--Skip Microsoft logs and so log only own logs--> <logger name="*" minlevel="Info" writeTo="allfile" /> </rules> </nlog>
最後是給項目註入NLog的Nuget核心包引用:
使用方式是註入的方式:
public ILogger<DomainMappingMiddleware> Logger { get; set; } public HttpClient HttpClient = null; private static object _Obj = new object(); public DomainMappingMiddleware(RequestDelegate next, IConfiguration configuration, IMemoryCache memoryCache, ConfigSetting configSetting, ILogger<DomainMappingMiddleware> logger, IHttpClientFactory clientFactory) : base(next, configuration, memoryCache) { this.ConfigSetting = configSetting; this.Logger = logger; this.HttpClient = clientFactory.CreateClient("domainServiceClient"); }
this.Logger.LogInformation($"{dateFlag}_記錄請求地址:{requestUrl},是否存在當前域:{isExistDomain},是否是本地環境:{isLocalWebsite}");
3-坑說完了,最後說說怎麼繞過IP限制吧:
首先我們需要將https請求改成http請求,當然如果你的IIS支持Https可以不改;然後你需要修改本機的Host功能變數名稱解析規則,將你要繞的域指向本機IIS伺服器:127.0.0.1,不知道的小伙伴可以百度怎麼修改本機功能變數名稱解析;
IIS接收到請求後,你還需要在項目中加上功能變數名稱配置,埠號一定是80哦:
應用程式池配置:
這樣就實現了將網路請求轉到IIS中了,那麼通過IIS部署的項目接收後,使用Core3.0最新的httpClient技術將請求轉發到你的伺服器中,當然你的伺服器也需要一個項目來接收發來的請求;
最後是通過伺服器項目發送網路請求到目標網站請求真正的內容,最後再依次返回給用戶,也就是我們的瀏覽器,進行展示。。。
結束了。。。寫了2個小時的博客,有點累,歡迎大家留言討論哈,不足之處歡迎指教!