.Net Core 3.0后台使用httpclient請求網絡網頁和圖片_使用Core3.0做一個簡單的代理服務器


目標:使用.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個小時的博客,有點累,歡迎大家留言討論哈,不足之處歡迎指教!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM