如何透明地将文件流传输到浏览器? [英] How to transparently stream a file to the browser?

查看:824
本文介绍了如何透明地将文件流传输到浏览器?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

CONTROLLED ENVIRONMENT:IE8,IIS 7,ColdFusion



发出指向媒体文件mpeg等,从IE,浏览器将启动相关的应用程序(窗口媒体播放器),我猜,IIS服务的文件允许应用程序流。it / b>

我们希望能够完全控制文件的流式传输过程,以便我们可以在特定时间只允许某些用户使用。因此,我们不能简单地让IIS直接服务该文件,而是要使用ColdFusion来代替该文件。



我们尝试了几种不同的方法,但在每种情况下,浏览器都会在启动外部应用程序之前下载整个文件内容。



请注意,我们不想使用基于NTFS权限的解决方案。



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



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

解决方案

工作解决方案:



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



在我们的例子中,由于网络服务器(ISS 7.0)已经知道如何做HTTP流式传输,这是我们唯一要做的。



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

HttpForwardRequest 代码:



 < cfcomponent output =no> 

< cffunction name =initaccess =publicreturntype =HttpForwardRequestoutput =no>
< cfargument name =urltype =stringrequired =yeshint =请求应转发到的网址。>
< cfargument name =requestHeaderstype =structrequired =yeshint =HTTP请求标头>
< cfargument name =responsetype =anyrequired =yeshint =servlet的响应对象。
< cfargument name =responseHeaderstype =structrequired =nodefault =#{}#hint =自定义响应标头以覆盖初始请求响应标头。

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

< cfreturn this>
< / cffunction>

< cffunction name =sendaccess =publicreturntype =voidoutput =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()>


< 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()>

<!---
对等连接重置:套接字写入错误

上述错误发生在用户正在查找视频时。
这可能是正常的,因为我假设客户端(例如Window Media Player)
在查找时关闭连接。
--->
< cfcatch type =java.net.SocketException>
< cfbreak>
< / cfcatch>
< / cfloop>

< cffinally>

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

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

< / cffinally>
< / cftry>

< / cffunction>

< cffunction name =setRequestHeadersaccess =privatereturntype =voidoutput =no>

< cfargument name =connectiontype =anyrequired =yes>

< cfset var requestHeaders = variables.instance.requestHeaders>

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

< / cffunction>

< cffunction name =setResponseHeadersaccess =privatereturntype =voidoutput =no>
< cfargument name =connectiontype =anyrequired =yes>

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

<!---复制连接头--->
< 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)>
<!---二者,键和值都为null,break --->
< cfbreak>
< / cfif>

<!---有时密钥是null但是值不是,只是忽略并继续迭代--->
< cfcontinue>
< / cfif>

< cfset setResponseHeader(key,value)>

<!---应用自定义标题--->
< cfloop collection =#responseHeaders#item =key>
< cfset setResponseHeader(key,responseHeaders [key])>

< / cffunction>

< cffunction name =setResponseHeaderaccess =privatereturntype =voidoutput =no>
< cfargument name =keytype =stringrequired =yes>
< cfargument name =valuetype =stringrequired =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 =createBufferaccess =privatereturntype =anyoutput =no>
< cfreturn repeatString(12345,1024).getBytes()>
< / cffunction>

< / cfcomponent>



cf_streamurl 代码:



 < cfparam name =attributes.urltype =url> 

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

< cfset pageContext = getPageContext()>

< cfset requestHeaders = {
'授权'='匿名'
}>

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

< cfset pageContext.setFlushOutput(false)>

<!---将请求转发到IIS --->
< cfset new references.cfc.servlet.HttpForwardRequest(
attributes.url,
requestHeaders,
pageContext.getResponse()。getResponse()
).send ()>

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

 < cf_streamurl url =http://sh34lprald94/media_stream/unprotected/trusts.mp4/> 

重要:目前只支持匿名验证。




上半次尝试(仅限历史目的):



一个解决方案(实际上很简单),它适合我们的需要,通过检查响应数据包的HTTP头,并查看IIS返回的mime类型,当它让服务器的媒体文件。



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

 文件扩展名MIME类型
.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 / wvx
.wm video / x-ms-wm
.wmx video / x-ms-wmx
.wmz application / x-ms-wmz
.wmd application / ms-wmd

