视频播放网站CDN内容分发网络简单代码实现
视频播放如果只有一台视频服务器,当访问用户过多时,服务器将承受不了负载。
所以我们需要在视频服务器下面增加边缘服务器,下面以视频服务器加三台边缘服务器为例。
网络环境图:
1. 用户可通过PC机或手机访问网站。
2. 网站将用户请求转向到负载较小的边缘服务器。
3. 边缘服务器接收到用户请求,先在本地检查用户请求文件是否存在。
4. 如果存在则直接返回本地文件进行播放。
5. 如果不存在则将用户请求转向到视频服务器,并将该视频文件下载到本地。
6. 当用户请求边缘服务器上视频文件,将更新文件最后修改时间,最后修改时间超过一定天数则将该文件删除。
根据以上介绍我们应该对CDN有了一些了解,下面来看看代码方面怎么实现:
第一步先编写一个用于检查本地文件是否存在的IModule,该IModule存在以下文件:
配置文件:Config.xml
<?xml version="1.0" encoding="utf-8" ?> <Config> <!-- 本地点播下载IP --> <LocalIP>192.168.1.103</LocalIP> <!-- 本地点播下载端口 --> <LocalPort>8002</LocalPort> <!-- 服务器IP --> <ServerIP>192.168.1.6</ServerIP> <!-- 服务端口 --> <ServerPort>80</ServerPort> <!-- 本地文件保存路径 --> <SavePath>C:\\File</SavePath> <!-- 待下载文件TXT路径 --> <TxtPath>C:\\IisFileCheckModule\\WaitDownFile.txt</TxtPath> </Config>
文件检查IModule:FileCheckModule.cs
public class FileCheckModule : IHttpModule { /// <summary> /// 初始化 /// </summary> /// <param name="app"></param> public virtual void Init(HttpApplication app) { app.BeginRequest += new EventHandler(app_BeginRequest); app.AuthenticateRequest += new EventHandler(app_AuthenticateRequest); } void app_AuthenticateRequest(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; if (!String.IsNullOrEmpty(app.Request.Path) && app.Request.Path != "/") { FileCheckConfig config = new FileCheckConfig(); //获得文件路径 string filePath = config.SavePath + app.Request.Path.Replace("/", "\\"); //获得文件名 string fileName = Path.GetFileName(filePath); string fileExtension = Path.GetExtension(filePath); if (!String.IsNullOrEmpty(fileExtension)) { if (!File.Exists(filePath)) { //生成点播或下载路径 string downUrl = "http://" + config.ServerIP + app.Request.Path; //在文本文件中新增待下载文件 WaitDownFileTxtManage myWaitDownFileTxtManage = new WaitDownFileTxtManage(); myWaitDownFileTxtManage.AddNewFileDownText(fileName, filePath, downUrl); //重写用户请求URL app.Context.Response.Redirect(downUrl); } else { //生成本地点播或下载路径 string localUrl = "http://" + config.LocalIP + app.Request.Path; //设置请求文件修改时间为当前时间 File.SetLastWriteTime(filePath, DateTime.Now); //重写用户请求URL app.Context.Response.Redirect(localUrl); } } } } protected void app_BeginRequest(object sender, EventArgs e) { } /// <summary> /// 释放 /// </summary> public virtual void Dispose() { } }
文件检查配置获取:FileCheckConfig.cs
public class FileCheckConfig { private string localIP; /// <summary> /// 本地点播IP地址 /// </summary> public string LocalIP { get { return localIP; } set { localIP = value; } } private string serverIP; /// <summary> /// 服务器IP地址 /// </summary> public string ServerIP { get { return serverIP; } set { serverIP = value; } } private string savePath; /// <summary> /// 文件保存路径 /// </summary> public string SavePath { get { return savePath; } set { savePath = value; } } private string txtPath; /// <summary> /// 待下载文件列表txt文件路径 /// </summary> public string TxtPath { get { return txtPath; } set { txtPath = value; } } public FileCheckConfig() { XmlDocument doc = new XmlDocument(); //string configPath = ConfigurationManager.AppSettings["ConfigPath"].ToString(); doc.Load("C:\\inetpub\\IISCdn\\IisFileCheckModule\\Config.xml"); XmlNode configNode = doc.SelectSingleNode("Config"); LocalIP = configNode.SelectSingleNode("LocalIP").InnerText + ":" + configNode.SelectSingleNode("LocalPort").InnerText; ServerIP = configNode.SelectSingleNode("ServerIP").InnerText + ":" + configNode.SelectSingleNode("ServerPort").InnerText; SavePath = configNode.SelectSingleNode("SavePath").InnerText; TxtPath = configNode.SelectSingleNode("TxtPath").InnerText; } }
待下载文件信息添加:WaitDownFileTxtManage.cs
public class WaitDownFileTxtManage { /// <summary> /// 配置对象 /// </summary> FileCheckConfig config = null; public WaitDownFileTxtManage() { config = new FileCheckConfig(); //检查文件是否存在 if (!File.Exists(config.TxtPath)) { //创建一个新的txt文件 StreamWriter sw = File.CreateText(config.TxtPath); sw.Close(); } } /// <summary> /// 添加一个新的下载文件 /// </summary> /// <param name="fileName">文件名称</param> /// <param name="fileSavePath">文件保存路径</param> /// <param name="fileDownPath">文件下载路径</param> public void AddNewFileDownText(string fileName, string fileSavePath, string fileDownPath) { try { string downFile = fileName + ";" + fileSavePath + ";" + fileDownPath; //检查是否已存在相同下载文件 using (StreamReader sr = new StreamReader(config.TxtPath)) { String line; while ((line = sr.ReadLine()) != null) { if (line == downFile) return; } } //添加待下载文件 using (StreamWriter sw = File.AppendText(config.TxtPath)) { sw.WriteLine(downFile); sw.Close(); } } catch (System.Exception e) { WriteLog("添加待下载文件时出现异常:" + e.Message, true); } } /// <summary> /// 打印日志方法 /// </summary> /// <param name="message">错误信息</param> /// <param name="isShowDate">是否显示时间</param> public static void WriteLog(string message, bool isShowDate) { string LogPath = "C:\\Log"; FileStream MainFileStream = null; string outMess = message; if (isShowDate) { string dateStamp = System.DateTime.Now.ToString("HH:mm:ss") + " >> "; outMess = dateStamp + message; } outMess = outMess + "\r\n"; //年 string year = System.DateTime.Now.Year.ToString(); //月 string month = System.DateTime.Now.Month.ToString(); //日 string day = System.DateTime.Now.Day.ToString(); string aimPath = LogPath + "/" + year + "/" + month + "/" + day; if (!Directory.Exists(aimPath)) { #region try { Directory.CreateDirectory(aimPath); } catch (Exception me) { me.ToString(); } #endregion } try { string currentLogPath = aimPath + "/" + System.DateTime.Now.Hour.ToString() + ".txt"; byte[] messB = System.Text.Encoding.Default.GetBytes(outMess); MainFileStream = new FileStream(currentLogPath, System.IO.FileMode.Append, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite); MainFileStream.Write(messB, 0, messB.Length); } catch (Exception me) { me.ToString(); } finally { if (MainFileStream != null) { MainFileStream.Flush(); MainFileStream.Close(); } } } }
这里将待下载文件信息记录在txt文档里,大家在做的时候可以用一个Access数据库实现。
在部署该IModule的时候,需要将IIS处理程序映射中的StaticFile删除
然后添加新的脚本映射
可执行文件路径为
C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll
这样的话,IModule便部署完成了,IModule接收到用户请求后先会在配置文件配置的本地文件路径中查找视频文件,如果找到文件则会转向到本地IIS中部署的另外一个文件站点;如果未找到文件则将请求转向到视频服务器。
第二步我们需要编写一个文件下载服务,该服务存在以下文件:
配置文件:App.config
<appSettings> <!-- 服务器地址 --> <add key="ServerIP" value="192.168.1.6"/> <!-- 本地文件保存路径 --> <add key="SavePath" value="C:\\inetpub\\IISCdn\\File"/> <!-- 待下载XML文件路径 --> <add key="txtPath" value="C:\\inetpub\\IISCdn\\IisFileCheckModule\\WaitDownFile.txt"/> <!-- 服务运行间隔时间 --> <add key="Interval" value="1"/> <!-- 并行下载数 --> <add key="QueueCount" value="5"/> </appSettings>
下载文件服务,需在服务中添加一个时间控件:FileDown.cs
partial class FileDown : ServiceBase { //保存下载文件 Hashtable htDowFile = new Hashtable(); //下载文件数 int queueCount = Convert.ToInt32(ConfigurationManager.AppSettings["QueueCount"]); //当前下载文件数 int downCount = 0; public FileDown() { InitializeComponent(); } protected override void OnStart(string[] args) { // TODO: 在此处添加代码以启动服务。 fileDownTimer.Interval = 10000; fileDownTimer.Start(); //记录服务运行日志 Logger.LogDebug("FileDown", "OnStart", "文件下载服务开始运行。", null); } protected override void OnStop() { // TODO: 在此处添加代码以执行停止服务所需的关闭操作。 fileDownTimer.Stop(); //记录服务运行日志 Logger.LogDebug("FileDown", "OnStop", "文件下载服务停止运行。", null); } /// <summary> /// 时间控件处理事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void fileDownTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { //记录文件下载日志 Logger.LogDebug("FileDown", "fileDownTimer_Elapsed", "开始一轮新的文件下载。", null); fileDownTimer.Stop(); WaitDownFileTxtManage myWaitDownFileXmlManage = new WaitDownFileTxtManage(); FileDownCommon myFileDownCommon = new FileDownCommon(); //从配置文件内获取txt文件路径 string txtPath = ConfigurationManager.AppSettings["txtPath"].ToString(); //循环下载所有文件 using (StreamReader sr = new StreamReader(txtPath)) { String line; while ((line = sr.ReadLine()) != null) { string[] fileElement = line.Split(';'); string downFileSavePath = fileElement[1] + ".downing"; if (!File.Exists(downFileSavePath)) { if (downCount <= queueCount) { downCount++; ThreadPool.QueueUserWorkItem(DownFile, line); } } } } //记录文件下载日志 Logger.LogDebug("FileDown", "fileDownTimer_Elapsed", "结束一轮新的文件下载。", null); Double interval = Convert.ToDouble(ConfigurationManager.AppSettings["Interval"].ToString()); fileDownTimer.Interval = interval * 60 * 1000; fileDownTimer.Start(); } /// <summary> /// 下载文件方法 /// </summary> /// <param name="downFile">下载文件</param> private void DownFile(object downFile) { WaitDownFileTxtManage myWaitDownFileTxtManage = new WaitDownFileTxtManage(); string[] fileElement = downFile.ToString().Split(';'); string fileName = fileElement[0]; string downFileSavePath = fileElement[1] + ".downing"; string trueFilePath = fileElement[1]; string fileDownPath = fileElement[2]; string pathNoFile = downFileSavePath.Substring(0, downFileSavePath.LastIndexOf("\\")); //检查保存路径是否存在 if (!Directory.Exists(pathNoFile)) { Directory.CreateDirectory(pathNoFile); } //记录文件下载日志 Logger.LogDebug("FileDownCommon", "DownFile", "\"" + fileDownPath + "\"文件开始下载。", null); //打开上次下载的文件或新建文件 long lStartPos = 0; System.IO.FileStream fs; if (System.IO.File.Exists(downFileSavePath)) { fs = System.IO.File.OpenWrite(downFileSavePath); lStartPos = fs.Length; fs.Seek(lStartPos, System.IO.SeekOrigin.Current); //移动文件流中的当前指针 } else { fs = new System.IO.FileStream(downFileSavePath, System.IO.FileMode.Create); lStartPos = 0; } //打开网络连接 try { System.Net.WebRequest webRequest = System.Net.WebRequest.Create(fileDownPath); webRequest.Timeout = 10000; System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)webRequest; System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse(); //System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(fileDownPath); //请求正常 if (response.StatusCode == System.Net.HttpStatusCode.OK) { if (lStartPos > 0) request.AddRange((int)lStartPos); //设置Range值 //向服务器请求,获得服务器回应数据流 System.IO.Stream ns = response.GetResponseStream(); byte[] nbytes = new byte[10240]; int nReadSize = 0; nReadSize = ns.Read(nbytes, 0, 10240); while (nReadSize > 0) { fs.Write(nbytes, 0, nReadSize); nReadSize = ns.Read(nbytes, 0, 10240); } fs.Close(); ns.Close(); File.Move(downFileSavePath, trueFilePath); //删除下载完成的文件 myWaitDownFileTxtManage.DeleteDownFile(fileName, trueFilePath, fileDownPath); downCount--; //记录文件下载日志 Logger.LogDebug("FileDownCommon", "DownFile", "\"" + fileDownPath + "\"文件下载完成。", null); } //文件未找到 else if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { //删除不存在的文件 myWaitDownFileTxtManage.DeleteDownFile(fileName, trueFilePath, fileDownPath); downCount--; fs.Close(); Logger.LogDebug("FileDownCommon", "DownFile", "\"" + fileDownPath + "\"文件在服务器上不存在。", null); } } catch (FileNotFoundException fileEx) { fs.Close(); downCount--; //删除不存在的文件 myWaitDownFileTxtManage.DeleteDownFile(fileName, trueFilePath, fileDownPath); //记录异常日志 Logger.LogError("FileDownCommon", "DownFile", AppError.EROR, 0, fileEx, "\"" + fileDownPath + "\"文件在服务器上不存在。", null); } catch (Exception ex) { fs.Close(); downCount--; //记录异常日志 Logger.LogError("FileDownCommon", "DownFile", AppError.EROR, 0, ex, "下载文件\"" + fileDownPath + "\"过程中出现错误:" + ex.ToString(), null); } } }
待下载文件读取:WaitDownFileTxtManage.cs
public class WaitDownFileTxtManage { //txt文件路径 string txtPath = ""; public WaitDownFileTxtManage() { //从配置文件内获取txt文件路径 txtPath = ConfigurationManager.AppSettings["txtPath"].ToString(); //检查文件是否存在 if (!File.Exists(txtPath)) { //创建一个新的txt文件 StreamWriter sw = File.CreateText(txtPath); sw.Close(); } } /// <summary> /// 删除一个下载文件 /// </summary> /// <param name="fileName">文件名称</param> /// <param name="fileSavePath">文件保存路径</param> /// <param name="fileDownPath">文件下载路径</param> public void DeleteDownFile(string fileName, string fileSavePath, string fileDownPath) { try { string downFile = fileName + ";" + fileSavePath + ";" + fileDownPath; //将除了要删除的下载文件外的所有下载文件取出 ArrayList list = new ArrayList(); using (StreamReader sr = new StreamReader(txtPath)) { String line; while ((line = sr.ReadLine()) != null) { if (line == downFile) continue; list.Add(line); } sr.Close(); } //重写下载文件 using (StreamWriter sw = new StreamWriter(txtPath)) { for (int i = 0; i < list.Count; i++) sw.WriteLine(list[i].ToString()); sw.Close(); } } catch (System.Exception e) { //记录异常日志 Logger.LogError("WaitDownFileTxtManage", "DeleteDownFile", AppError.EROR, 0, e, "删除下载文件节点出错:" + e.ToString(), null); } } /// <summary> /// 更新下载文件状态 /// </summary> /// <param name="fileName">文件名称</param> /// <param name="fileSavePath">文件保存路径</param> /// <param name="fileDownPath">文件下载路径</param> /// <param name="oldStatus">旧状态</param> /// <param name="newStatus">新状态</param> public void UpdateDownFileStatus(string fileName, string fileSavePath, string fileDownPath, string oldStatus, string newStatus) { try { string downFile = fileName + ";" + fileSavePath + ";" + fileDownPath + ";" + oldStatus; string newDownFile = fileName + ";" + fileSavePath + ";" + fileDownPath + ";" + newStatus; //将除了要删除的下载文件外的所有下载文件取出 ArrayList list = new ArrayList(); using (StreamReader sr = new StreamReader(txtPath)) { String line; while ((line = sr.ReadLine()) != null) { if (line == downFile) continue; list.Add(line); } } //重写下载文件 using (StreamWriter sw = new StreamWriter(txtPath)) { for (int i = 0; i < list.Count; i++) sw.WriteLine(list[i].ToString()); sw.Close(); } using (StreamWriter sw = File.AppendText(txtPath)) { sw.WriteLine(newDownFile); sw.Close(); } } catch (System.Exception e) { //记录异常日志 Logger.LogError("WaitDownFileTxtManage", "UpdateDownFileStatus", AppError.EROR, 0, e, "更新下载文件节点状态出错:" + e.ToString(), null); } } }
通过以上几步便完成了一个简单的CDN视频文件内容分发网络。
大家在实现的时候还需要编写一个定时删除边缘服务器上过期文件的服务,该服务比较简单,这里便不做说明了。
希望我的文章对大家有帮助!