C#Winform下使用WebKit、Geckofx、CefSharp对比及CefSharp代码实现
使用winform项目实现类似浏览器开发,获取页面html元数据,并实现操作可以使用以下几个组件:
浏览器 | 内核 | 兼容 | 获取cookies |
---|---|---|---|
VS自带webBrowser | IE | 最差 | 不全 |
WebKit | Firefox | 一般 | 不全 |
Geckofx | Firefox | 好 | 不全 |
CefSharp | chrome(谷歌) | 好 | 全 |
只要是项目里需要用到浏览器的强烈建议用CefSharp,而且对js支持也不错,直接可以执行js代码。
一、使用visual studio 2021自带的NuGet程序包管理器安装CefSharp
新版本visual studio2021中自带NuGet工具,在菜单栏的工具中可以看到NuGet包管理器相关菜单,下面介绍安装和使用NuGet工具方式。
1.工具→NuGet程序包管理器→程序包管理控制台
此处可以在控制台输入执行命令(如下图):
2.如果VS没有Nuget工具,就去 “工具→扩展和更新 ” 搜索nuget
如果你点击工具,没看到Nuget这些字样,请注意汉化名字为:库程序包管理器(N)
3.NuGet的使用
当你的vs已经安装了扩展NuGet后,你就可以在项目中,点击引用右键,看到右键菜单:
点击管理NuGet程序包后,搜索CefSharp,将CefSharp的所有包都安装或者下图红框里的三个安装即可
安装完成后,项目解决方案的引用中就已经引用了CefSharp,如下图所示
接下来在工具箱中也可以看到CefSharp组件了,可以拖到Form中使用了。
二、c#端编写的类向CefSharp端页面暴露功能
通过C#后边编写的类中方法可以通过向ChromiumWebBrowser组件注册的方式,在chromiumWebBrowser组件中加载的的js代码调用。
1.编写C#端用于前端调用的类CefCustomJSObject
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CefSharp.WinForms;
using System.Windows.Forms;
namespace WindowsFormsApp1.bll
{
public class CefCustomJSObject
{
// 传入主框架的ChromiumWebBrowser 实例
private static ChromiumWebBrowser _instanceBrowser = null;
public CefCustomJSObject(ChromiumWebBrowser originalBrowser)
{
_instanceBrowser = originalBrowser;
}
/// <summary>
/// 显示一个弹出对话框,前端JS调用的方法
/// </summary>
/// <param name="msg">传入参数</param>
public void showAlertMsg(string msg)
{
MessageBox.Show("您从前端js调用传入的信息是 [" + msg + "]");
}
}
}
2.向ChromiumWebBrowser组件注册暴露CefCustomJSObject类
//需要添加此句代码,否则下面执行会报错,低版本需要设置CefSharpSettings.LegacyJavascriptBindingEnabled = true;
//高版本如此设置
chromiumWebBrowser1.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
// 下面代码可以注释掉,设置WcfEnabled只影响内部通信方式
//CefSharpSettings.WcfEnabled = true;
//(bound为前端被调用的对象名称.如:bound.showAlertMsg();CefCustomJSObject为C#被暴露的Class对象,对应的js调用的方法就是BoundObject.showAlertMsg
chromiumWebBrowser1.JavascriptObjectRepository.Register("bound", new CefCustomJSObject(chromiumWebBrowser1), isAsync: false, options: BindingOptions.DefaultBinder);
3.在前端调用暴露出的类中方法
var frame = chromiumWebBrowser1.GetMainFrame();
var task = frame.EvaluateScriptAsync("(function() { bound.showAlertMsg(\"测试内容\"); })();", null);
上面代码向前端注册了一个javascript函数,函数中调用了第二步注册的bound中的showAlertMsg方法。
运行结果如下:
三、CefSharp请求资源拦截及自定义处理
通过实现IRequestHandler接口来自定义实现ChromiumWebBrowser组件每次请求的拦截处理,比如实现将一个页面内所有图片资源、css资源进行下载。
在CefSharp命名空间中定义了IRequestHandler接口,实现此接口进行处理与浏览器请求相关的事件(将在指示的线程上调用此类的方法)。
不需要自行实现IRequestHandler接口,因为在CefSharp.Handler命名空间中有一个IRequestHandler接口的默认实现类RequestHandler,我们在自定义处理时可以使用方便的基类。
1.自定义实现IRequestHandler接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CefSharp.Handler;
using CefSharp;
namespace WindowsFormsApp1.bll
{
/// <summary>
/// 自定义实现MyRequestHandler类,继承自 CefSharp.Handler.RequestHandler
/// 只实现两个方法 GetResourceRequestHandler和OnBeforeBrowse
/// </summary>
public class MyRequestHandler : RequestHandler
{
/// <summary>
/// 在一个资源请求初始化前在CEF IO线程上调用
/// </summary>
/// <param name="chromiumWebBrowser"></param>
/// <param name="browser"></param>
/// <param name="frame"></param>
/// <param name="request"></param>
/// <param name="isNavigation"></param>
/// <param name="isDownload"></param>
/// <param name="requestInitiator"></param>
/// <param name="disableDefaultHandling"></param>
/// <returns></returns>
protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
{
Console.WriteLine("GetResourceRequestHandler:"+request.Url);
return base.GetResourceRequestHandler(chromiumWebBrowser, browser, frame, request, isNavigation, isDownload, requestInitiator, ref disableDefaultHandling);
}
/// <summary>
/// 在浏览器导航前调用
/// </summary>
/// <param name="chromiumWebBrowser"></param>
/// <param name="browser"></param>
/// <param name="frame"></param>
/// <param name="request"></param>
/// <param name="userGesture"></param>
/// <param name="isRedirect"></param>
/// <returns></returns>
protected override bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect)
{
Console.WriteLine("OnBeforeBrowse" + request.Url);
return base.OnBeforeBrowse(chromiumWebBrowser, browser, frame, request, userGesture, isRedirect);
}
}
}
通过上述代码可以看出我只重写了两个方法OnBeforeBrowse和 GetResourceRequestHandler。OnBeforeBrowse方法每次载入页面url后只执行一次在浏览器导航前调用,GetResourceRequestHandler方法则在一个资源请求初始化前在CEF IO线程上调用,也就是说打开一个页面后如果页面中有多个请求资源如多个css链接、多个图片链接等,每个链接请求都会执行一次GetResourceRequestHandler方法。
如果我要将每个css资源、js资源以及图片资源自动下载下来,就可以在GetResourceRequestHandler方法中进行拦截实现自动下载。
//判断是何种资源,这段if代码块应该写在IResourceRequestHandler的return之前
if (request.Url.EndsWith("test1.js") || request.Url.EndsWith("test1.css"))
{
MessageBox.Show($@"资源拦截:{request.Url}");
string type = request.Url.EndsWith(".js") ? "js" : "css"; // 这里简单判断js还是css,不过多编写
//此处可以编写资源自动保存程序
}
GetResourceRequestHandler方法返回值类型是IResourceRequestHandler 接口类型,IResourceRequestHandler接口也在CefSharp命名空间,该接口处理与浏览器请求相关的事件,除非另有说明,否则将在 CEF IO 线程上调用此类的方法。
IResourceRequestHandler接口在CefSharp.Handler命名空间也有一个默认实现类ResourceRequestHandler,该类实现了接口中的方法,我们自定义开发时可以用此类作为基类进行派生实现。
public class MyResourceRequestHandler : ResourceRequestHandler
{
private readonly System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
protected override IResourceHandler GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request)
{
//获取post请求中请求体中参数数据
/* if (request.Url.ToLower().Contains("dologin".ToLower()))
{
using (var postData = request.PostData)
{
if (postData != null)
{
var elements = postData.Elements;
//获取请求中字符编码charset属性
var charSet = request.GetCharSet();
foreach (var element in elements)
{
//PostDataElementType枚举有三种类型Empty空类型、Bytes字节类型和File文件类型
if (element.Type == PostDataElementType.Bytes)
{
//从IPostDataElement中获取指定字符集格式的字符串
var body = element.GetBody(charSet);
Console.WriteLine(body);
}
}
}
}
}*/
// return new MyResourceHandler("");
return null;
}
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{
return new CefSharp.ResponseFilter.StreamResponseFilter(memoryStream);
}
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)
{
var bytes = memoryStream.ToArray();
memoryStream.Close();
memoryStream.Dispose();
var charSet = request.GetCharSet();
//此处修改表单内容
Encoding encoding = Encoding.UTF8;
if (charSet != null)
{
try
{
encoding = Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
}
}
string json = encoding.GetString(bytes);
Console.WriteLine(json);
JObject jObj = JObject.Parse(json);
Console.WriteLine(jObj["total"].Value<int>());
Info info=bll.JsonUtil.DeserializeObject<Info>(json);
JArray jArray = JArray.Parse(jObj["data"].ToString());
base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength);
}
}
public class Info
{
public long total { get; set; }
public int page { get; set; }
public List<Data> data{ get; set; }
}
public class Data
{
public long id { get; set; }
public string year { get; set; }
public string season { get; set; }
}
-
可以看出来个类中重写了三个方法,分别是
GetResourceHandler、
GetResourceResponseFilter、
OnResourceLoadComplete三个方法。
-
GetResourceHandler方法如果实现返回null,那么Cef会使用默认的网络加载器来发起请求,或者我们可以返回一个自定义的资源处理器ResourceHandler来处理一个合法的数据流(Stream)也就是说,对于资源的处理,要想实现自定义的处理(不是拦截,拦截到目前为止我们可以在上述的两个Handler中进行处理)我们还需要实现一个IResourceHandler接口的实例,并在GetResourceHandler处进行返回,Cef才会在进行处理的时候使用我们的Handler。
-
GetResourceResponseFilter方法,可以对response数据进行过滤、修改,新版本cef中给定了一个默认实现类CefSharp.ResponseFilter.StreamResponseFilter,这个类初始化时传入一个stream,在OnResourceLoadComplete方法中stream可以获取response中返回的数据,如果这个filter类我们可以自己继承CefSharp.IResponseFilter接口自己重写进行改进,具体代码可以参考StreamResponseFilter类的源代码。
-
OnResourceLoadComplete方法资源载入完成时执行本方法,上一个方法GetResourceResponseFilter中传入的stream在本方法中实现了response响应数据的填充,可以在本方法中读取数据;
好了,现在我们总结一下这几个接口和方法的调用关系
IRequestHandle接口有个默认实例CefSharp.Handler.RequestHandler
这个类继承后主要重写两个方法:
- GetResourceRequestHandler方法
该方法中如果返回null则使用默认处理器加载,方法中可以通过request参数中的请求相关信息,如url、post请求中的postData中数据、请求头信息等。
如果需要对response中数据进行拦截处理,则需要实现IResourceRequestHandler接口实例并返回。
此处给个小例子在GetResourceRequestHandler方法中获取post类型请求的参数:
//拦截url中含有dologin的地址
if (request.Url.ToLower().Contains("dologin".ToLower()))
{
using (var postData = request.PostData)
{
if (postData != null)
{
var elements = postData.Elements;
//获取请求中字符编码charset属性
var charSet = request.GetCharSet();
foreach (var element in elements)
{
//PostDataElementType枚举有三种类型Empty空类型、Bytes字节类型和File文件类型
if (element.Type == PostDataElementType.Bytes)
{
//从IPostDataElement中获取指定字符集格式的字符串
var body = element.GetBody(charSet);
//此处修改表单内容
Encoding encoding = Encoding.Default;
if (charSet != null)
{
try
{
encoding = Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
}
}
//转成字节数组,此处将参数截取后人为加上ss=1后重新编辑
element.Bytes = encoding.GetBytes(body + "&ss=1");
}
}
}
}
}
return null;
或者如果我们要获取某一请求的ajax结果这个方法中可以这样写:
//拦截指定的请求
if (request.Url == "http://192.168.1.221/vue/conditionDataHandler" || request.Url.ToLower() == "http://192.168.1.221/vue/conditionDataHandler".ToLower())
{
//自定义MyResourceRequestHandler类中截取response的数据
return new MyResourceRequestHandler();
}
MyResourceRequestHandler类时自己实现的CefSharp.Handler.ResourceRequestHandler类的派生类。
- OnBeforeBrowse方法
在浏览器导航前调用
IResourceRequestHandler接口有个默认实例CefSharp.Handler.ResourceRequestHandler
这个类继承后主要重写三个方法:
- GetResourceHandler方法
本方法返回一个IResourceHandler实例,实例中可以获得response参数,这里就可以截取和变造响应流数据。 - GetResourceResponseFilter方法
配合下面方法使用,返回一个IResponseFilter实例,CefSharp.ResponseFilter.StreamResponseFilter是默认实现的一个IResponseFilter实例,这个类可以通过传入一个stream获取响应数据,自定义实现IResponseFilter时可以参考StreamResponseFilter类的源码。 - OnResourceLoadComplete方法
资源加载完成时执行,此处可以获取上一个方法中stream获取响应数据。
此处给个例子,获取ajax请求数据
private readonly System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{
return new CefSharp.ResponseFilter.StreamResponseFilter(memoryStream);
}
protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength)
{
var bytes = memoryStream.ToArray();
memoryStream.Close();
memoryStream.Dispose();
var charSet = request.GetCharSet();
//此处修改表单内容
Encoding encoding = Encoding.UTF8;
if (charSet != null)
{
try
{
encoding = Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
}
}
//获取到ajax的响应结果,方法中没有区分具体url
string json = encoding.GetString(bytes);
Console.WriteLine(json);
base.OnResourceLoadComplete(chromiumWebBrowser, browser, frame, request, response, status, receivedContentLength);
}
IResourceHandler接口实现
public class MyResourceHandler : IResourceHandler
{
string _localResourceFileName;
public MyResourceHandler(string localResourceFileName)
{
this._localResourceFileName = localResourceFileName;
}
public void Cancel()
{
throw new NotImplementedException();
}
//对于通常进行资源释放的Dispose,因为我们这里只是一个Demo,所以暂时留空。
public void Dispose()
{
throw new NotImplementedException();
}
/// <summary>
/// 获取响应头信息。如果响应的数据长度未知,则设置responseLength为-1,
/// 然后CEF会一直调用ReadResponse(即将废除,实际上是Read方法)直到该Read方法返回false。
/// 如果响应数据的长度是已知的,可以直接设置responseLength长度为一个正数,
/// 然后ReadResponse(Read)将会一直调用,直到该Read方法返回false或者在已
/// 经读取的数据的字节长度达到了设置的responseLength的值。当然你也可以通过
/// 设置response.StatusCode值为重定向的值(30x)以及redirectUrl为对应的重定向Url来实现资源重定向
/// </summary>
/// <param name="response"></param>
/// <param name="responseLength"></param>
/// <param name="redirectUrl"></param>
/// <exception cref="NotImplementedException"></exception>
public void GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl)
{
//重新定向
responseLength = -1;
response.StatusCode = 301;
redirectUrl ="http://www.baidu.com";
}
/// <summary>
/// 官方注释指出,ProcessRequest将会在不久的将来弃用,改为Open。所以ProcessRequest我们直接返回true。对于Open方法,其注释告诉我们:
/// 要想要立刻进行资源处理(同步),请设置handleRequest参数为true,并返回true
/// 决定稍后再进行资源的处理(异步),设置handleRequest为false,并调用callback对应的continue和cancel方法来让请求处理继续还是取消,并且当前Open返回false。
/// 要立刻取消资源的处理,设置handleRequest为true,并返回false。
/// 也就是说,handleRequest的true或false决定是同步还是异步处理。若同步,则Cef会立刻通过Open的返回值true或false来决定后续继续进行还是取消。若为异步,则Cef会通过异步的方式来检查callback的调用情况(这里的callback实际上是要我们创建Task回调触发的)
/// </summary>
/// <param name="request"></param>
/// <param name="handleRequest"></param>
/// <param name="callback"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public bool Open(IRequest request, out bool handleRequest, ICallback callback)
{
handleRequest = true;
return true; ;
}
public bool ProcessRequest(IRequest request, ICallback callback)
{
throw new NotImplementedException();
}
/// <summary>
/// 读取响应数据。如果数据是可以立即获得的,那么可以直接将dataOut.Length
/// 长度的字节数据拷贝到dataOut这个流中,然后设置bytesRead的值为拷贝的数据
/// 字节长度值,最后再返回true。如果开发者希望继续持有dataOut的引用
/// (注释是pointer指针,但是个人觉得这里写为指向该dataOut的引用更好)
/// 然后在稍后填充该数据流,那么可以设置bytesRead为0,通过异步方式在
/// 数据准备好的时候执行callback的操作函数,然后立刻返回true。
/// (dataOut这个流会一直保持不被释放直到callback被调用为止)。
/// 为了让CEF知道当前的响应数据已经填充完毕,需要设置bytesRead为0然后返回false。
/// 要想让CEF知道响应失败,需要设置bytesRead为一个小于零的数(例如ERR_FAILED: -2),
/// 然后返回false。这个方法将会依次调用但不是在一个专有线程。
/// 根据上述的注释,总结如下:
/// bytesRead > 0,return true:填充了数据,但Read还会被调用
/// bytesRead = 0,return false:数据填充完毕,当前为最后一次调用
/// bytesRead< 0,return false:出错,当前为最后一次调用
/// bytesRead = 0,return true:CEF不会释放dataOut流,在异步调用中准备好数据后调用callback
/// </summary>
/// <param name="dataOut"></param>
/// <param name="bytesRead"></param>
/// <param name="callback"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public bool Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback)
{
throw new NotImplementedException();
}
public bool ReadResponse(Stream dataOut, out int bytesRead, ICallback callback)
{
throw new NotImplementedException();
}
public bool Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback)
{
throw new NotImplementedException();
}
这里主要看方法的注释,没有具体实现方法,这里需要强调一个场景,如果想要篡改某一请求的响应数据,可以在IResourceHandler接口的open方法或者IResponseFilter接口的Filter方法中实现,具体实现方式可以参考CefSharp.ResponseFilter.StreamResponseFilter源码。
具体open方法中如何写来引入本地html或css、js等文件,参考下面这个文章传送门
2、cefsharp官方文档
四、利用cefsharp实现向服务器发送Post请求
1、Post方式向服务器上传文件Request头及载荷格式规范
这里先讲解一下一个post请求上传文件时请求头及有效载荷中参数的书写规范,这个规范不是单指在cefsharp中准守,而是HTML 协议中multipart/form-data类型上传文件都要准守的标准规范。
具体格式如下:
- 请求头中Content-Type参数配置要求
headers.Add("Content-Type", "multipart/form-data; boundary=---------------------------245724925011072431322702593154" );
这里要重点说明一下 multipart/form-data; boundary= 部分是固定格式,等号后面的横线和数字组合是设置的分隔参数,这个参数在请求的有效载荷中用来分隔每个参数的,所以这个参数可以是:
Content-Type=multipart/form-data; boundary=---------123456abc
也可以是
Content-Type=multipart/form-data; boundary=--------**123456abc**
boundary=后面的参数可以自己设置,这里我们假设这个参数是六个横杠接两个星号接23456接两个星号,形式如下:
------**23456**
,现在设参数变量
boundary=------**23456**`
那么我们设置请求头完整格式就是
Content-Type=multipart/form-data; boundary=------**23456**
代码中因为设置了boundary变量(注意下文中黄色背景的boundary都是代表变量值是------**23456**
的变量),就可以写成
headers.Add("Content-Type", "multipart/form-data; boundary=" + boundary);
这时重点来了,此时在请求头中设置的boundary变量值,在请求载荷中是每个参数的分隔符,而且这个分隔符还有一个特定格式:
在载荷中出现时必须在Content-Type参数中设置的boundary变量值前加上两个横杠,在参数结尾时在boundary变量值后再加两个横杠,看文字不好理解,看下面的一个上传的例子:
请求头中设置
Content-Type=multipart/form-data; boundary=------**23456**
载荷中格式,注意“(boundary变量值)”对应的就是boundary变量值
--(boundary变量值)
Content-Disposition: form-data; name="id"
WU_FILE_0
--(boundary变量值)
Content-Disposition: form-data; name="name"
4F40D18130.jpg
--(boundary变量值)
Content-Disposition: form-data; name="type"
image/jpeg
--(boundary变量值)
Content-Disposition: form-data; name="lastModifiedDate"
2021/12/30 14:42:07
--(boundary变量值)
Content-Disposition: form-data; name="size"
7188
--(boundary变量值)
Content-Disposition: form-data; name="file"; filename="4F40D18130.jpg"
Content-Type: image/jpeg
(二进制文件数据)
--(boundary变量值)--
这个载荷参数有六个,每个参数用下面格式定义,name中指定参数名
Content-Disposition: form-data; name="id"
然后参数值写在参数名后面,二者之间需要空一行,然后紧跟参数分隔符。
上面载荷配置中参数id的值设置的是WU_FILE_0.
这里还需要强调一下,上传文件的参数设置需要用到下面形式来指定参数名(name)、文件名称(filename)和参数类型(Content-Type)
Content-Disposition: form-data; name="file"; filename="4F40D18130.jpg"
Content-Type: image/jpeg
整个载荷中name中指定的参数,都是后端服务器端接收请求controller中接收的参数名,文件的参数值就是二进制文件,这里参数数量根据需要自己设置。
2、cefsharp实现向服务器发送Post及实现文件上传
这里先说一下,使用cefsharp发送Post时,有几个坑需要躲避,我在这个坑里卡了一个礼拜,死活调试不出来原因。注意看代码中注释,尽量把有坑的地方用注释写出来。
下面写一个文件上传的代码
string filePathName =string.Empty;//图片路径
string fileName=string.Empty;//图片完整名称
int len = 0;
using (OpenFileDialog ofd = new OpenFileDialog())
{
//设置打开文件类型
ofd.Filter = "图片文件(*.jpg)|*.jpg|所有图片(*.*)|*.*";
if (ofd.ShowDialog(this) == DialogResult.OK)
{
//FileName就是要打开的文件路径
filePathName = ofd.FileName;
//SafeFileName就是要打开的文件名称(不含路径)
fileName = ofd.SafeFileName;
// Console.WriteLine(ofd.SafeFileName);
//Console.WriteLine(ofd.FileName);
}
}
//获取主浏览器框架,因为通过链接打开浏览器时可能会开新窗口,
//我们要在主窗口上开新请求
IFrame frame = chromiumWebBrowser1.GetMainFrame();
//创建请求
IRequest request = frame.CreateRequest();
//配置上传地址
request.Url = "http://192.168.1.110:8080/Index";
request.Method = "POST";
//初始化设置请求的postData参数
request.InitializePostData();
var element = request.PostData.CreatePostDataElement();
//获取请求头中"Content-Type=multipart/form-data; boundary="中boundary参数
string boundary ="------**23456**";
//内存流,需要将所有参数分别写入内存流然后统一生成字节数组
MemoryStream memStream = new MemoryStream();
//将文件读取
try
{
//文件流通过前面选中的图片或文件的路径读取文件到字节数组
using (FileStream fs = new FileStream(filePathName, FileMode.Open))
{
//FileMode.Open开启被读文件的内容,将文件内容存放到fs
byte[] bArr = new byte[fs.Length];
fs.Read(bArr, 0, bArr.Length);
//二进制数据参数结尾时boundary参数后面多两个--,这个要注意
//分隔符结尾行字节数组,注意前后各加两个--
byte[] boundarys = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--");
//读取除文件参数最后生成字节数组
StringBuilder sb = new StringBuilder();
//注意每个分隔符前需要加两个--
sb.Append("--" + boundary);
//换行
sb.Append("\r\n");
//配置参数名
sb.Append("Content-Disposition: form-data; name=\"id\"");
//换行后在换行实现一个空行
sb.Append("\r\n");
sb.Append("\r\n");
//设置参数值
sb.Append("WU_FILE_0");
//换行
sb.Append("\r\n");
//在配置分隔符,注意前面需要加两个--
sb.Append("--" + boundary);
sb.Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"name\"");
sb.Append("\r\n");
sb.Append("\r\n");
sb.Append(fileName);
sb.Append("\r\n");
sb.Append("--" + boundary);
sb.Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"type\"");
sb.Append("\r\n");
sb.Append("\r\n");
sb.Append("image/jpeg");
sb.Append("\r\n");
sb.Append("--" + boundary);
sb.Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"lastModifiedDate\"");
sb.Append("\r\n");
sb.Append("\r\n");
sb.Append(DateTime.Now.ToString());
sb.Append("\r\n");
sb.Append("--" + boundary);
sb.Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"size\"");
sb.Append("\r\n");
sb.Append("\r\n");
sb.Append(bArr.Length);
sb.Append("\r\n");
sb.Append("--" + boundary);
sb.Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"");
sb.Append("\r\n");
sb.Append("Content-Type: image/jpeg");
//上传文件参数配置完后,也需要空一行才能上传文件二进制,所以需要两个换行符
sb.Append("\r\n");
sb.Append("\r\n");
//第一段参数转成字节
byte[] firstarr = Encoding.UTF8.GetBytes(sb.ToString());
//将三段参数分别按照顺序写入内存流
memStream.Write(firstarr, 0, firstarr.Length);//写入前段参数
memStream.Write(bArr, 0, bArr.Length);//写入文件二进制
memStream.Write(boundarys, 0, boundarys.Length);//写入结尾参数
}
}
catch (IOException ex)
{
MessageBox.Show("错误消息:" + ex.Message, "IOException异常");
}
//将内存流数据读取到字节数组
byte[] mems = memStream.ToArray();
//将载荷字节数组赋值给element.Bytes
element.Bytes = mems;
memStream.Close();
memStream.Dispose();
request.PostData.AddElement(element);
NameValueCollection headers = new NameValueCollection();
//上传文件主要配置下面参数,其他请求头也可以配置但一定不能配置Content-Length,配置就上传失败
headers.Add("Content-Type", "multipart/form-data; boundary=" + boundary);
request.Headers = headers;
//允许随请求一起发送 Cookie,不设置默认就不发送cookie去服务器
request.Flags = UrlRequestFlags.AllowStoredCredentials;
frame.LoadRequest(request);
这里有几个炕要强调一下:
1、设置请求头时千万不要设置Content-Length,配置就上传失败,官方给的例子里就设置了一个Content-Type参数,所以我就设置了这个参数,按照测试来看除了Content-Length参数不能设置,其他的参数设置了也不会出错。
2、第二个坑说明一下,如果自己在cefsharp浏览器上进行上传文件,打断点进行调试,会发现request.PostData.Elements中存在多个element ,而且有一个指向上传文件的element的file属性写的是上传文件地址,但是如果我们要是这样模仿去写就会报错。
下面写法就不对就是在线一下错误提示(例子还是上面那个上传代码,自己改造了一下,部分代码就隐藏了能看出来我创建了三个element):
IFrame frame = chromiumWebBrowser1.GetMainFrame();
IRequest request = frame.CreateRequest();
request.Url = "http://192.168.1.110:8080/Index";
request.Method = "POST";
//初始化设置请求的postData参数
request.InitializePostData();
var element = request.PostData.CreatePostDataElement();
var element1 = request.PostData.CreatePostDataElement();
var element2 = request.PostData.CreatePostDataElement();
.............
byte[] firstarr = Encoding.UTF8.GetBytes(sb.ToString());
element.Bytes = firstarr;//将载荷中文件之前的参数字节数组赋值
element1.File = filePathName;//将文件地址路径赋值
element2.Bytes = boundarys;//分隔符结尾字节数组
request.PostData.AddElement(element);
request.PostData.AddElement(element1);
request.PostData.AddElement(element2);
...................................
//允许随请求一起发送 Cookie
request.Flags = UrlRequestFlags.AllowStoredCredentials;
frame.LoadRequest(request);
代码运行控制台中就会报错:
[0111/154340.113:ERROR:bad_message.cc(29)] Terminating renderer for bad IPC message, reason 170
所以只能按照上面那个正确的例子自己将所有参数字节数组分批次写入内存流中,然后生成一个字节数组赋值给postdata中的element.Bytes.
上面的例子是向服务器上传文件了,下面这个例子是向服务器post提交参数数据
这里再强调一句上传文件时需要设置请求头
Content-Type=multipart/form-data; boundary=---------123456abc
正常提交参数需要设置请求头
Content-Type=application/x-www-form-urlencoded
下面给出一个向服务器提交参数的post请求
IFrame frame = chromiumWebBrowser1.GetMainFrame();
string url = frame.Url;
IRequest request = frame.CreateRequest();
request.Url = "http://192.168.1.110:8080/Data";
request.Method = "POST";
//初始化设置请求的postData参数
request.InitializePostData();
StringBuilder sb = new StringBuilder();
//设置参数值,格式就是aa=1&bb=2&cc=3这种格式
//但是如果存在特殊字符就必须要用System.Web.HttpUtility.UrlEncode去编码
//下面参数配置就是编码过得所以存在好多%
sb.Append("goodsInfo%5BcoverImgUrl%5D="+ imgsrc + "&goodsInfo%5Bname%5D="+UserInfo.UrlEncode(name)+"&goodsInfo%5Bprice%5D="+price+"&goodsInfo%5Bprice2%5D=&goodsInfo%5BpriceType%5D=1&goodsInfo%5Burl%5D="+ UserInfo.UrlEncode(path) +"&goodsInfo%5BgoodsId%5D=&goodsInfo%5BthirdPartyAppid%5D=&goodsInfo%5BisThirdType%5D=0&isAudit=true");
//Console.WriteLine(UserInfo.UrlDecode(sb.ToString()));
//此处直接调用AddData添加参数值,上传文件时就不能这么设置
request.PostData.AddData(sb.ToString());
NameValueCollection headers = new NameValueCollection();
//配置请求头,注意千万不能配置Content-Length,配置请求失败
headers.Add("Content-Type", "application/x-www-form-urlencoded");
request.Headers = headers;
//允许随请求一起发送 Cookie
request.Flags = UrlRequestFlags.AllowStoredCredentials;
frame.LoadRequest(request);
最后在强调一下使用frame.CreateRequest();
方法建立请求时是不能跨域的,只能在当前打开的域名下开展新的请求。
五、介绍一个WinForm跨线程进行更新UI的方法
这里有个例子,用到的都是前面已经讲过的知识点,我首先打开了一个页面,然后先上传了一个文件,上传成功后我在 ResourceRequestHandler接口的自定义实现类的GetResourceResponseFilter方法配合OnResourceLoadComplete方法能够拿到响应结果(参考3.1章节ajax拿响应值那个小结),然后这个响应结果是异步非UI线程执行的,需要同步到主线程,然后再用这个结果重新post请求传给服务器去增加档案。
这里就介绍一个WinForm跨线程进行更新UI的方法。
- 首先在类中声明
// UI线程的同步上下文
SynchronizationContext m_SyncContext = null;
- 在构造函数中取得UI线程同步上下文
//获取UI线程同步上下文
m_SyncContext = SynchronizationContext.Current;
- 在非UI线程中调用UI线程的方法并传值
//在线程中更新UI(通过UI线程同步上下文m_SyncContext)
m_SyncContext.Post(addImageCompleted, arg);
- 在UI线程中声明addImageCompleted方法,方法名称可以自定义,然后方法签名是固定的,必须有一个object参数
//方法签名必须要有一个object参数
private void addImageCompleted(object arg)
{
//arg参数就是上一步中传入的参数,如果是多参数需要单独定义类来存储
string str = arg.ToString();//假设传入的是字符串
//下面就可以将获取到的参数在当前UI线程使用
}