如何透明地将文件流式传输到浏览器?

pla*_*alx 5 java iis coldfusion streaming http

受控环境:IE8,IIS 7,ColdFusion

当从IE发出指向媒体文件(如.mp3,.mpeg等)的GET请求时,浏览器将启动关联的应用程序(Window Media Player),我猜想IIS服务文件的方式允许应用程序流它.

我们希望能够完全控制文件的流处理过程,以便我们可以在特定时间和某些用户允许它.出于这个原因,我们不能简单地让IIS直接提供文件,我们希望使用ColdFusion来提供文件.

我们尝试了一些不同的方法,但在每种情况下,浏览器都会在启动外部应用程序之前下载整个文件内容.这就是我们想要避免的.

请注意,我们不需要基于NTFS权限的解决方案.

看起来最有希望的解决方案是使用ColdFusion将文件传输到客户端而不将整个文件加载到内存中但似乎提供的唯一优势是文件在提供给浏览器时不会完全加载到内存中,但是浏览器仍然等待传输结束,然后在Winwodw Media Player中打开文件.

有没有办法使用ColdFusion或Java将文件流式传输到浏览器并让浏览器将处理委托给关联的应用程序,就像我们让IIS直接提供文件一样?

pla*_*alx 2

工作解决方案:

我终于找到了一个支持搜索并且不需要太多工作的解决方案。我基本上创建了一个HttpForwardRequest组件,通过向指定媒体 URL 发出新的 HTTP 请求,同时保留其他初始 servlet 请求详细信息(例如 HTTP 标头),将请求处理委托给 Web 服务器。然后,Web 服务器的响应将通过管道传输到 servlet 的响应输出流中。

在我们的例子中,由于 Web 服务器 (ISS 7.0) 已经知道如何进行 HTTP 流式传输,因此这是我们唯一要做的事情。

注意:我已经尝试过getRequestDispatcher('some_media_url').forward(...),但似乎它无法提供具有正确标头的媒体文件。

HttpForwardRequest代码:

