老徐的Blog - C#
type: channel
link: https://bigger.ee/tag/C/
items: ASP.NET Framework下的HttpClient性能优化问题
items: .net下发送Email的Helper类
items: Mongodb Driver for C#/.Net的增删改查
items: .Net网站下使用Oracle数据库的环境设置
items: 使用iText5来处理PDF
items: C#多线程并发处理的问题
items: 提高WebRequest方法的并发限制
items: C# 实现的Javascript加密算法
items: 关于在IIS下的.NET网站出现应用池挂掉的Bug排查方法
items: .Net Core系列教程（五）—— Token Base身份认证

ASP.NET Framework下的HttpClient性能优化问题
type: item
date: 2020-07-22T20:09:00+08:00
description: 一、背景公司的一个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(); HttpConfigu…

.net下发送Email的Helper类
type: item
date: 2018-12-25T09:11:40+08:00
description: 一、适用场景前段时间因为业务需要自动给客户发送支持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 N…

Mongodb Driver for C#/.Net的增删改查
type: item
date: 2017-11-14T22:47:00+08:00
description: 我之前在《.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数据库的环境设置
type: item
date: 2017-10-12T09:29:00+08:00
description: 在.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.最后一步，也是重要的一步，那就是重启服务器，不重启服务器不会生效。这就可以了。

使用iText5来处理PDF
type: item
date: 2017-09-05T17:33:00+08:00
description: 项目要求，通过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 =>…

C#多线程并发处理的问题
type: item
date: 2017-05-27T14:39:00+08:00
description: 公司某业务，需要按条来处理大批量数据，大概几万条到几十万条都有可能。这个是由另外一个同事写的，写完之后测试，速度、效率都非常低。我问了下，原来是直接拿这些数据进行循环处理，之后又把有问题的数据循环入库。这就有两个瓶颈，一个是单线程处理数据，另一个就是又一次循环入库。我给出的建议是，处理数据部分多线程，处理完数据之后直接把有问题的数据入库。可是同事用不明白多线程，那好吧，我给写个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++; } }); }这就搞定了。以上。

提高WebRequest方法的并发限制
type: item
date: 2017-03-13T11:31:00+08:00
description: 在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# 实现的Javascript加密算法
type: item
date: 2017-02-05T22:41:00+08:00
description: 不打算公开的，不过转念一想，反正又没人看，就发出来吧。这是使用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 strin…

关于在IIS下的.NET网站出现应用池挂掉的Bug排查方法
type: item
date: 2017-01-16T17:14:00+08:00
description: 环境：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文件摘要页面，通过右侧“操作”菜单下的“使用仅限托管进行调试”菜单进行调试，可以定位到问题代码位置进行解决处理。当然根据问题的不同，还可以选择其他几个调试方法。以上。

.Net Core系列教程（五）—— Token Base身份认证
type: item
date: 2017-01-04T22:55:00+08:00
description: 在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; //w…