Asp.Net Core 快速邮件队列设计与实现
发送邮件几乎是软件系统中必不可少的功能,在Asp.Net Core 中我们可以使用MailKit发送邮件,MailKit发送邮件比较简单,网上有许多可以参考的文章,但是应该注意附件名长度,和附件名不能出现中文的问题,如果你遇到了这样的问题可以参考我之前写的这篇博客Asp.Net Core MailKit 完美附件(中文名、长文件名)。
在我们简单搜索网络,并成功解决了附件的问题之后,我们已经能够发送邮件啦!不过另一个问题显现出来——发送邮件太慢了,没错,在我使用QQ邮箱发送时,单封邮件发送大概要用1.5秒左右,用户可能难以忍受请求发生1.5秒的延迟。
所以,我们必须解决这个问题,我们的解决办法就是使用邮件队列来发送邮件
设计邮件队列
Ok, 第一步就是规划我们的邮件队列有什么
EmailOptions
我们得有一个邮件Options类,来存储邮件相关的选项
/// <summary>
/// 邮件选项
/// </summary>
public class EmailOptions
{
public bool DisableOAuth { get; set; }
public string DisplayName { get; set; }
public string Host { get; set; } // 邮件主机地址
public string Password { get; set; }
public int Port { get; set; }
public string UserName { get; set; }
public int SleepInterval { get; set; } = 3000;
...
SleepInterval
是睡眠间隔,因为目前我们实现的队列是进程内的独立线程,发送器会循环读取队列,当队列是空的时候,我们应该让线程休息一会,不然无限循环会消耗大量CPU资源
然后我们还需要的就是 一个用于存储邮件的队列,或者叫队列提供器,总之我们要将邮件存储起来。以及一个发送器,发送器不断的从队列中读取邮件并发送。还需要一个邮件写入工具,想要发送邮件的代码使用写入工具将邮件转储到队列中。
那么我们设计的邮件队列事实上就有了三个部分:
- 队列存储提供器(邮件的事实存储)
- 邮件发送机 (不断读取队列中的邮件,并发送)
- 邮件服务 (想法送邮件时,调用邮件服务,邮件服务会将邮件写入队列)
队列存储提供器设计
那么我们设计的邮件队列提供器接口如下:
public interface IMailQueueProvider
{
void Enqueue(MailBox mailBox);
bool TryDequeue(out MailBox mailBox);
int Count { get; }
bool IsEmpty { get; }
...
四个方法,入队、出队、队列剩余邮件数量、队列是否是空,我们对队列的基本需求就是这样。
MailBox是对邮件的封装,并不复杂,稍后会介绍到
邮件服务设计
public interface IMailQueueService
{
void Enqueue(MailBox box);
对于想要发送邮件的组件或者代码部分来讲,只需要将邮件入队,这就足够了
邮件发送机(兼邮件队列管理器)设计
public interface IMailQueueManager
{
void Run();
void Stop();
bool IsRunning { get; }
int Count { get; }
启动队列,停止队列,队列运行中状态,邮件计数
现在,三个主要部分就设计好了,我们先看下MailBox
,接下来就去实现这三个接口
MailBox
MailBox 如下:
public class MailBox
{
public IEnumerable<IAttachment> Attachments { get; set; }
public string Body { get; set; }
public IEnumerable<string> Cc { get; set; }
public bool IsHtml { get; set; }
public string Subject { get; set; }
public IEnumerable<string> To { get; set; }
...
这里面没什么特殊的,大家一看便能理解,除了IEnumerable<IAttachment> Attachments { get; set; }
。
附件的处理
在发送邮件中最复杂的就是附件了,因为附件体积大,往往还涉及非托管资源(例如:文件),所以附件处理一定要小心,避免留下漏洞和bug。
在MailKit中附件实际上是流Stream
,例如下面的代码:
attachment = new MimePart(contentType)
{
Content = new MimeContent(fs),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
};
其中new MimeContent(fs)
是创建的Content,fs是Stream
,MimeContent的构造函数如下:
public MimeContent(Stream stream, ContentEncoding encoding = ContentEncoding.Default)
所以我们的设计的附件是基于Stream
的。
一般情况附件是磁盘上的文件,或者内存流MemoryStream
或者 byte[]数据。附件需要实际的文件的流Stream
和一个附件名,所以附件接口设计如下:
public interface IAttachment : IDisposable
{
Stream GetFileStream();
string GetName();
那么我们默认实现了两中附件类型 物理文件附件和内存文件附件,byte[]数据可以轻松的转换成 内存流,所以没有写这种
MemoryStreamAttechment
public class MemoryStreamAttechment : IAttachment
{
private readonly MemoryStream _stream;
private readonly string _fileName;
public MemoryStreamAttechment(MemoryStream stream, string fileName)
{
_stream = stream;
_fileName = fileName;
}
public void Dispose()
=> _stream.Dispose();
public Stream GetFileStream()
=> _stream;
public string GetName()
=> _fileName;
内存流附件实现要求在创建时传递一个 MemoryStream和附件名称,比较简单
物理文件附件
public class PhysicalFileAttachment : IAttachment
{
public PhysicalFileAttachment(string absolutePath)
{
if (!File.Exists(absolutePath))
{
throw new FileNotFoundException("文件未找到", absolutePath);
}
AbsolutePath = absolutePath;
}
private FileStream _stream;
public string AbsolutePath { get; }
public void Dispose()
{
_stream.Dispose();
}
public Stream GetFileStream()
{
if (_stream == null)
{
_stream = new FileStream(AbsolutePath, FileMode.Open);
}
return _stream;
}
public string GetName()
{
return System.IO.Path.GetFileName(AbsolutePath);
...
这里,我们要注意的是创建FileStream的时机,是在请求GetFileStream
方法时,而不是构造函数中,因为创建FileStream
FileStream会占用文件,如果我们发两封邮件使用了同一个附件,那么会抛出异常。而写在GetFileStream
方法中相对比较安全(除非发送器是并行的)
实现邮件队列
在我们这篇文章中,我们实现的队列提供器是基于内存的,日后呢我们还可以实现其它的基于其它存储模式的,比如数据库,外部持久性队列等等,另外基于内存的实现不是持久的,一旦程序崩溃。未发出的邮件就会boom然后消失 XD...
邮件队列提供器IMailQueueProvider
实现
代码如下:
public class MailQueueProvider : IMailQueueProvider
{
private static readonly ConcurrentQueue<MailBox> _mailQueue = new ConcurrentQueue<MailBox>();
public int Count => _mailQueue.Count;
public bool IsEmpty => _mailQueue.IsEmpty;
public void Enqueue(MailBox mailBox)
{
_mailQueue.Enqueue(mailBox);
}
public bool TryDequeue(out MailBox mailBox)
{
return _mailQueue.TryDequeue(out mailBox);
}
本文的实现是一个 ConcurrentQueue ,这是为了避免资源竞争带来问题,写入队列和出队不在同一个线程中
邮件服务IMailQueueService
实现
代码如下:
public class MailQueueService : IMailQueueService
{
private readonly IMailQueueProvider _provider;
/// <summary>
/// 初始化实例
/// </summary>
/// <param name="provider"></param>
public MailQueueService(IMailQueueProvider provider)
{
_provider = provider;
}
/// <summary>
/// 入队
/// </summary>
/// <param name="box"></param>
public void Enqueue(MailBox box)
{
_provider.Enqueue(box);
}
这里,我们的服务依赖于IMailQueueProvider
,使用了其入队功能
邮件发送机IMailQueueManager
实现
这个相对比较复杂,我们先看下完整的类,再逐步解释:
public class MailQueueManager : IMailQueueManager
{
private readonly SmtpClient _client;
private readonly IMailQueueProvider _provider;
private readonly ILogger<MailQueueManager> _logger;
private readonly EmailOptions _options;
private bool _isRunning = false;
private bool _tryStop = false;
private Thread _thread;
/// <summary>
/// 初始化实例
/// </summary>
/// <param name="provider"></param>
/// <param name="options"></param>
/// <param name="logger"></param>
public MailQueueManager(IMailQueueProvider provider, IOptions<EmailOptions> options, ILogger<MailQueueManager> logger)
{
_options = options.Value;
_client = new SmtpClient
{
// For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)
ServerCertificateValidationCallback = (s, c, h, e) => true
};
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
if (_options.DisableOAuth)
{
_client.AuthenticationMechanisms.Remove("XOAUTH2");
}
_provider = provider;
_logger = logger;
}
/// <summary>
/// 正在运行
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// 计数
/// </summary>
public int Count => _provider.Count;
/// <summary>
/// 启动队列
/// </summary>
public void Run()
{
if (_isRunning || (_thread != null && _thread.IsAlive))
{
_logger.LogWarning("已经运行,又被启动了,新线程启动已经取消");
return;
}
_isRunning = true;
_thread = new Thread(StartSendMail)
{
Name = "PmpEmailQueue",
IsBackground = true,
};
_logger.LogInformation("线程即将启动");
_thread.Start();
_logger.LogInformation("线程已经启动,线程Id是:{0}", _thread.ManagedThreadId);
}
/// <summary>
/// 停止队列
/// </summary>
public void Stop()
{
if (_tryStop)
{
return;
}
_tryStop = true;
}
private void StartSendMail()
{
var sw = new Stopwatch();
try
{
while (true)
{
if (_tryStop)
{
break;
}
if (_provider.IsEmpty)
{
_logger.LogTrace("队列是空,开始睡眠");
Thread.Sleep(_options.SleepInterval);
continue;
}
if (_provider.TryDequeue(out MailBox box))
{
_logger.LogInformation("开始发送邮件 标题:{0},收件人 {1}", box.Subject, box.To.First());
sw.Restart();
SendMail(box);
sw.Stop();
_logger.LogInformation("发送邮件结束标题:{0},收件人 {1},耗时{2}", box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "循环中出错,线程即将结束");
_isRunning = false;
}
_logger.LogInformation("邮件发送线程即将停止,人为跳出循环,没有异常发生");
_tryStop = false;
_isRunning = false;
}
private void SendMail(MailBox box)
{
if (box == null)
{
throw new ArgumentNullException(nameof(box));
}
try
{
MimeMessage message = ConvertToMimeMessage(box);
SendMail(message);
}
catch (Exception exception)
{
_logger.LogError(exception, "发送邮件发生异常主题:{0},收件人:{1}", box.Subject, box.To.First());
}
finally
{
if (box.Attachments != null && box.Attachments.Any())
{
foreach (var item in box.Attachments)
{
item.Dispose();
}
}
}
}
private MimeMessage ConvertToMimeMessage(MailBox box)
{
var message = new MimeMessage();
var from = InternetAddress.Parse(_options.UserName);
from.Name = _options.DisplayName;
message.From.Add(from);
if (!box.To.Any())
{
throw new ArgumentNullException("to必须含有值");
}
message.To.AddRange(box.To.Convert());
if (box.Cc != null && box.Cc.Any())
{
message.Cc.AddRange(box.Cc.Convert());
}
message.Subject = box.Subject;
var builder = new BodyBuilder();
if (box.IsHtml)
{
builder.HtmlBody = box.Body;
}
else
{
builder.TextBody = box.Body;
}
if (box.Attachments != null && box.Attachments.Any())
{
foreach (var item in GetAttechments(box.Attachments))
{
builder.Attachments.Add(item);
}
}
message.Body = builder.ToMessageBody();
return message;
}
private void SendMail(MimeMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
try
{
_client.Connect(_options.Host, _options.Port, false);
// Note: only needed if the SMTP server requires authentication
if (!_client.IsAuthenticated)
{
_client.Authenticate(_options.UserName, _options.Password);
}
_client.Send(message);
}
finally
{
_client.Disconnect(false);
}
}
private AttachmentCollection GetAttechments(IEnumerable<IAttachment> attachments)
{
if (attachments == null)
{
throw new ArgumentNullException(nameof(attachments));
}
AttachmentCollection collection = new AttachmentCollection();
List<Stream> list = new List<Stream>(attachments.Count());
foreach (var item in attachments)
{
var fileName = item.GetName();
var fileType = MimeTypes.GetMimeType(fileName);
var contentTypeArr = fileType.Split('/');
var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);
MimePart attachment = null;
Stream fs = null;
try
{
fs = item.GetFileStream();
list.Add(fs);
}
catch (Exception ex)
{
_logger.LogError(ex, "读取文件流发生异常");
fs?.Dispose();
continue;
}
attachment = new MimePart(contentType)
{
Content = new MimeContent(fs),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
};
var charset = "UTF-8";
attachment.ContentType.Parameters.Add(charset, "name", fileName);
attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName);
foreach (var param in attachment.ContentDisposition.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
foreach (var param in attachment.ContentType.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
collection.Add(attachment);
}
return collection;
}
}
在构造函数中请求了另外三个服务,并且初始化了SmtpClient
(这是MailKit中的)
public MailQueueManager(
IMailQueueProvider provider,
IOptions<EmailOptions> options,
ILogger<MailQueueManager> logger)
{
_options = options.Value;
_client = new SmtpClient
{
// For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)
ServerCertificateValidationCallback = (s, c, h, e) => true
};
// Note: since we don't have an OAuth2 token, disable
// the XOAUTH2 authentication mechanism.
if (_options.DisableOAuth)
{
_client.AuthenticationMechanisms.Remove("XOAUTH2");
}
_provider = provider;
_logger = logger;
}
启动队列时创建了新的线程,并且将线程句柄保存起来:
public void Run()
{
if (_isRunning || (_thread != null && _thread.IsAlive))
{
_logger.LogWarning("已经运行,又被启动了,新线程启动已经取消");
return;
}
_isRunning = true;
_thread = new Thread(StartSendMail)
{
Name = "PmpEmailQueue",
IsBackground = true,
};
_logger.LogInformation("线程即将启动");
_thread.Start();
_logger.LogInformation("线程已经启动,线程Id是:{0}", _thread.ManagedThreadId);
}
线程启动时运行了方法StartSendMail
:
private void StartSendMail()
{
var sw = new Stopwatch();
try
{
while (true)
{
if (_tryStop)
{
break;
}
if (_provider.IsEmpty)
{
_logger.LogTrace("队列是空,开始睡眠");
Thread.Sleep(_options.SleepInterval);
continue;
}
if (_provider.TryDequeue(out MailBox box))
{
_logger.LogInformation("开始发送邮件 标题:{0},收件人 {1}", box.Subject, box.To.First());
sw.Restart();
SendMail(box);
sw.Stop();
_logger.LogInformation("发送邮件结束标题:{0},收件人 {1},耗时{2}", box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "循环中出错,线程即将结束");
_isRunning = false;
}
_logger.LogInformation("邮件发送线程即将停止,人为跳出循环,没有异常发生");
_tryStop = false;
_isRunning = false;
}
这个方法不断的从队列读取邮件并发送,当 遇到异常,或者_tryStop
为true
时跳出循环,此时线程结束,注意我们会让线程睡眠,在适当的时候。
接下来就是方法SendMail
了:
private void SendMail(MailBox box)
{
if (box == null)
{
throw new ArgumentNullException(nameof(box));
}
try
{
MimeMessage message = ConvertToMimeMessage(box);
SendMail(message);
}
catch (Exception exception)
{
_logger.LogError(exception, "发送邮件发生异常主题:{0},收件人:{1}", box.Subject, box.To.First());
}
finally
{
if (box.Attachments != null && box.Attachments.Any())
{
foreach (var item in box.Attachments)
{
item.Dispose();
...
这里有一个特别要注意的就是在发送之后释放附件(非托管资源):
foreach (var item in box.Attachments)
{
item.Dispose();
...
发送邮件的核心代码只有两行:
MimeMessage message = ConvertToMimeMessage(box);
SendMail(message);
第一行将mailbox转换成 MailKit使用的MimeMessage实体,第二步切实的发送邮件
为什么,我们的接口中没有直接使用MimeMessage而是使用MailBox?
因为MimeMessage比较繁杂,而且附件的问题不易处理,所以我们设计接口时单独封装MailBox简化了编程接口
转换一共两步,1是主体转换,比较简单。二是附件的处理这里涉及到附件名中文编码的问题。
private MimeMessage ConvertToMimeMessage(MailBox box)
{
var message = new MimeMessage();
var from = InternetAddress.Parse(_options.UserName);
from.Name = _options.DisplayName;
message.From.Add(from);
if (!box.To.Any())
{
throw new ArgumentNullException("to必须含有值");
}
message.To.AddRange(box.To.Convert());
if (box.Cc != null && box.Cc.Any())
{
message.Cc.AddRange(box.Cc.Convert());
}
message.Subject = box.Subject;
var builder = new BodyBuilder();
if (box.IsHtml)
{
builder.HtmlBody = box.Body;
}
else
{
builder.TextBody = box.Body;
}
if (box.Attachments != null && box.Attachments.Any())
{
foreach (var item in GetAttechments(box.Attachments))
{
builder.Attachments.Add(item);
}
}
message.Body = builder.ToMessageBody();
return message;
}
private AttachmentCollection GetAttechments(IEnumerable<IAttachment> attachments)
{
if (attachments == null)
{
throw new ArgumentNullException(nameof(attachments));
}
AttachmentCollection collection = new AttachmentCollection();
List<Stream> list = new List<Stream>(attachments.Count());
foreach (var item in attachments)
{
var fileName = item.GetName();
var fileType = MimeTypes.GetMimeType(fileName);
var contentTypeArr = fileType.Split('/');
var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);
MimePart attachment = null;
Stream fs = null;
try
{
fs = item.GetFileStream();
list.Add(fs);
}
catch (Exception ex)
{
_logger.LogError(ex, "读取文件流发生异常");
fs?.Dispose();
continue;
}
attachment = new MimePart(contentType)
{
Content = new MimeContent(fs),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
};
var charset = "UTF-8";
attachment.ContentType.Parameters.Add(charset, "name", fileName);
attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName);
foreach (var param in attachment.ContentDisposition.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
foreach (var param in attachment.ContentType.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
collection.Add(attachment);
}
return collection;
}
在转化附件时下面的代码用来处理附件名编码问题:
var charset = "UTF-8";
attachment.ContentType.Parameters.Add(charset, "name", fileName);
attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName);
foreach (var param in attachment.ContentDisposition.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
foreach (var param in attachment.ContentType.Parameters)
{
param.EncodingMethod = ParameterEncodingMethod.Rfc2047;
}
到这了我们的邮件队列就基本完成了,接下来就是在程序启动后,启动队列,找到 Program.cs文件,并稍作改写如下:
var host = BuildWebHost(args);
var provider = host.Services;
provider.GetRequiredService<IMailQueueManager>().Run();
host.Run();
这里在host.Run()
主机启动之前,我们获取了IMailQueueManager
并启动队列(别忘了注册服务)。
运行程序我们会看到控制台每隔3秒就会打出日志:
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using 'C:\Users\Administrator\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest.
info: MailQueueManager[0]
线程即将启动
info: MailQueueManager[0]
线程已经启动,线程Id是:9
trce: MailQueueManager[0]
队列是空,开始睡眠
Hosting environment: Development
Content root path: D:\publish
Now listening on: http://[::]:5000
Application started. Press Ctrl+C to shut down.
trce: MailQueueManager[0]
队列是空,开始睡眠
trce: MailQueueManager[0]
队列是空,开始睡眠
到此,我们的邮件队列就完成了! :D
欢迎转载,不过要著名原作者和出处
觉得写的不错的话帮忙点个赞撒
Asp.Net Core 快速邮件队列设计与实现的更多相关文章
- 【笔记目录2】【jessetalk 】ASP.NET Core快速入门_学习笔记汇总
当前标签: ASP.NET Core快速入门 共2页: 上一页 1 2 任务27:Middleware管道介绍 GASA 2019-02-12 20:07 阅读:15 评论:0 任务26:dotne ...
- 【笔记目录1】【jessetalk 】ASP.NET Core快速入门_学习笔记汇总
当前标签: ASP.NET Core快速入门 共2页: 1 2 下一页 任务50:Identity MVC:DbContextSeed初始化 GASA 2019-03-02 14:09 阅读:16 ...
- [外包]!采用asp.net core 快速构建小型创业公司后台管理系统(六.结语)
到这里就结束了,真的结束了,源码会在文末分享! 另外录了两个视频,对这个系统进行了演示! 做有意义的事情,原此生无悔! 视频地址:使用asp.net core 快速构建权限管理模块1 使用asp.ne ...
- ASP.NET Core快速入门--学习笔记系列文章索引目录
课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 抓住国庆假期的尾巴完成了此系列课程的学习笔记输出! ASP.NET Core快 ...
- 通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[上篇]
<200行代码,7个对象--让你了解ASP.NET Core框架的本质>让很多读者对ASP.NET Core管道有了真实的了解.在过去很长一段时间中,有很多人私信给我:能否按照相同的方式分 ...
- 【ASP.NET Core快速入门】(六)配置的热更新、配置的框架设计
配置的热更新 什么是热更新:一般来说,我们创建的项目都无法做到热更新:即项目无需重启,修改配置文件后读取到的信息就是修改配置之后的 我们只需要吧项目中用到的IOptions改成IOptionsSnap ...
- ASP.NET Core快速入门学习笔记(第2章:配置管理)
课程链接:http://video.jessetalk.cn/course/explore 良心课程,大家一起来学习哈! 任务9:配置介绍 命令行配置 Json文件配置 从配置文件文本到c#对象实例的 ...
- ASP.NET Core快速入门_学习笔记汇总
第2章 配置管理 任务12:Bind读取配置到C#实例 任务13:在Core Mvc中使用Options 任务14:配置的热更新 任务15:配置框架设计浅析 第3章 依赖注入 任务16:介绍- 任务1 ...
- ASP.NET Core快速入门(第2章:配置管理)- 学习笔记(转载)
原文地址:https://mp.weixin.qq.com/s?__biz=MjM5NjMzMzE2MA==&mid=2451733443&idx=2&sn=6d01721c5 ...
随机推荐
- STM32 堆栈使用解析
安富莱_STM32-V5开发板_μCOS-III教程.pdf 第4章
- uoj233/BZOJ4654/洛谷P1721 [Noi2016]国王饮水记 【dp + 斜率优化】
题目链接 uoj233 题解 下面不加证明地给出几个性质: 小于\(h[1]\)的城市一定是没用的 任何城市联通包含\(1\)且只和\(1\)联通一次 联通顺序从小到大最优 单个联通比多个一起联通要优 ...
- luogu P4198 楼房重建——线段树
题目大意: 小A在平面上(0,0)点的位置,第i栋楼房可以用一条连接(i,0)和(i,Hi)的线段表示,其中Hi为第i栋楼房的高度.如果这栋楼房上任何一个高度大于0的点与(0,0)的连线没有与之前的线 ...
- A1012. The Best Rank
To evaluate the performance of our first year CS majored students, we consider their grades of three ...
- (转)在Eclipse中创建Maven多模块工程
背景:以前只总结了怎么在命令行下创建maven的多模块项目,在eclipse下怎么创建不是很清楚.最近需要在git的资源库中上传多模块项目,方便后期的维护,所以将网上的资料进行整理. 原文链接:htt ...
- 斯坦福大学公开课机器学习:监督学习在行人检测的应用(supervised learning for pedestrian detection)
对于下图,左边是行人,作为阳性例子,赋值y=1,右边是景物,作为阴性例子,赋值y=0; 步长概念: 如下图所示,步长表示绿色框框移动的距离,有时候也称为滑动参数stride,如果一次移动一个像素,则称 ...
- NOIP 普及组 2014 比例简化
传送门 https://www.cnblogs.com/violet-acmer/p/9898636.html 题解: 一开始想多了,以为得保证两者之间的相对比率,至少不能改变的太离谱啊. but,直 ...
- A*算法(附c源码)
关于A*算法网上介绍的有很多,我只是看了之后对这个算法用c写了一下,并测试无误后上传以分享一下,欢迎指正!下面是我找的一个介绍,并主要根据这个实现的. 寻路算法不止 A* 这一种, 还有递归, 非递归 ...
- PHP自动加载(__autoload和spl_autoload_register)
一:什么是自动加载 我们在new出一个class的时候,不需要手动去require或include来导入这个class文件,而是程序自动帮你导入这个文件不需要手动的require那么多class文件了 ...
- pymysql 解决 sql 注入问题
1. SQL 注入 SQL 注入是非常常见的一种网络攻击方式,主要是通过参数来让 mysql 执行 sql 语句时进行预期之外的操作. 即:因为传入的参数改变SQL的语义,变成了其他命令,从而操作了数 ...