视频CDN技术原理与流程说明

视频CDN专为移动互联网视频内容分发量身定做的一套自主研发的分布式平台,该平台以深圳为中心,分布在全国各地BGP机房的服务器为边缘节点,汇聚中国电信、中国联通、中国移动、中国教育网等运营商网络资源,构建一个全国性的全网覆盖网络,将视频内容缓存或镜像到这些节点边缘服务器上,为不同地域的不同用户提供视频内容加速服务。实现就近访问,从而加速访问速度,提升用户的访问体验,并可以避免源站因访问量过大而造成的服务器负载过量与带宽资源不足等问题,解决网络拥塞,提高用户访问网站的响应速度,有效保证用户体验。

以多媒体技术有限公司的视频CDN产品为例对其技术架构及工作流程进行说明如下:

 

基于媒体内容和实时流媒体节目的地域性特点,不同于一般CDN,视频CDN采用分布式多源架构,它允许位于不同地理位置的边缘分节点可以单独发布自己的媒体内容和实时节目,即各边缘节点在某种程度上也可作为源站,同时也可以将这些内容和节目分发到位于其他各地的分节点,因此每一个节点既可以作为系统中心源站存在,同时也可以实现边缘节点的功能。而源站也可作为边缘节点,来接受用户视频内容的快速响应服务,对于普通的Internet用户来讲,每个CDN边缘节点就相当于一个放置在它周围的Web网站,通过负载均衡系统的控制,用户的请求被透明地指向距离用户最近,网络状态最优的节点,节点中的WebServer服务器会像网站的原始服务器一样,可快速响应用户的请求。

    用户可以预定义内容分发的规则,对上传的内容添加权限及优先级,内容上传后,可选择将优先级高的热点内容预先全镜像同步部署到其他节点,优先级低的可先不进行同步,仅保存在本地源站,系统建立全局性的内容检索查询索引,一旦有用户访问,系统通过对应用层访问请求的智能判断,按照真实用户所处的实际地理位置,所属运营商,以及遍布全网的节点健康状态、网络流量、负载状况以及到用户的距离等信息,根据多种优化策略,通过网络的动态内容分配和全局负载均衡,将用户请求重定向到距离用户最近且健康可用的节点缓存服务器上,使用户可以就近取得所需的内容,如本节点不存在所需内容,系统将自动启动内容检索功能,回源实时获取所需的内容源提供给用户。

    利用多媒体的视频CDN网络,网络内容提供商无需投资昂贵的各类服务器、设立分站点,其基础视频服务,拥有海量存储与分发能力,能够帮助视频网站节省流量。特别是对于视频流媒体信息、远程教学课件等消耗带宽资源多的网络应用服务,使用CDN技术把内容复制到网络的最边缘,使内容请求点和交付点之间的距离缩至最小,对于促进Web站点性能的提高具有重要的意义。网站使用CDN后,即由CDN服务提供商负责信息的传递,保证信息的传输,而网站只需要内容维护,不再需要考虑带宽服务器问题。CDN能够为网络的快速、安全、稳定、可扩展性等方面提供保障。

    目前视频CDN网络体系,可提供对视频内容的点播加速、直播加速、网站内容的缓存加速、镜像部署等服务。打破了由传统CDN设备厂商提供的加速解决方案的技术模式,开创了符合中国互联网/移动互联网情况的CDN技术,可以消除视频内容客户巨大的基础投资及研发与运维投入,同时使视频内容分发的效果在异构网络中都能达到专业级的流畅度与表现力。有效降低了运营成本,适应了中国国情的复杂的网络环境。根据客户需求,提供高性能的互联网及移动互联网视频云服务,使客户只需要最低的成本投入,即能享受到广播级的视听内容。

    当终端用户访问流媒体视频内容时,首先通过负载均衡系统确定最接近用户的最佳边缘节点,同时将终端用户的请求指向该节点。当终端用户的请求到达指定节点时,边缘节点服务器负责将请求的内容提供给终端用户。

    终端用户访问的基本服务流程如下:

    1、终端用户通过域名访问网站指定的视频内容,用户请求将由本地DNS

      最终交给视频CDN负载均衡系统进行处理;

    2、负载均衡系统智能判断访问用户所处地理区域及所属运营商,并搜集相

      关节点的负载及健康状态,返回最优及离用户最近的网络边缘节点IP 

      用户;

    3、终端用户根据返回的IP地址发送媒体请求到相应的网络边缘节点去请求

      内容;

    4、网络边缘节点接收客户请求,根据节点内各设备的实际能力、负载状况

       等因素选择合适的服务器设备给客户提供服务;

    5、如果本地边缘节点存在用户请求的内容,则直接将用户请求媒体内容发

       往最终用户。

    6、如果本地边缘节点不存在用户请求的内容,则查找相应节目列表数据库,

      找出存有目标内容的最佳节点;

    7、边缘节点将用户请求重定向到存有目标内容的节点,由该节点完成对用

       户请求的应答服务;

    8、本地边缘节点启动对目标内容的拉取功能,将目标内容从重定向节点上

       同步过来,以供后续用户的就近访问;

视频播放网站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视频文件内容分发网络。

大家在实现的时候还需要编写一个定时删除边缘服务器上过期文件的服务,该服务比较简单,这里便不做说明了。