序
本文主要记录AS3(flash)基于socket连接ftp服务的处理过程,过程比较艰难,坑很多,记录下来,也许能够帮到有缘人。限于公司相关政策,未能放出全部源码请见谅,主要提供解决思路,以及相关问题的处理。
相关技术
- AS3 socket ,主要用于ftp连接处理;
- ftp命令以及参数响应 ,进行数据交互的过程理解;
- flash FileReference 类 ,文件选择;
- flash 安全沙箱,以及端口授权 ;
- AS,js交互;
- ftp 被动模式传输的丢包问题,如果有拆包处理,并且发送的文件不完整;
实现过程简述
基本结构如下图所示:
- flash客户端编写
实现思路
使用FileReference来进行文件选择,注册监听
fileReference.addEventListener(Event.SELECT, onFileSelect);//文件选择触发
fileReference.addEventListener(ProgressEvent.PROGRESS, progressHandle);//文件加载监听,用于刷新百分比进度
fileReference.addEventListener(Event.COMPLETE, completeHandle);//文件加载完成事件
在completeHandle中调用我们实现的ftp上传服务处理。
其中过程注册的js交互方法就不列举了。
Socket交互处理
private function connect():void{
Security.loadPolicyFile("xmlsocket://"+serverIP+":"+_securityPort);
ftpSocket = new Socket();
ftpSocket.addEventListener(ProgressEvent.SOCKET_DATA, ftpSocketDataHandle);
ftpSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR,ftpSocketSecurityErrorHandle);
ftpSocket.addEventListener(IOErrorEvent.IO_ERROR,ftpIOErrorHandle);
ftpSocket.addEventListener(Event.CONNECT,onConnect);
ftpSocket.connect(serverIP, serverPort);
}
其中Security.loadPolicyFile方法为授权检查,我们访问的端口需要通过flashplayer的安全沙箱检查,flashplayer会在访问之前调用843端口,这里我们可以明确定义要访问的端口,例如:1234。在ftp服务器需要创建一个socket服务,此处我使用java实现了一个。
package org.jod.web.center.flashUpload;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class FTPUploadAuthPortListener extends Thread{
private String authxml = "<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>";
private int port;
private boolean keepRunning = true;
private ServerSocket serverSocket;
public FTPUploadAuthPortListener(int port) {
this.port = port;
}
public void stopForSafe() {
keepRunning = false;
if(!serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
try{
serverSocket = new ServerSocket(port);
Socket client=null;
while(keepRunning){
System.out.println("服务器"+port+"已启动,等待客户端请求。。。。");
client = serverSocket.accept();
BufferedReader br=new BufferedReader(new InputStreamReader(client.getInputStream()));
char[] ch=new char[22];
br.read(ch, 0, ch.length);
StringBuffer sb=new StringBuffer();
for(int i=0;i< ch.length;i++){
sb.append(ch[i]);
}
String st=sb.toString();
System.out.println(port+"接受客户端:["+client.getInetAddress().getHostAddress()+"]请求");
System.out.println("请求信息:"+st);
if(st.indexOf("<policy-file-request/>")!=-1){
OutputStream os = client.getOutputStream();
os.write(authxml.getBytes("utf-8"));
os.flush();
os.close();
}
if(!client.isClosed()) {
client.close();
}
System.out.println("服务响应结束");
}
if(!serverSocket.isClosed()) {
serverSocket.close();
}
System.out.println("服务器已关闭。");
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String args[]){
new FTPUploadAuthPortListener(843).start();
new FTPUploadAuthPortListener(1234).start();
}
}
FTP 状态说明
在监听事件ftpSocketDataHandle中我们使用
var serverResponse:String = socket.readUTFBytes(socket.bytesAvailable);
var stateResponse:String = serverResponse.substr(0, 3);
来获取ftp服务交互的信息
以下状态均来自主socket的交互信息。
"220": //FTP连接就绪
sendCommand("USER "+this.userName);
case “331”: //用户名正确,请继续输入口令
sendCommand("PASS "+this.userPwd);
case “230”: //登入成功
//指定下載文件的類型,I是二進位文件,A是字元文件
sendCommand(“TYPE A”);//設定TYPE為ASCII
sendCommand(“TYPE I”);//設定上傳的編碼為8-bit binary
if(!StringUtil.isEmpty(userDir)) //设定FTP 上传目录
sendCommand("CWD "+userDir);
sendCommand(“PASV”);//passive模式
case “227” : //进入被动模式 返回ip以及端口号 (h1,h2,h3,h4,p1,p2).
解析地址信息创建dataSocket
dataSocket = new Socket(clientIP,clientPort);
dataSocket.addEventListener(ProgressEvent.SOCKET_DATA, receiveData);
dataSocket.addEventListener(IOErrorEvent.IO_ERROR,dataIOErrorHandle);
dataSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR,dataSocketSecurityErrorHandle);
dataSocket.addEventListener(OutputProgressEvent.OUTPUT_PROGRESS,outputProgress_Handler);
sendCommand("STOR "+this.fileName);//告知要上传的文件
case “125”: //服务器告知可以开始文件传输
curPos = 0; //设定传输的开始位置
beginTime = new Date();//记录开始时间(为解决后续的丢包问题做铺垫
)
intervalID = setInterval(sendData,30);//设置定时执行处理,防止flashplayer假死。
sendData时需要处理分包,以及进度控制
case “226”: //关闭数据连接。请求的文件操作成功(比如,文件传输或文件终止)
注意此时只是说明了dataSocket已经关闭
额外用到的状态
case “530”: //530 用户名或者密码有误!
case “421”: //连接超时!
在outputProgress_Handler做特殊处理以解决丢包问题
丢包问题产生原因是因为网络传输时有延时,客户端flush之后还需要一些时间处理,网上的代码都是在处理完成时直接关闭了socket导致数据尚未传输,因此我们需要解决socket提前关闭的问题。
由于ftp文件在写入时不允许访问,size命令无法执行,因此我们只能采用延时处理的方式,先计算得到全局的平均速度,计算传输整包的时间再乘以2得到等待时间
private function outputProgress_Handler(event: OutputProgressEvent): void {
var bytesLoaded: Number = 0;
var bytesTotal: Number = 0;
bytesLoaded = event.bytesTotal;
bytesTotal = this.fileData.length;
var fileUploadPercent : uint = bytesLoaded / bytesTotal * 100;
if(lastPercent==fileUploadPercent){
}else{
dispatcher.dispatchEvent(new FileUpDownloadEvent(FileUpDownloadEvent.PROGRESS,fileUploadPercent+"%"));
lastPercent = fileUploadPercent;
}
if(bytesLoaded>=bytesTotal){
endTime = new Date();
var number:Number = Math.round((endTime.time-beginTime.time)/1000);
var speedByte:Number = bytesTotal/number;
var willwait:Number = chunk/speedByte*2000;
if(willwait<200){
willwait = 200;
}
setTimeout(closeDataSocket,willwait);
}
}
- AS JS交互
讲交互的文章有很多,在此就不细说了。
as调用js
js 代码
function swfInItCallBack(msg){
doSome;
}
as 代码
ExternalInterface.call(“swfInItCallBack”,msg);
js调用as
as 代码
ExternalInterface.addCallback(“getFile”,getFile);//注册方法
js 代码
thisMovie:function(){
return window[this.movieId]?window[this.movieId]:document[this.movieId];
}
thisMovie.getFile();
参数对象是有区别的,传递我使用的json形式的字符串,或者基本类型。
注意事项
flash程序中应含有一个按钮触发文件选择,外部程序调用含有browse方法的方法时会有异常
不同浏览器生成的swf文件引用代码是不同的,最好使用swfobject.js进行处理,否则会取不到swf对象,也就无法进行交互
由于ftp数据传输socket是没有响应的(其他的没有测试,本例使用win7自带ftp服务),因此处理socket的关闭时机尤为重要。
flash中的按钮可能需要自己编写,默认的button应该不能满足html引用时的样式兼容
ftp端口授权处理需要放在服务器,这里可能有人有疑问,很多人都使用过swfupload,为什么它不需要授权处理,其实是有授权处理的,由于http服务器一般都支持flash的授权访问,所以不需要单独进行配置。
flash沙箱问题,这个的确比较恶心,浪费的时间不少。一定要注意本地应用与web插件的区别,本文提到的实现是基于web的需要授权,
在本机测试时使用抓包工具wireshark抓不到授权处理的包,可以使用以下方法
1.以管理员身份运行cmd
2.route add 本机ip mask 255.255.255.255 网关ip
如:route add 172.16.51.115 mask 255.255.255.255 172.16.1.1
使用完毕后用route delete 172.16.51.115 mask 255.255.255.255 172.16.1.1删除,否则所有本机报文都经过网卡出去走一圈回来很耗性能。
此时再利用wireshark进行抓包便可以抓到本机自己同自己的通信包,这样配置的原因是将发往本机的包发送到网关,而此时wireshark可以捕获到网卡驱动的报文实现抓包。
但这样有一个缺点,那就是本地请求的URL的IP只能写本地的IP地址,不能写localhost或127.0.0.1,写localhost或127.0.0.1还是抓不到包。
socket 直连21端口时,无法连接,此时也无授权数据包交互,可能是flashplayer web端屏蔽了吧,具体的原因不明,防火墙,抓包都试了。但是java写socket能连接。
flash有安全访问限制时使用firefox可能导致假死(无法切换页面,flash也不加载,不显示无响应),尝试过网上的解决方案都没好使。