<cfcomponent output="no">

    <cffunction name="init" access="public" returntype="HttpForwardRequest" output="no">
        <cfargument name="url" type="string" required="yes" hint="The URL to which the request should be forwarded to.">
        <cfargument name="requestHeaders" type="struct" required="yes" hint="The HTTP request headers.">
        <cfargument name="response" type="any" required="yes" hint=" The servlet's response object.">
        <cfargument name="responseHeaders" type="struct" required="no" default="#{}#" hint="Custom response headers to override the initial request response headers.">

        <cfset variables.instance = {
            url = arguments.url,
            requestHeaders = arguments.requestHeaders,
            response = arguments.response,
            responseHeaders = arguments.responseHeaders
        }>

        <cfreturn this>
    </cffunction>

    <cffunction name="send" access="public" returntype="void" output="no">
        <cfset var response = variables.instance.response>
        <cfset var outputStream = response.getOutputStream()>
        <cfset var buffer = createBuffer()>

        <cftry>

            <cfset var connection = createObject('java', 'java.net.URL')
                    .init(variables.instance.url)
                    .openConnection()>

            <cfset setRequestHeaders(connection)>

            <cfset setResponseHeaders(connection)>

            <cfset var inputStream = connection.getInputStream()>

            <cfset response.setStatus(connection.getResponseCode(), connection.getResponseMessage())>

            <cfloop condition="true">
                <cfset var bytesRead = inputStream.read(buffer, javaCast('int', 0), javaCast('int', arrayLen(buffer)))>

                <cfif bytesRead eq -1>
                    <cfbreak>
                </cfif>

                <cftry>
                    <cfset outputStream.write(buffer, javaCast('int', 0), bytesRead)>

                    <cfset outputStream.flush()>

                    <!--- 
                    Connection reset by peer: socket write error

                    The above error occurs when users are seeking a video.
                    That is probably normal since I assume the client (e.g. Window Media Player) 
                    closes the connection when seeking.
                    --->
                    <cfcatch type="java.net.SocketException">
                        <cfbreak>
                    </cfcatch>
                </cftry>
            </cfloop>

            <cffinally>

                <cfif not isNull(inputStream)>
                    <cfset inputStream.close()>
                </cfif>

                <cfif not isNull(connection)>
                    <cfset connection.disconnect()>
                </cfif>

            </cffinally>
        </cftry>

    </cffunction>

    <cffunction name="setRequestHeaders" access="private" returntype="void" output="no">

        <cfargument name="connection" type="any" required="yes">

        <cfset var requestHeaders = variables.instance.requestHeaders>

        <cfloop collection="#requestHeaders#" item="local.key">
            <cfset arguments.connection.setRequestProperty(key, requestHeaders[key])>
        </cfloop>

    </cffunction>

    <cffunction name="setResponseHeaders" access="private" returntype="void" output="no">
        <cfargument name="connection" type="any" required="yes">

        <cfset var response = variables.instance.response>
        <cfset var responseHeaders = variables.instance.responseHeaders>
        <cfset var i = -1>

        <!--- Copy connection headers --->
        <cfloop condition="true">

            <cfset i = javaCast('int', i + 1)>

            <cfset var key = arguments.connection.getHeaderFieldKey(i)>

            <cfset var value = arguments.connection.getHeaderField(i)>

            <cfif isNull(key)>
                <cfif isNull(value)>
                    <!--- Both, key and value are null, break --->
                    <cfbreak>
                </cfif>

                <!--- Sometimes the key is null but the value is not, just ignore and keep iterating --->
                <cfcontinue>
            </cfif>

            <cfset setResponseHeader(key, value)>
        </cfloop>

        <!--- Apply custom headers --->
        <cfloop collection="#responseHeaders#" item="key">
            <cfset setResponseHeader(key, responseHeaders[key])>
        </cfloop>

    </cffunction>

    <cffunction name="setResponseHeader" access="private" returntype="void" output="no">
        <cfargument name="key" type="string" required="yes">
        <cfargument name="value" type="string" required="yes">

        <cfset var response = variables.instance.response>

        <cfif arguments.key eq 'Content-Type'>
            <cfset response.setContentType(arguments.value)>
        <cfelse>
            <cfset response.setHeader(arguments.key, arguments.value)>
        </cfif>
    </cffunction>

    <cffunction name="createBuffer" access="private" returntype="any" output="no">
        <cfreturn repeatString("12345", 1024).getBytes()>
    </cffunction>

</cfcomponent>
Run Code Online (Sandbox Code Playgroud)

cf_streamurl代码:

<cfparam name="attributes.url" type="url">

<cfif thisTag.executionMode neq 'start'>
    <cfexit>
</cfif>

<cfset pageContext = getPageContext()>

<cfset requestHeaders = {
    'Authorization' = 'Anonymous'
}>

<cfset structAppend(requestHeaders, getHTTPRequestData().headers, false)>

<cfset pageContext.setFlushOutput(false)>

<!--- Forward the request to IIS --->
<cfset new references.cfc.servlet.HttpForwardRequest(
    attributes.url,
    requestHeaders,
    pageContext.getResponse().getResponse()
).send()>
Run Code Online (Sandbox Code Playgroud)

然后您可以使用cf_streamurl自定义标签,例如:

<cf_streamurl url="http://sh34lprald94/media_stream/unprotected/trusts.mp4"/>
Run Code Online (Sandbox Code Playgroud)

重要提示:目前仅支持匿名身份验证。


第一次半工作尝试(仅用于历史目的):

我们找到了一个满足我们需求的解决方案(实际上非​​常简单),方法是检查响应数据包的 HTTP 标头,并在让 IIS 处理媒体文件时查看 IIS 返回的 mime 类型。