解决问题的第一步是编写一个函数来解析mime类型正确基于文件的扩展名。 IIS已经知道了,但是我还没有找到一种方法来查询它的MIME注册表。



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

 < cffunction name =getMimeTypeaccess =publicreturntype =string> 
< cfargument name =fileNametype =stringrequired =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:有没有办法读取IIS MIME注册表? --->
< cfregistry action =getbranch =HKEY_CLASSES_ROOT\。#ext#entry =Content Typevariable =mimeType>

< cfreturn mimeType>

< / cffunction>

然后我们实现了 stream 它封装了基于使用ColdFusion将文件流式传输到客户端而不将整个文件加载到内存中



<注意:它也适用于 cfcontent ,但我读到它是相当低效,因为它消耗太多的资源,特别是因为它加载整个文件在内存



 < cffunction name =streamaccess =publicreturntype = void> 
< cfargument name =filetype =stringrequired =yes>
< cfargument name =mimeTypetype =stringrequired =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>

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



文件到客户端(或我们使用的CF解决方案)将永远不会像使用媒体服务器一样高效,如 @ Miguel-F建议的文章



主要DOWNSIDE:将不支持实际上可能使解决方案几乎不可用的寻找。


CONTROLLED ENVIRONMENT: IE8, IIS 7, ColdFusion

When issuing a GET request pointing to a media file such as .mp3, .mpeg, etc. from IE, the browser will launch the associated application (Window Media Player) and I guess that the way IIS serves the file allows the application to stream it.

We would like to be able to have a full control over the streaming process of a file so that we can allow it at certain times and to certain users only. For that reason we cannot simply let IIS directly serve the file and we wanted to use ColdFusion to serve the file instead.

We have tried a few different approaches, but in every case the browser is downloading the entire file content before launching the external application. That's what we want to avoid.

Please note that we do not want an NTFS-permission based solution.

The solution that looked the most promising was Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory but the only advantage that seemed to offer was that the file wouldn't be entirely loaded in memory when served to the browser, but the browser still waits until the end of the transfer and then opens up the file in Winwodw Media Player.

Is there a way to use ColdFusion or Java to stream a file to the browser and having the browser delegate the handling to the associated application just like when we let IIS directly serve the file?

解决方案

WORKING SOLUTION:

I finally managed to find a solution that supports seeking and that doesn't involve too much work. I basically created an HttpForwardRequest component that delegates the request handling to the web server by issuing a new HTTP request to the specified media URL while preserving other initial servlet request details, such as HTTP headers. The web server's response will then be piped into the servlet's response output stream.

In our case, since the web server (ISS 7.0) already know how to do HTTP streaming, that's the only thing we have to do.

Note: I have tried with getRequestDispatcher('some_media_url').forward(...) but it seems that it cannot serve media files with the correct headers.

HttpForwardRequest code:

<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>

cf_streamurl code:

<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()>

You can then use the cf_streamurl custom tag like:

<cf_streamurl url="http://sh34lprald94/media_stream/unprotected/trusts.mp4"/>

IMPORTANT: It only supports Anonymous authentication for now.


First half-working attempt (historical purpose only):

We found a solution (which was actually quite simple) that suits our needs by inspecting the HTTP headers of the response packet and looking at the mime type returned by IIS when letting it server the media file.

The issue was that when trying to serve the file content to the browser using ColdFusion, we had to use one of the Window Media Services mime types to force the browser to delegate the handling to Window Media Player directly (which is then able to stream the file).

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

The first step for solving the issue was to write a function that would resolve the mime type correctly based on the file's extension. IIS has that knowledge already, however I haven't found a way of querying it's MIME registry yet.

Note: wmsMimeTypes is a struct used as a map to lookup WMS mime types.

<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>

Then we implemented a stream method like below that encapsulates the streaming process based on the implementation found in Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory

Note: It also works with cfcontent, but I read that it was quite inefficient because it's consuming too much resources, especially because it loads the entire file in memory before flushing to the browser.

<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>

You must NOT set the Content-Disposition header or the browser will download the file instead of delegating the control to WMP.

Note: Letting the web server to stream the file to the client (or the CF solution we used) will never be as efficient as using a media server, like stated in the article that @Miguel-F suggested.

MAJOR DOWNSIDE: The previous implementation will not support seeking which actually might make the solution almost unusable.

这篇关于如何透明地将文件流传输到浏览器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