.Net Core 3.0后台使用httpclient请求网络网页和图片_使用Core3.0做一个简单的代理服务器
目标:使用.net core最新的3.0版本,借助httpclient和本机的host域名代理,实现网络请求转发和内容获取,最终显示到目标客户端!
背景:本人在core领域是个新手,对core的使用不多,因此在实现的过程中遇到了很多坑,在这边博客中,逐一介绍下。下面进入正文
正文:
1-启用httpclient注入:
参考文档:
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已启动"); } } } } }
继承类:
/// <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; } } } }
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]; } }
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版本是否是选择的无托管模式。
参考其他道友文章:
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个小时的博客,有点累,欢迎大家留言讨论哈,不足之处欢迎指教!