一、背景公司的一个asp.net项目，需要请求Http接口，并发比较高，会经常出现接口服务无响应同时IIS连接数非常高的现象，但是在服务端观察请求只有几十个，而IIS上的总连接数达到了几万，影响了业务的正常使用。二、排查因为服务端是用C++自己写的一个Web服务，先是怀疑服务端的承载能力不够，用压测工具直接压服务端接口，吞吐量可以达到600QPS，可以满足目前的业务需求，说明瓶颈不在服务端，而在客户端。分别尝试了HttpWebRequest的同步方法和异步方法，以及HttpClient的异步方法，压测的结果显示这三种方法的性能从高到低顺序是：HttpWebRequest同步方法> HttpWebRequest异步方法>HttpClient异步方法。使用4000并发压测，HttpWebRequest同步方法吞吐量在120QPS左右，HttpWebRequest异步方法在100左右，HttpClient异步方法只有40个左右，相差非常大。三、解决1.分析要提高Http请求的效率，一般有两种方法，一是维护一个连接池，二是连接复用。HttpWebRequest自带连接池，所以在前面的测试结果中，HttpWebRequest的吞吐量比较高；而HttpWebRequest的异步方法很可能是伪异步，所以效果不是特别明显；HttpClient也只有异步方法，但是却是表现最差的，原因就在每次请求都会重新实例化，请求结束后释放资源，在实例化和释放资源上损耗大量性能。2.方案有两种方案，一是使用HttpWebRequest的同步方法，维护好连接池并调优参数，但是这对耐力是一个很大的考验，而且没有把握可以做到让人满意的结果；第二个方案就是复用HttpClient，但是对原有项目的改动比较大，但是更有把握成功。我这里选择的是复用HttpClient。3.过程1) 静态化HttpClient首先尝试静态化HttpClient来实现复用，测试效果，代码如下： public class DemoController : ApiController { private static readonly HttpClient httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost") }; public async Task<string> Post() { using (var responseData = await httpClient.PostAsync("/", new StringContent(""))) { return await responseData.Content.ReadAsStringAsync(); } } } 测试发现返回的HashCode是一致的，说明实现了复用；并发1000压测之后会发现吞吐量可以达到700左右，但是因为本机性能和没有调优的原因响应会比较慢，但是没有请求失败的情况，这说明复用之后可以有效的提高性能。图1.1000并发压测结果2) 解决不同请求时复用的问题静态化可以解决HttpClient的复用问题，但是如果这个项目中需要请求多个不同的接口地址，那么只是一个静态是解决不了的，因为HttpClient静态实例化之后，参数不能再次修改，怎么解决这个问题呢？如果是在.Net Core 2.1及以上的项目中，可以直接使用 HttpClientFactory 来实现，具体 HttpClientFactory 的使用方法后面再讲。但是在.Net Framework项目中不支持 HttpClientFactory ，而且不直接支持注入，这里我用到另一个方法来实现类似的功能。3) 解决依赖注入如果我们把每个不同的请求都定义一个静态实例，那就完全没了写代码的乐趣。要解决这个问题，就需要依赖注入来实现。在.Net Framework中微软自身没有提供依赖注入的方法，我们可以借用第三方库来完成，我这里使用Autofac来实现，前提是项目为.Net Framework 4.6.1及以上，如果低于该版本，需要先对项目进行升级。使用nuget分别安装 Autofac 、 Autofac.Integration.Mvc 和 Autofac.Integration.WebApi 这三个扩展，个别情况还需要安装 System.ValueTuple ，因环境而异。创建类 IocConfig ，代码如下： public class IocConfig { public static void RegisterDependencies() { ContainerBuilder builder = new ContainerBuilder(); HttpConfiguration config = GlobalConfiguration.Configuration; builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); HttpClient httpClient = new HttpClient { BaseAddress = new Uri("http://localhost") }; builder.Register(x=>httpClient).As<HttpClient>().SingleInstance(); var container = builder.Build(); config.DependencyResolver = new AutofacWebApiDependencyResolver(container); } } 在Global.asax中的 Application_Start 方法内，增加： //依赖注入 IocConfig.RegisterDependencies();我们去看一下注入有没有成功，在控制器的构造函数中增加要注入的内容： private static HttpClient _httpClient; public DemoController(HttpClient httpClient) { _httpClient = httpClient; }之后调用： public async Task<string> Post() { using (var responseData = await _httpClient.PostAsync("/", new StringContent(""))) { return await responseData.Content.ReadAsStringAsync(); } }之后我们测试一下，可以发现httpClient实例的HashCode保持一致，说明实现了复用，同样使用1000并发2分钟进行压测，压测结果与1) 的相差不大甚至略好于第一次，吞吐量稳定在700+，同样没有请求失败的情况。图2.依赖注入后1000并发压测结果这里成功的通过依赖注入实现了HttpClient的复用，下面我们要继续改造，对HttpClient进行封装。4) 进一步优化，对HttpClient进行封装新建类库项目 HttpService ，添加接口 IHttpService ，接口中增加一个异步Post方法： public interface IHttpService { /// /// 异步POST方法 /// /// 请求路径 /// 参数 /// 编码 /// 类型 /// <returns></returns> Task<string> PostAsync(string path, string paramsData, string charset, string contentType); }创建 HttpService 类，用来实现 IHttpService 接口： public class HttpService:IHttpService { private readonly HttpClient _client; public HttpService(HttpClient httpClient) { _client = httpClient; } /// /// 异步Post提交 /// /// /// /// /// /// <returns></returns> public async Task<string> PostAsync(string path,string paramsData,string charset,string contentType) { try { using (var responseData = await _client.PostAsync(path, new StringContent(paramsData, Encoding.GetEncoding(charset), contentType))) { return await responseData.Content.ReadAsStringAsync(); } } catch (System.Threading.ThreadAbortException e) { System.Threading.Thread.ResetAbort(); } catch (WebException e) { } catch (Exception e) { return ""; } finally { } } }修改Autofac的依赖注入配置文件中的代码： public class IocConfig { public static void RegisterDependencies() { ContainerBuilder builder = new ContainerBuilder(); HttpConfiguration config = GlobalConfiguration.Configuration; builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); HttpClient httpClient = new HttpClient { BaseAddress = new Uri("http://localhost") }; builder.Register(x => new HttpService.HttpService(httpClient)).As<HttpService.IHttpService>().SingleInstance(); var container = builder.Build(); config.DependencyResolver = new AutofacWebApiDependencyResolver(container); } }修改注入处的构造函数和调用方法，这里是直接把我们封装好的HttpService注入了进来： private readonly HttpService.IHttpService _httpService; public DemoController(HttpService.IHttpService httpService) { _httpService = httpService; } public async Task<string> Post() { return await _httpService.PostAsync("/", "", "utf-8", "text/html"); } 同样1000并发2分钟压测，吞吐量比前面的两个少了一点，稳定在680~690左右，不过同样稳定，没有请求失败的情况，性能同样很好。图3.对HttpClient进行简单封装后依赖注入，1000并发压测结果这样我们就可以复用我们封装好了的Http请求的方法了，但是如果要提前不同的接口，这里就不行了，下面继续优化。5) 多个HttpClient实例注入如果我们的项目需要请求多个不同的第三方接口，只需要改造一下 IocConfig 和注入的参数就可以了，IocConfig的修改： public class IocConfig { public static void RegisterDependencies() { ContainerBuilder builder = new ContainerBuilder(); HttpConfiguration config = GlobalConfiguration.Configuration; builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); var httpServices = new Dictionary<string, HttpService.IHttpService> { { "demo1", new HttpService.HttpService(new HttpClient(){ BaseAddress=new Uri("http://localhost")}) }, { "demo2", new HttpService.HttpService(new HttpClient(){ BaseAddress=new Uri("http://localhost:8001")}) } }; builder.RegisterInstance(httpServices).As<Dictionary<string, HttpService.IHttpService>>().SingleInstance(); var container = builder.Build(); config.DependencyResolver = new AutofacWebApiDependencyResolver(container); } }这里是分别实例化多个 HttpService.HttpService 方法，放到 Dictionary<string,HttpService.IHttpService> 中，之后把 Dictionary<string,HttpService.IHttpService> 通过控制器的构造函数注入进来，相应的我们的控制器的构造函数作如下的改动： private readonly Dictionary<string, HttpService.IHttpService> _httpServices; public DemoController(Dictionary<string, HttpService.IHttpService> httpService) { _httpServices = httpService; } public async Task<string> Post([FromUri]string key="demo1") { return await _httpServices[key].PostAsync("/", "", "utf-8", "text/html"); }测试一下，参数key分别传demo1和demo2 可以分别返回对应的接口的数据，说明我们这个方法成功了。继续压测一下，效果没有什么差别。图4.多HttpClient实例注入压测结果6) 进一步优化如果每增加一个需要调用的第三方API就改一次IocConfig，那就太不方便了，我们可以再进一步优化，把这些放到配置文件中，只需要维护配置文件就可以了。继续在第5)步的基础上进行封装，HttpService 项目中增加一个Config实体类，包含这些属性： /// /// 配置 /// public class Config { /// /// 标识 /// public string Key { get; set; } /// /// 中文名称 /// public string Name { get; set; } /// /// Http请求服务器 /// public string BaseAddress { get; set; } /// /// 超时时间 /// public int Timeout { get; set; } = 0; /// /// Http请求头 /// public Dictionary<string, string> Headers { get; set; } }创建配置文件，我这里使用json文件，放到了 App_Data 文件夹中，命名为 httpConfig.json，内容如下：[ { "Key": "demo1", "Name": "演示1接口", "BaseAddress": "http://localhost", "Headers": { "Accept": "application/json", "User-Agent": "Biz126-HttpClient", "KeepAlive": "true" }, "Timeout": 10 }, { "Key": "demo2", "Name": "演示2接口", "BaseAddress": "http://localhost:8001", "Headers": { "Accept": "application/json", "User-Agent": "Biz126-HttpClient", "KeepAlive": "true" }, "Timeout": 10 } ]新建类 HttpConfig 用来处理配置文件，代码如下： /// /// 处理配置 /// public class HttpConfig { string configPath; public HttpConfig() { configPath = string.Format("{0}{1}", HttpRuntime.AppDomainAppPath, System.Web.Configuration.WebConfigurationManager.AppSettings["httpConfig"]); } public HttpConfig(string path) { configPath = string.Format("{0}{1}", HttpRuntime.AppDomainAppPath, path); } /// /// 取链接配置 /// /// <returns></returns> public List<Config> GetConfig() { if (File.Exists(configPath)) { using (StreamReader f2 = new StreamReader(configPath, System.Text.Encoding.GetEncoding("gb2312"))) { return JsonConvert.DeserializeObject<List<Config>>(f2.ReadToEnd()); } } return new List<Config>(); } /// /// 指定链接的配置 /// /// /// <returns></returns> public Config GetConfig(string key) { var list = GetConfig(); if (list.Count > 0) { return list.Where(x => (x.Key.Equals(key, StringComparison.OrdinalIgnoreCase))).FirstOrDefault(); } return new Config(); } /// /// 生成HttpService列表 /// /// <returns></returns> public Dictionary<string, IHttpService> HttpServiceList() { var dict=new Dictionary<string, IHttpService>(); GetConfig().ForEach(x => { var httpClient = new HttpClient(); dict.Add(x.Key, new HttpService(httpClient, x)); }); return dict; } }修改 HttpService 类，在构造函数中增加配置相关的设置： /// /// 构建Http方法 /// /// HttpClient实例 /// 配置 public HttpService(HttpClient httpClient,Config config) { httpClient.BaseAddress = new Uri(config.BaseAddress); foreach (var header in config.Headers) { httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); } httpClient.Timeout = new TimeSpan(config.Timeout*10000000); httpClient.DefaultRequestHeaders.ExpectContinue = false; //关闭Expect:[100-continue]，默认为开启状态 很多旧的HTTP/1.0和HTTP/1.1应用不支持Expect头部 _client = httpClient; //忽略https证书有效性检查 if (_client.BaseAddress.Scheme.StartsWith("https", StringComparison.OrdinalIgnoreCase)) { ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult); } } /// /// 忽略证书 /// /// /// /// /// /// <returns></returns> public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { //直接确认，否则打不开 return true; }修改 IocConfig 中的注入相关方法:builder.RegisterInstance(new HttpService.HttpConfig().HttpServiceList()).As<Dictionary<string, HttpService.IHttpService>>().SingleInstance();注入点的构造函数和调用方法不变，如下： private readonly Dictionary<string, HttpService.IHttpService> _httpServices; public DemoController(Dictionary<string, HttpService.IHttpService> httpService) { _httpServices = httpService; } public async Task<string> Post([FromUri]string key="demo1") { return await _httpServices[key].PostAsync("/", "", "utf-8", "text/html"); }最后在Web.config中的AppSettings节点下，增加一项配置：<add key="httpConfig" value="/App_Data/httpConfig.json" />我们运行项目，可以看到实现了注入，两项设置都成功加载了图5.加载配置文件后进行注入以上我们就完成了整个的优化，代码我放到了github上，其中 Biz126.HttpService 是封装后的HttpClient方法， Biz126.WebDemo 是前面的每一步的演示代码， Biz126.WebUI 是最终的调用代码。Github:https://github.com/hongbai/HttpClientFactory
一、适用场景前段时间因为业务需要自动给客户发送支持HTML格式的而且带附件的Email，而且使用的还是Office365邮箱，就抽时间看了一下这方面的内容，并写了这个帮助类。该类是使用的SMTP服务发送Email，可以支持OFFICE365等邮箱，支持SSL、HTML邮件、附件等。目前未启用加密的常规发送和使用Office365邮箱发送都已经测试通过。该代码目前是使用在.net 4.0环境下，理论上.net 4.0~.net 4.7都可以使用，.net core下没有测试。二、代码1.添加引用using System.Net; using System.Net.Mail; using System.Net.Security; using System.Security.Cryptography.X509Certificates;2.公共属性： #region 公共属性 #region SMTP服务器及发信人 /// /// SMTP服务器 /// public string SmtpHost { get; set; } /// /// SMTP端口号 /// public int SmtpPort { get; set; } /// /// 是否SSL /// public bool EnableSsl { get; set; } /// /// 发信人邮箱 /// public string FromEmailAddress { get; set; } /// /// 发信人密码 /// public string FromEmailPassword { get; set; } #endregion #region 收件人信邮件内容 /// /// 收件人Email /// public string ToEmailAddress { get; set; } /// /// 抄送，多个使用英文逗号分割 /// public string CCEmailAddress { get; set; } /// /// 密送，多个使用英文逗号分割 /// public string BCCEmailAddress { get; set; } /// /// 标题 /// public string Subject { get; set; } /// /// 邮件内容 /// public string Body { get; set; } /// /// 是否HTML邮件 /// public bool IsBodyHtml { get; set; } /// /// 附件列表 /// public List<Attachment> AttachmentList { get; set; } #endregion #endregion3.实现方法 /// /// 使用HTTPS发送的时候需要使用 /// /// /// /// /// /// <returns></returns> public static bool ValidateServerCertificate(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; } public void Send() { if (EnableSsl) { //启用SSL ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate); } SmtpClient smtp = new SmtpClient(); //实例化一个SmtpClient smtp.DeliveryMethod = SmtpDeliveryMethod.Network; //将smtp的出站方式设为 Network smtp.EnableSsl = false; //smtp服务器是否启用SSL加密 smtp.Host = this.SmtpHost; //指定 smtp 服务器地址 smtp.Port = this.SmtpPort; //指定 smtp 服务器的端口，默认是25，如果采用默认端口，可省去 smtp.UseDefaultCredentials = false; //如果你的SMTP服务器不需要身份认证，则使用下面的方式，不过，目前基本没有不需要认证的了 smtp.EnableSsl = EnableSsl; smtp.Credentials = new NetworkCredential(this.FromEmailAddress, this.FromEmailPassword); //如果需要认证，则用下面的方式 MailMessage mm = new MailMessage(); //实例化一个邮件类 mm.Priority = MailPriority.High; //邮件的优先级，分为 Low, Normal, High，通常用 Normal即可 mm.From = new MailAddress(this.FromEmailAddress, "中一期货", Encoding.GetEncoding(936)); //收件人 if (!string.IsNullOrEmpty(this.ToEmailAddress)) mm.To.Add(this.ToEmailAddress); //抄送人 if (!string.IsNullOrEmpty(this.CCEmailAddress)) mm.CC.Add(this.CCEmailAddress); //密送人 if (!string.IsNullOrEmpty(this.BCCEmailAddress)) mm.Bcc.Add(this.BCCEmailAddress); mm.Subject = this.Subject; //邮件标题 mm.SubjectEncoding = Encoding.GetEncoding(936); //这里非常重要，如果你的邮件标题包含中文，这里一定要指定，否则对方收到的极有可能是乱码。 mm.IsBodyHtml = this.IsBodyHtml; //邮件正文是否是HTML格式 mm.BodyEncoding = Encoding.GetEncoding(936); //邮件正文的编码， 设置不正确， 接收者会收到乱码 mm.Body = this.Body; //邮件正文 //邮件附件 if (this.AttachmentList != null && this.AttachmentList.Count > 0) { foreach (Attachment attachment in this.AttachmentList) { mm.Attachments.Add(attachment); } } //发送邮件，如果不返回异常， 则大功告成了。 smtp.Send(mm); }要注意的是，如果SMTP服务器要求SSL的话，必须要加ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);。4.完整的代码如下：using System.Net; using System.Net.Mail; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class EmailHelper { #region 公共属性 #region SMTP服务器及发信人 /// /// SMTP服务器 /// public string SmtpHost { get; set; } /// /// SMTP端口号 /// public int SmtpPort { get; set; } /// /// 是否SSL /// public bool EnableSsl { get; set; } /// /// 发信人邮箱 /// public string FromEmailAddress { get; set; } /// /// 发信人密码 /// public string FromEmailPassword { get; set; } #endregion #region 收件人信邮件内容 /// /// 收件人Email /// public string ToEmailAddress { get; set; } /// /// 抄送，多个使用英文逗号分割 /// public string CCEmailAddress { get; set; } /// /// 密送，多个使用英文逗号分割 /// public string BCCEmailAddress { get; set; } /// /// 标题 /// public string Subject { get; set; } /// /// 邮件内容 /// public string Body { get; set; } /// /// 是否HTML邮件 /// public bool IsBodyHtml { get; set; } /// /// 附件列表 /// public List<Attachment> AttachmentList { get; set; } #endregion #endregion public EmailHelper() { } /// /// 使用HTTPS发送的时候需要使用 /// /// /// /// /// /// <returns></returns> public static bool ValidateServerCertificate(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { return true; } public void Send() { if (EnableSsl) { //启用SSL ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate); } SmtpClient smtp = new SmtpClient(); //实例化一个SmtpClient smtp.DeliveryMethod = SmtpDeliveryMethod.Network; //将smtp的出站方式设为 Network smtp.EnableSsl = false; //smtp服务器是否启用SSL加密 smtp.Host = this.SmtpHost; //指定 smtp 服务器地址 smtp.Port = this.SmtpPort; //指定 smtp 服务器的端口，默认是25，如果采用默认端口，可省去 smtp.UseDefaultCredentials = false; //如果你的SMTP服务器不需要身份认证，则使用下面的方式，不过，目前基本没有不需要认证的了 smtp.EnableSsl = EnableSsl; smtp.Credentials = new NetworkCredential(this.FromEmailAddress, this.FromEmailPassword); //如果需要认证，则用下面的方式 MailMessage mm = new MailMessage(); //实例化一个邮件类 mm.Priority = MailPriority.High; //邮件的优先级，分为 Low, Normal, High，通常用 Normal即可 mm.From = new MailAddress(this.FromEmailAddress, "中一期货", Encoding.GetEncoding(936)); //收件人 if (!string.IsNullOrEmpty(this.ToEmailAddress)) mm.To.Add(this.ToEmailAddress); //抄送人 if (!string.IsNullOrEmpty(this.CCEmailAddress)) mm.CC.Add(this.CCEmailAddress); //密送人 if (!string.IsNullOrEmpty(this.BCCEmailAddress)) mm.Bcc.Add(this.BCCEmailAddress); mm.Subject = this.Subject; //邮件标题 mm.SubjectEncoding = Encoding.GetEncoding(936); //这里非常重要，如果你的邮件标题包含中文，这里一定要指定，否则对方收到的极有可能是乱码。 mm.IsBodyHtml = this.IsBodyHtml; //邮件正文是否是HTML格式 mm.BodyEncoding = Encoding.GetEncoding(936); //邮件正文的编码， 设置不正确， 接收者会收到乱码 mm.Body = this.Body; //邮件正文 //邮件附件 if (this.AttachmentList != null && this.AttachmentList.Count > 0) { foreach (Attachment attachment in this.AttachmentList) { mm.Attachments.Add(attachment); } } //发送邮件，如果不返回异常， 则大功告成了。 smtp.Send(mm); } }三、调用方法1.处理附件： var attachs = new List<System.Net.Mail.Attachment>(); attachList.ToList().ForEach(x => { attachs.Add(new System.Net.Mail.Attachment(System.Web.HttpContext.Current.Request.MapPath(x), System.Net.Mime.MediaTypeNames.Application.Octet)); });2.实例化EmailHelper类并赋值： var email = new EmailHelper { SmtpHost = "smtp.office365.com", SmtpPort = 587, EnableSsl = true, FromEmailAddress = "yourname@example.com", FromEmailPassword = "your password", ToEmailAddress = "customer@example.com", Subject = "邮件标题", Body = "邮件内容，支持HTML", IsBodyHtml = true, AttachmentList = attachs };3.发送邮件email.Send();四、结束有问题欢迎留言。以上。
我之前在《.Net Core系列教程（三）——使用Mongodb》中，说过在.Net Core下，怎样使用Mongodb数据库，这篇文章就作为它的延伸，来说下在.Net中，怎样使用Mongodb Driver来进行最常用的增、删、改、查操作。当然，我这个的环境是在.Net Core下，在其他版本的.Net环境下，相差不大。我这实例中使用的驱动是2.4.0版的，而现在最新版本是2.4.4。闲言少叙，还是撸代码吧。先按《.Net Core系列教程（三）——使用Mongodb》文章中的方法，设置好数据库的相关配置，之后取得数据库： var db = client.GetDatabase("database");这个代码可以按照自己的实际需求来写，比如这样： var db = client.GetDatabase(MongoUrl.Create(settings.Value.MongodbConnection).DatabaseName);取得一个collection，这里以news表为例，Models.News是News的实例类： var collection = db.GetCollection<Models.News>("news");我们再准备一下具体要操作的数据： var request=new Models.News(){title:"新闻测试",body:"这里是新闻测试的内容",author:"张三","status":True};这些前提准备好了之后，再开始具体的数据库操作了1.增加操作： collection.InsertOne(request);2.修改操作： var query = new BsonDocument("_id", new ObjectId(id)); var dict = new Dictionary<string, object> { { "title",request.title}, { "body",request.body}, { "author",request.author}, { "status",request.status} }; var data = new BsonDocument(dict); collection.UpdateOne(query, new BsonDocument("$set", data));3.查询操作： //查列表 int page=1; //当前页号 int pagesize=50; //每页50条记录 BsonDocument query = new BsonDocument(){"author":"张三","status":True}; int total = Convert.ToInt32(collection.Count(query)); //数据总记录数 var list = collection.Find(query).Sort(new BsonDocument("_id", -1)).Limit(pagesize).Skip((page-1)*pagesize).ToList(); //带分页查询，按_id倒序排序 //查单条 BsonDocument query = new BsonDocument("_id", _id); var data = collection.Find(query).FirstOrDefault();4.删除操作： ObjectId _id = new ObjectId(id); collection.DeleteOne(new BsonDocument("_id", _id));以上只是其中几个比较简单的用法，其实还有很多实现方法，比如异步方法、插入多条等等，其他的等有时间再整理吧。
在.Net网站下使用Oracle，需要网站服务器安装Oracle数据库的客户端，这是目前必须要安装的。以下是在Windows Server 2008 R2服务器环境下的设置方法及注意事项，其他系统可能会略有不同。1.服务器需要安装Oracle数据库的客户端，版本为Oracle Database 11g Release 1，而且只能使用这个版本，其他版本不可行，另外需要与你的网站应用池的位数（32位或64位）一致；2.安装时，安装类型选择“运行时”；3.安装完成后，查看环境变量中，有没有把安装目录下的BIN文件夹添加到环境变量中，如：D:\app\Administrator\product\11.2.0\client_1\bin;；4.打开安装目录右键属性，切换到“安全”选项卡，编辑，选中“Authenticated Users“，把”读取和执行“权限去掉，之后再重新勾选，之后”应用“；5.最后一步，也是重要的一步，那就是重启服务器，不重启服务器不会生效。这就可以了。
项目要求，通过pdf模板，把用户提交的数据保存到一个PDF文件中。其中有文字内容，也有图片。之前选了aspose.pdf，因为抠门，不能花钱买，就从网上找的的开心版，好不容易出来点模板，结果插入图片的时候，同一页只能插入一张图片，而官方的试用版是可以正常两张的，另外字段比较多，速度比较慢，几百个字段需要一分多钟，效率很低，放弃。之后尝试iText，发现要比aspose.pdf好用的多，下面就说下用法。需要通过nuget安装iTextSharp，选第一个，版本号是5.5.12，也就是iText5版本，该版本是AGPL许可。完整代码如下：using iTextSharp.text; using iTextSharp.text.pdf; using System.IO; namespace Tools { public class iTextPdf { Document pdfDocument = new Document(); private string _path = ""; #region 内容属性 /// /// 文字 /// public string Content { get; set; } /// /// X坐标 /// public float X { get; set; } /// /// Y坐标 /// public float Y { get; set; } /// /// 页索引（从0开始） /// public int PageIndex { get; set; } /// /// 类型 /// 1-文字 /// 2-勾选（图片） /// 3-签名 /// 4-身份证正面 /// 5-身份证反而 /// public int Type { get; set; } /// /// 字体大小 /// public float textSize { get; set; } /// /// 宽度（图片使用） /// public float Width { get; set; } /// /// 高度（图片使用） /// public float Height { get; set; } #endregion public iTextPdf() { _path = string.Format("{0}/template.pdf", System.Web.HttpContext.Current.Request.MapPath(System.Web.Configuration.WebConfigurationManager.AppSettings["ConfigDir"])); } public iTextPdf(string path) { _path = path; } public void SavePdf(List<iTextPdf> pdflist,string savefile) { using (Stream inputPdfStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read)) //打开pdf模板 using (Stream outputPdfStream = new FileStream(savefile, FileMode.Create, FileAccess.Write, FileShare.None)) //创建新的pdf对象用于保存 { var reader = new PdfReader(inputPdfStream); var stamper = new PdfStamper(reader, outputPdfStream); var list = pdflist.OrderBy(x => x.PageIndex); //对页号运行排序 //FontFactory.RegisterDirectory("C:\\WINDOWS\\Fonts"); //字体目录 list.Select(x => x.PageIndex).Distinct().ToList().ForEach(page => { //先按页进行遍历，可以减少实例化pdfContentByte的次数，减少资源占用 var pdfContentByte = stamper.GetOverContent(page); Rectangle pagesize = reader.GetPageSizeWithRotation(page); float height = pagesize.Height; list.Where(x => x.PageIndex.Equals(page)).ToList().ForEach(item => { //针对每页进行要插入的内容进行遍历 switch (item.Type) { case 1: case 2: string content = item.Content; float fontsize = 12; //字号 if (!string.IsNullOrEmpty(content)) { //只处理有文字内容的 if ((2).Equals(item.Type)) { //对勾选的内容进行处理 if ("1".Equals(content)) { //标记为1的，内容换成√，按插入文字处理 content = "√"; fontsize = 16; } else { content = ""; break; } } BaseFont bfTimes = BaseFont.CreateFont(System.Web.HttpContext.Current.Request.MapPath("~/fonts/simhei.ttf"), BaseFont.IDENTITY_H, BaseFont.EMBEDDED); //注册字体，支持中文 pdfContentByte.SetColorFill(BaseColor.BLACK); pdfContentByte.SetFontAndSize(bfTimes, fontsize); //设置字体及字号 pdfContentByte.BeginText(); //开始处理文字 pdfContentByte.ShowTextAligned(Element.ALIGN_LEFT, content, item.X, height - item.Y, 0); pdfContentByte.EndText(); //文字处理达成 } break; case 4: case 5: //身份证图片 string imgpath = System.Web.HttpContext.Current.Request.MapPath(item.Content); //图片物理路径 using (Stream inputImageStream = new FileStream(imgpath, FileMode.Open, FileAccess.Read, FileShare.Read)) //打开图片 { Image image = Image.GetInstance(inputImageStream); image.SetAbsolutePosition(item.X, item.Y); //图片坐标 image.ScaleAbsolute(item.Width, item.Height); //设置图片宽度和高度 pdfContentByte.AddImage(image); } break; } }); }); stamper.Close(); //关闭同时保存 } } } }
公司某业务，需要按条来处理大批量数据，大概几万条到几十万条都有可能。这个是由另外一个同事写的，写完之后测试，速度、效率都非常低。我问了下，原来是直接拿这些数据进行循环处理，之后又把有问题的数据循环入库。这就有两个瓶颈，一个是单线程处理数据，另一个就是又一次循环入库。我给出的建议是，处理数据部分多线程，处理完数据之后直接把有问题的数据入库。可是同事用不明白多线程，那好吧，我给写个Demo吧。这里需要用到队列(Queue)和Task或Thread。我这里使用的是ConcurrentQueue，ConcurrentQueue队列是一个高效的线程安全的队列，是.Net Framework 4.0，System.Collections.Concurrent命名空间下的一个数据结构。首先，实例化一个队列，名为queue:private ConcurrentQueue<Models.Demo> queue;之后，给这个队列填充上测试用的数据：//生成测试用数据列表 for (int i = 1; i <= 50000; i++) { Models.Demo dat = new Models.Demo() { id = i, value = Libs.Rand.RndCode(8) }; queue.Enqueue(dat); Thread.Sleep(1); //这里是随机数生成时需要 }下面是处理启用多线程来处理数据了int threadcount=10; //开启10个线程 for(int i=0;i<threadcount;i++) { string filename = string.Format("task{0}.txt", i); filename = System.Web.HttpContext.Current.Request.MapPath(string.Format("~/file/{0}", filename)); Task.Factory.StartNew(() => { Models.Demo demo = new Models.Demo(); StringBuilder sb = new StringBuilder(); int j = 0; while (queue.TryDequeue(out demo)) { //处理数据 if (demo != null) sb.AppendLine(string.Format("{0}.{1}", demo.id, demo.value)); if (j % 100 == 0 || (queue.Count.Equals(0) && j<100)) { //每100条写一次文件，并暂停100毫秒 Libs.FileObj.WriteFile(filename, sb.ToString()); sb = new StringBuilder(); Thread.Sleep(100); } j++; } }); }这就搞定了。以上。
在C#中，WebRequest方法和WebClient方法，在多线程时，对并发请求数量有一个默认限制，这个限制与操作系统相关，在Windows XP/Windows 7等PC系统上默认为2个，而在Windows 2008等服务器系统中，默认为10个，也就是说在出现高并发的环境中，使用WebRequest方法来进行POST或GET请求时，最多只能同时发送10个，剩下的都在排队。出现这个问题的原因是，在HTTP 1.0和HTTP 1.1规定最大连接数就是2，就一直延用到现在。解决方法：在程序中设置：System.Net.ServicePointManager.DefaultConnectionLimit = 512; //建议最高不要超过1024，具体看使用环境是否需要或者在配置中设置：<configuration> <system.net> <connectionManagement> <add address = "http://www.google.com" maxconnection = "512" ></add> <add address = "*" maxconnection = "512" ></add> </connectionManagement> </system.net> </configuration>以上。
不打算公开的，不过转念一想，反正又没人看，就发出来吧。这是使用C#来实现的JS加密的，被加密的JS文件，代码需要写的规范，不能少了分号（;）等js中可能会被忽略的符号。下面是完整代码，自己看吧 public class JavascriptEncoder { public static string ENCODE_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$"; public bool isDebug = false; public string JsEncode(string filename, int offset) { string js = WebTools.FileObj.ReadFile(HttpContext.Current.Request.MapPath(string.Format("/tmp/{0}", filename))); int length = js.Length; js = RemoveAnnotation(js); WebTools.FileObj.WriteFile(System.Web.HttpContext.Current.Request.MapPath("tmp.js"), js); StringBuilder sb = new StringBuilder(js); sb.Replace(@"\r", " "); //\n替换为空格 sb.Replace(@"\n", " "); //\n替换为空格 sb.Replace(Environment.NewLine, " "); sb.Replace((char)13, (char)32); sb.Replace((char)10, (char)32); sb.Replace(@"\", @"\\"); //\替换为\\ sb.Replace(@"'", @"\'"); // '替换为\' js = sb.ToString(); Regex regex = new Regex(@"([\w\$]+)"); MatchCollection matches = regex.Matches(js); string element = ""; List<string> dict = new List<string>(); int index = 0; StringBuilder encscript = new StringBuilder(js); StringBuilder dicttab = new StringBuilder(); foreach (Match match in matches) { element = match.Groups[1].Value; index = dict.FindIndex(m => m.Equals(element)); if ( index < 0) { dict.Add(element); index = dict.Count - 1; } encscript.Replace(element, Base64Encode(offset + index + 1)); //regex.Replace(js,element, Base64Encode(offset + index + 1)); } foreach(string d in dict) { dicttab.AppendFormat("{0}|", d); } string dictstr = dicttab.ToString().TrimEnd('|'); //字典字符串，删除最后一个| string res = formatCode(encscript.ToString(), dictstr, dict.Count, offset); return res; } /// /// 仿Base64 /// /// /// <returns></returns> private string Base64Encode(int c) { string res=""; if (c < 0) throw new Exception("Error:Offset必须>=0."); if (c > 63) res = Base64Encode(c >> 6) + Base64Encode(c & 63); else { //为了配合appendReplacement方法的使用，将$替换为\$ res = c.Equals(63) ? @"\$" : ENCODE_BASE64[c].ToString(); } return res; } private string formatCode(string enc, string dict, int size, int offset) { StringBuilder sb = new StringBuilder(); sb.Append("/* Power by Shoresoft.Cn */\neval(function(E,I,A,D,J,K,L,H){function C(A){return A<62?String.fromCharCode(A+=A<26?65:A<52?71:-4):A<63?'_':A<64?'$':C(A>>6)+C(A&63)}while(A>0)K[C(D--)]=I[--A];function N(A){return K[A]==L[A]?A:K[A]}if(''.replace(/^/,String)){var M=E.match(J),B=M[0],F=E.split(J),G=0;if(E.indexOf(F[0]))F=[''].concat(F);do{H[A++]=F[G++];H[A++]=N(B)}while(B=M[G]);H[A++]=F[G]||'';return H.join('')}return E.replace(J,N)}("); sb.Append("'" + enc + "',"); sb.Append("'" + dict + "'.split('|'),"); sb.Append(size + "," + (size + offset) + ",/[\\w\\$]+/g, {}, {}, []))"); return sb.ToString(); } /// /// 移除块注释 /// /// /// <returns></returns> private string RemoveAnnotation(string str) { string result = str; int start = 0; int end = 0; start = str.IndexOf("/*"); end = str.IndexOf("*/"); StringBuilder sb = new StringBuilder(str); if (start >= 0 && end>start) { sb.Remove(start, end - start + 2); return RemoveAnnotation(sb.ToString()); } else return sb.ToString(); } }
关于在IIS下的.NET网站出现应用池挂掉的Bug排查方法
环境：MVC4+IIS7现象：测试的时候没有显现出来，当运行到生产环境出现大量访问量的情况下，会出现如下错误：如果应用程序池挂掉的话，可以肯定的是，代码出现了很严重的错误，导致程序挂了，原因可能是死循环之类的。而又是个别现象引起，测不出来，只能想其他的办法了。解决方法：首先，在服务器端：1.开启Windows Error Reporting Service服务2.执行下面的注册表脚本，在w3wp.exe程序挂掉的时候，自动将dump文件保存到D:\dumps文件夹中：Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\w3wp.exe] "DumpFolder"=hex(2):64,00,3a,00,5c,00,64,00,75,00,6d,00,70,00,73,00,00,00 "DumpCount"=dword:00000002 "DumpType"=dword:000000023.记录出问题的w3wp.exe程序的pid，把生成的dump文件复制到本地，进行调试。之后，在本机：1.双击从服务器上拿下来的dump文件，使用vs（2013、2015等，2010、2008没有测试过）打开，之后会显示出异常摘要等信息。2.点击右侧“操作”列中的“设置符号路径”菜单3.通过菜单“视图”——“解决方案资源管理器”，打开解决方案管理器，之后打开解决方案的属性页，选择调试源文件，之后新加一行，选择对应的源代码（项目）文件夹，之后确定保存。4.回到dump文件摘要页面，通过右侧“操作”菜单下的“使用仅限托管进行调试”菜单进行调试，可以定位到问题代码位置进行解决处理。当然根据问题的不同，还可以选择其他几个调试方法。以上。
在ASP.Net Core中实现一个Token Base身份认证，使用场景主要就是Web API下，可以调用Web API的不止是浏览器，还有各种各样的客户端，有些客户端没有Cookies，也无法使用Session。这时候就需要Token来救场了，相比Cookies，Token更开放，而安全性也要比Cookies高很多。下面使用微软JwtSecurityTokenHandler来实现一个基于beare token的身份认证。1.创建辅助类：在项目中，新建一个Auth文件夹，在Auth文件夹中添加一个RSAKeyHelper类：using System.Security.Cryptography; namespace Biz126.WebAPI.Auth { public class RSAKeyHelper { public static RSAParameters GenerateKey() { using (var key = new RSACryptoServiceProvider(2048)) { return key.ExportParameters(true); } } } }和TokenAuthOption类：using Microsoft.IdentityModel.Tokens; namespace Biz126.WebAPI.Auth { public class TokenAuthOption { public static string Audience { get; } = "ExampleAudience"; public static string Issuer { get; } = "ExampleIssuer"; public static RsaSecurityKey Key { get; } = new RsaSecurityKey(RSAKeyHelper.GenerateKey()); public static SigningCredentials SigningCredentials { get; } = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256Signature); public static TimeSpan ExpiresSpan { get; } = TimeSpan.FromMinutes(20); } }2.修改Startup.cs文件：打开Startup.cs文件，在ConfigureServices中添加： services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌) .RequireAuthenticatedUser().Build()); }); 完整的ConfigureServices方法： public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddApplicationInsightsTelemetry(Configuration); services.Configure<Biz126.Models.ConnectionStrings>(Configuration.GetSection("ConnectionStrings")); //这个是取数据库配置，与本例无关 services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌) .RequireAuthenticatedUser().Build()); }); services.AddMvc(); }修改Configure方法，在其中增加： app.UseExceptionHandler(appBuilder => { appBuilder.Use(async (context, next) => { var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature; //when authorization has failed, should retrun a json message to client if (error != null && error.Error is SecurityTokenExpiredException) { context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( //new { authenticated = false, tokenExpired = true } new { Status = false, Message = "Token已失效", Code = -401, Result = new { tokenExpired = true } } )); } //when orther error, retrun a error message json to client else if (error != null && error.Error != null) { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( //new { success = false, error = error.Error.Message } new { Status = false, Code = -500, Message = error.Error.Message } )); } //when no error, do next. else await next(); }); }); app.UseExceptionHandler(appBuilder => { appBuilder.Use(async (context, next) => { var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature; //when authorization has failed, should retrun a json message to client if (error != null && error.Error is SecurityTokenExpiredException) { context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( new { Status = false, Message = "Token已失效", Code = -401, Result = new { tokenExpired = true } } )); } //when orther error, retrun a error message json to client else if (error != null && error.Error != null) { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( new { Status = false, Code = -500, Message = error.Error.Message } )); } //when no error, do next. else await next(); }); }); //应用JwtBearerAuthentication app.UseJwtBearerAuthentication(new JwtBearerOptions { TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = TokenAuthOption.Key, ValidAudience = TokenAuthOption.Audience, ValidIssuer = TokenAuthOption.Issuer, ValidateIssuerSigningKey = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(0) } });完整的Configure方法： // This method gets called by the runtime. Use this method to configure the HTTP request pipeline public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app.UseApplicationInsightsRequestTelemetry(); app.UseApplicationInsightsExceptionTelemetry(); app.UseExceptionHandler(appBuilder => { appBuilder.Use(async (context, next) => { var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature; //when authorization has failed, should retrun a json message to client if (error != null && error.Error is SecurityTokenExpiredException) { context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( //new { authenticated = false, tokenExpired = true } new { Status = false, Message = "Token已失效", Code = -401, Result = new { tokenExpired = true } } )); } //when orther error, retrun a error message json to client else if (error != null && error.Error != null) { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( //new { success = false, error = error.Error.Message } new { Status = false, Code = -500, Message = error.Error.Message } )); } //when no error, do next. else await next(); }); }); app.UseExceptionHandler(appBuilder => { appBuilder.Use(async (context, next) => { var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature; //when authorization has failed, should retrun a json message to client if (error != null && error.Error is SecurityTokenExpiredException) { context.Response.StatusCode = 401; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( new { Status = false, Message = "Token已失效", Code = -401, Result = new { tokenExpired = true } } )); } //when orther error, retrun a error message json to client else if (error != null && error.Error != null) { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonConvert.SerializeObject( new { Status = false, Code = -500, Message = error.Error.Message } )); } //when no error, do next. else await next(); }); }); app.UseJwtBearerAuthentication(new JwtBearerOptions { TokenValidationParameters = new TokenValidationParameters { IssuerSigningKey = TokenAuthOption.Key, ValidAudience = TokenAuthOption.Audience, ValidIssuer = TokenAuthOption.Issuer, ValidateIssuerSigningKey = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(0) } }); app.UseMvc(); }3.登录授权接口在Controllers中，增加TokenAuthController的Web API控制器，在该控制器下添加如下方法： /// /// 生成Token /// /// /// /// /// /// <returns></returns> private string GenerateToken(Biz126.Customer.Models.Detail user,string serviceid,string device, DateTime expires) { var handler = new JwtSecurityTokenHandler(); ClaimsIdentity identity = new ClaimsIdentity( new GenericIdentity(user.UserName, "TokenAuth"), new[] { new Claim("ID", user._Id.ToString()), new Claim("TrueName",user.TrueName), new Claim("ServiceId",serviceid), new Claim("Device",device) //绑定硬件号 } ); var securityToken = handler.CreateToken(new SecurityTokenDescriptor { Issuer = TokenAuthOption.Issuer, Audience = TokenAuthOption.Audience, SigningCredentials = TokenAuthOption.SigningCredentials, Subject = identity, Expires = expires }); return handler.WriteToken(securityToken); }正如注释中所说的，该方法用于生成Token。还是在该控制器下，继续添加GetAuthorize方法，用于取得授权，代码如下： [HttpPost] public Biz126.Models.ResultModel GetAuthorize(Biz126.Authorization.Models.UserAuth user) { var result = new Biz126.Models.ResultModel (); var log = new Biz126.Customer.Models.Login() { UserName = user.UserName, Password = user.Password }; var login = _users.Login(log); //登录 if (login.Status) { //用户账号状态正常 //1.查看该用户有几个有效的该授权 //2.已经开通了几个 //3.在剩余范围内 //4.查看该机器是否已经有过授权 //5.如果已经有过授权，更新Token，如果没有过授权，增加新授权 var requestAt = DateTime.Now; var expiresIn = requestAt + TokenAuthOption.ExpiresSpan; //过期时间（正常为授权结束日期） var token = GenerateToken(login.Result,user.ServiceId,user.Device, expiresIn); result.Result = new { requestAt = requestAt, expiresIn = TokenAuthOption.ExpiresSpan.TotalSeconds, accessToken = token }; result.Status = true; result.Message = "success"; result.Code = 0; } else { result.Status = false; result.Message = "用户名或密码错误"; result.Code = -403; } return result; }4.验证授权：在Controllers中新建一个CheckAuthController的Web API控制器，添加下面的方法： [Authorize("Bearer")] public string Post() { var claimsIdentity = User.Identity as ClaimsIdentity; var id = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "ID").Value; var device= claimsIdentity.Claims.FirstOrDefault(c => c.Type == "Device").Value; return $"Hello! {HttpContext.User.Identity.Name}, your Device is:{device}"; }5.测试：打开Postman，先访问/api/TokenAuth/GetAuthorize，使用用户名和密码进行登录，接口会返回生成的Token，记下返回的Token。继续使用Postman工具，访问接口/api/CheckAuth，在Headers中，添加：Authorization:Bearer 上一步生成的Token，如下图所示要注意“Bearer”与后面的Token之间有一个空格，之后提交请求，可以看到验证通过并给返回相应的信息。上面代码只是一个例子，具体可以灵活的运用到自己的项目中。以上。