问题是,当尝试使用 ColdFusion 将文件内容提供给浏览器时,我们必须使用一种Window Media Services mime 类型来强制浏览器将处理直接委托给 Window Media Player(然后它能够​​流式传输文件)文件)。

File extension MIME type 
.asf video/x-ms-asf 
.asx video/x-ms-asf 
.wma audio/x-ms-wma 
.wax audio/x-ms-wax 
.wmv audio/x-ms-wmv 
.wvx video/x-ms-wvx 
.wm video/x-ms-wm 
.wmx video/x-ms-wmx 
.wmz application/x-ms-wmz 
.wmd application/x-ms-wmd
Run Code Online (Sandbox Code Playgroud)

解决该问题的第一步是编写一个函数,根据文件扩展名正确解析 mime 类型。IIS 已经具备了这些知识,但是我还没有找到查询其 MIME 注册表的方法。

注意:wmsMimeTypes是一个用作查找 WMS mime 类型的映射的结构。

<cffunction name="getMimeType" access="public" returntype="string">
    <cfargument name="fileName" type="string" required="yes">

    <cfset var mimeType = 'application/x-unknown'>
    <cfset var ext = this.getFileExtension(arguments.fileName)>

    <cfif structKeyExists(this.wmsMimeTypes, ext)>
        <cfreturn this.wmsMimeTypes[ext]>
    </cfif>

    <!--- TODO: Is there a way to read the IIS MIME registry? --->
    <cfregistry action="get" branch="HKEY_CLASSES_ROOT\.#ext#" entry="Content Type" variable="mimeType">

    <cfreturn mimeType>

</cffunction>
Run Code Online (Sandbox Code Playgroud)

然后我们实现了stream如下所示的方法,该方法基于使用 ColdFusion 将文件流式传输到客户端而不将整个文件加载到内存中找到的实现来封装流处理过程

注意:它也适用于cfcontent,但我读到它效率很低,因为它消耗了太多资源,特别是因为它在刷新到浏览器之前将整个文件加载到内存中。

<cffunction name="stream" access="public" returntype="void">
    <cfargument name="file" type="string" required="yes">
    <cfargument name="mimeType" type="string" required="no">

    <cfscript>
        var fileName = getFileFromPath(arguments.file);
        var resolvedMimeType = structKeyExists(arguments, 'mimeType')? arguments.mimeType : this.getMimeType(fileName);
        var javaInt0 = javaCast('int', 0);
        var response = getPageContext().getResponse().getResponse();
        var binaryOutputStream = response.getOutputStream();
        var bytesBuffer = repeatString('11111', 1024).getBytes();
        var fileInputStream = createObject('java', 'java.io.FileInputStream').init(javaCast('string', getRootPath() & arguments.file));

        getPageContext().setFlushOutput(javaCast('boolean', false));

        response.resetBuffer();
        response.setContentType(javaCast('string', resolvedMimeType));

        try {
            while (true) {
                bytesRead = fileInputStream.read(bytesBuffer, javaInt0, javaCast('int', arrayLen(bytesBuffer)));

                if (bytesRead eq -1) break;

                binaryOutputStream.write(bytesBuffer, javaInt0, javaCast('int', bytesRead));
                binaryOutputStream.flush();
            }               
            response.reset();
         } finally {
             if (not isNull(fileInputStream)) fileInputStream.close();
             if (not isNull(binaryOutputStream)) binaryOutputStream.close();
         }
    </cfscript>
</cffunction>
Run Code Online (Sandbox Code Playgroud)

您不得设置Content-Disposition标头,否则浏览器将下载文件而不是将控制权委托给 WMP。

注意:让 Web 服务器将文件流式传输到客户端(或我们使用的 CF 解决方案)永远不会像使用媒体服务器那样高效,就像@Miguel-F 建议的文章中所述。

主要缺点:以前的实现不支持搜索,这实际上可能使解决方案几乎无法使用。