上次参加“软件杯”做了个文件传输软件,这个文件传输软件有管理远程文件/目录的功能,所以当时自己就编写了一个简单的文件管理传输协议。文件管理传输协议有比较成熟的协议,像ftp协议,它是用一端作为服务端,一端作为客户端来进行访问。本篇文章写的协议也与它类似。我们是两端都作为服务端,也都作为客户端来进行互相传输。相对于长连接,这种方式更加方便,简单。本文讲的代码是Android平台的,其他平台也可以参考,写出相对应的协议。协议主要包括以下内容:

1.      获取远程目录下的目录和文件

2.      发送目录到远程

3.      重命名远程文件和目录

4.      删除远程文件和目录

5.      发送发送文件的消息

6.      发送文件到远程的指定目录

7.      发送获取文件的消息

8.      获取远程的指定文件

协议主要包括两种数据,一种是消息数据,另一种是非消息数据(文件流)

消息数据格式如下:

消息头(1个字节)

消息体(多个字节)

1

json数据

非消息数据格式如下:

消息头(1个字节)

文件大小(8字节)

文件体(多个字节)

1

8字节的文件大小

文件数据

 

在Android平台上我们是开启一个服务来运行我们的所有socket服务,所有的协议代码都在这个服务类里边,这个服务类取名TcpService.java,继承至android.app.Service。

 

1.1 开启socket服务端

我们在服务启动时就开启socket服务端连接:

@Override
    publicIBinderonBind(Intent intent) {
        this.intent=intent;
        //启动为服务端
            try {
                String remoteIp=intent.getStringExtra("remote_ip");
                if(remoteIp!=null) {
                    remoteHost =intent.getStringExtra("remote_ip");
                }
                remotePort=intent.getIntExtra("remote_port",((ThisApplication)getApplication()).getAppPort());
                tcpListen(remotePort);
            } catch(IOException e) {
                e.printStackTrace();
            }
        returnsimpleBinder;
    }

上面是onBind方法,将服务绑定到Activity,Activity与它绑定后他将启动,Activity结束后它也结束。在上面的代码中remote_ip是Activity连接到其他主机时获取到的并传过来的远程主机的ip。romote_port是传过来要绑定的端口号。


tcpListen的方法内容如下,开启一个线程并启动套接字服务:

/**
     * 服务端监听
     * @param port
     * @throwsIOException
     */
    public void tcpListen(int port)throws IOException
    {
        serverThread=new SocketServerThread(port);
        serverThread.start();
    }

SocketServerThread的内容如下:

private  class SocketServerThread extends SocketThread {
        privateServerSocket serverSocket;
        private  booleanisCanCreateThread=true;//标记是否可以创建线程,巧妙解决多任务传输
        private int port;
        public SocketServerThread(int port){
            // TODO Auto-generated constructor stub
            this.port=port;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                serverSocket=new ServerSocket(port);
            } catch(IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                while(isServer)
                {
                    try {
                        isCanCreateThread=false;//标记不能创建新线程
                       socket=serverSocket.accept();
                        isCanCreateThread=true;//这个线程有连接进入了,不等待了,另外的一个线程可以创建线程了
                       out=socket.getOutputStream();
                       in=socket.getInputStream();
                        bytedataType=-1;
                        DataInputStreamdataInputStream=new DataInputStream(in);
                       dataType=dataInputStream.readByte();
                        if(dataType==DATA_TYPE_CHAR){
                            //字符流
                            runCmd();
                        }elseif(dataType==DATA_TYPE_BYTE) {
                            saveFile(new File(getSaveFilePath()));
                        }
                    }catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }
        }
        public void close() throwsIOException {
            isServer=false;
            if(serverSocket!=null)
                serverSocket.close();
            if(socket!=null)
                socket.close();
        }
    }

服务端开启后,死循环监听连接,接收到有连接进入。就读取头,然后进行相应的操作。读到数据头为1,说明客户端连接发送过来的是消息数据,就调用runCmd方法进行消息的处理。如果读到数据头为0,说明客户端连接发送过来的是文件数据,就调用saveFile方法对文件进行保存。


1.2 发送消息方法

/**
     * 发送文本消息
     * @param head 文本头
     * @param msg 文本消息
     */
    private synchronized   void sendTextMsg(final byte head, final String msg)
    {
        newThread(new Runnable() {
            @Override
            public void run() {
                Socket socket= null;
                OutputStream outputStream=null;
                DataOutputStream dataOutputStream=null;
                try {
                    socket = new Socket(remoteHost, remotePort);
                   outputStream=socket.getOutputStream();
                    dataOutputStream=new DataOutputStream(outputStream);
                    dataOutputStream.writeByte(head);//发送头
                   dataOutputStream.write(msg.getBytes());//发送内容体
                    dataOutputStream.flush();
                } catch(IOException e) {
                    e.printStackTrace();
                }finally {
                    if(dataOutputStream!=null) {
                        try {
                           dataOutputStream.close();
                        } catch (IOException e) {
                           e.printStackTrace();
                        }
                    }
                    if(socket!=null) {
                        try {
                            socket.close();
                        } catch (IOException e) {
                           e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

发送消息是开启一个线程后开启一个socket客户端来发送消息,先写入消息头再写入一个消息体。


1.3 发送获取远程目录消息

/**
     * 获取目录
     *
     * 发送的内容:
     * {
     *"command":"get_content",
     * "path":"远程目录"
     * }
     *
     * @param path
     * @throwsIOException
     */
    @Override
    public  void sendGetContentMsg( String path)throwsIOException {
        // TODO Auto-generated method stub
        finalString msg=String.format("{\"command\":\"%s\",\"path\":\"%s\"}", REQUEST_TYPE_GET_CONTENT,path);
        sendTextMsg(DATA_TYPE_CHAR,msg);
    }
发送获取目录消息,消息中的中command字段存储的是消息类型(下同,不再解释)。path是要获取的远程目录的路径。对方收到该命令就将远程目录封装成json数据作为消息发过来。
 
1.4发送目录消息
 
/**
     * 发送目录
     * 获取自己的目录并发过去
     *如果收到这个命令就更新自己的远程文件列表
     */
    @Override
    public synchronized  void sendContent() throwsIOException {
        // TODO Auto-generated method stub
        finalStringBuilder msg=new StringBuilder();
        File[] files=new File(getRemoteRequestRoot()).listFiles();
        Arrays.sort(files,new FileNameSort());
        msg.append(String.format("{\"command\":\"%s\",\"root\":\"%s\",\"files\":["
                ,RESPONSE_TYPE_CONTENT
                ,getRemoteRequestRoot()));
        for(Filef:files)
        {
            msg.append(String.format("{\"name\":\"%s\",\"length\":%s,\"isFile\":%s,\"lastModified\":%s},"
                    ,f.getName()
                    ,f.length()
                    ,f.isFile()
                    ,f.lastModified()));
        }
        msg.append("]}");
        String newMsg=msg.toString().replace(",]","]");//替换掉
        sendTextMsg(DATA_TYPE_CHAR,newMsg);
    }

发送目录消息方法将指定文件目录下的所有文件和目录按文件名排序,并将文件名、文件大小、文件类型、所在目录和文件修改日期等信息封装成json,最后以消息的形式发送出去。


1.4 发送获取文件的消息

/**
     * 发送获取文件命令,远端收到该命令就给本地流中写数据
     * {
     *"command":"get_file",
     * "path":""  --要获取的远程文件
     * }
     * @throwsIOException
     */
    @Override
    public  void sendGetFileMsg(String remoteFile)throwsIOException {
        // TODO Auto-generated method stub
        finalString msg=String.format("{\"command\":\"%s\",\"path\":\"%s\"}", REQUEST_TYPE_GET_FILE,remoteFile);
        sendTextMsg(DATA_TYPE_CHAR,msg);
    }

path是要获取的远程文件的绝对路径。


1.5 发送文件

/**
     * 发送文件
     * @param file 要发送的本地文件
     * @throwsIOException
     */
    @Override
    public synchronized void sendFile(final File file)throws IOException{
        // TODO Auto-generated method stub
        finalFileInputStream fileInputStream=newFileInputStream(file);
        newThread(new Runnable() {
            @Override
            public void run() {
                Socket socket= null;
                DataOutputStreamdataOutputStream = null;
                try {
                    socket = new Socket(remoteHost, remotePort);
                    out=socket.getOutputStream();
                     dataOutputStream=new DataOutputStream(out);
                    byte[]buf=newbyte[BUFFER_SIZE];
                    intcount=0;
                    intsentSize=0;//已发送的字节数
                    longfileSize=file.length();
                   dataOutputStream.write(DATA_TYPE_BYTE);//发送头
                   dataOutputStream.writeLong(fileSize);//文件大小
                    while((count=fileInputStream.read(buf))!=END_OF_STREAM)//发送内容体
                    {
                        sentSize+=count;
                       updateUI(sentSize,fileSize);
                       dataOutputStream.write(buf,0,count);
                       dataOutputStream.flush();
                    }
                    fileInputStream.close();//关闭读取文件流
                } catch(IOException e) {
                    e.printStackTrace();
                }finally {
                    if(dataOutputStream!=null) {
                        try {
                            dataOutputStream.close();
                        } catch (IOException e) {
                           e.printStackTrace();
                        }
                    }
                    if(socket!=null) {
                        try {
                            socket.close();
                        } catch (IOException e) {
                           e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

收到了get_file消息后命令处理函数就会调用该方法来读取本地文件并发送给对方。

 

1.6 发送发送文件消息

当你想主动给远程发送文件,就得先给远程发送该消息。

/**
     * 给远程发送发送文件命令,对方收到这个命令就可以知道它自己该存文件在哪了
     *
     * 发送的内容:
     * {
     *"command":"send_file",
     * "path":"远程文件"
     * }
     * @throwsIOException
     */
    @Override
    public  void sendSendFileMsg(String path)throwsIOException {
        // TODO Auto-generated method stub
        finalString msg=String.format("{\"command\":\"%s\",\"path\":\"%s\"}", REQUEST_TYPE_SEND_FILE,path);
        sendTextMsg(DATA_TYPE_CHAR,msg);
    }




1.7 发送重命名文件消息

/**
     * 重命名
     *
     * 发送的内容:
     * {
     *"command":"rename_file",
     * "path":"远程目录"
     * }
     *
     * @param path 原文件的路径
     * @param  newPath 新的文件的路径
     * @throwsIOException
     */
    @Override
    public void sendRenameFileMsg( String path,String newPath)throws IOException {
        // TODO Auto-generated method stub
        finalString msg=String.format("{\"command\":\"%s\",\"path\":\"%s\",\"new_path\":\"%s\"}", REQUEST_TYPE_RENAME_FILE,path,newPath);
        sendTextMsg(DATA_TYPE_CHAR,msg);
    }



1.8 发送删除文件消息

/**
     * 删除文件
     *
     * 发送的内容:
     * {
     *"command":"delete_file",
     * "path":"远程目录"
     * }
     *
     * @param path
     * @throwsIOException
     */
    @Override
    public  void sendDeleteFileMsg( String path)throwsIOException {
        // TODO Auto-generated method stub
        finalString msg=String.format("{\"command\":\"%s\",\"path\":\"%s\"}", REQUEST_TYPE_DELETE_FILE,path);
        sendTextMsg(DATA_TYPE_CHAR,msg);
    }





1.9 消息处理函数

收到以上消息后,要进行处理,才能使收到的消息能够起作用。消息处理器的函数内容如下:

public void runCmd() throws IOException {
            String cmd=readCommand();
            JSONObject json=null;
            try {
                json=new JSONObject(cmd);
            } catch(Exception e) {
                e.printStackTrace();
                return;//Json格式出错还搞毛啊,直接返回
            }
            //收到获取目录的命令
            if(cmd.contains(REQUEST_TYPE_GET_CONTENT))
            {
                if(json!=null) {
                    try {
                       setRemoteRequestRoot(json.getString("path"));
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    sendContent();
                }
            }
            //收到重命名的命令
            else if(cmd.contains(REQUEST_TYPE_RENAME_FILE))
            {
                if(json!=null) {
                    try {
                        File file=new File(json.getString("path"));
                        file.renameTo(new File(json.getString("new_path")));
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    sendContent();
                }
            }
            //收到删除文件命令
            else if(cmd.contains(REQUEST_TYPE_DELETE_FILE))
            {
                File f;
                try {
                    f=new File(json.getString("path"));
                    Utils.deleteFile(f);
                } catch(JSONException e) {
                    e.printStackTrace();
                }
                sendContent();
            }
            //收到目录这个命令就更新自己的远程文件列表
            else if(cmd.contains(RESPONSE_TYPE_CONTENT))
            {
                Intent intent=new Intent(NOTICE_TYPE_UPDATE_REMOTE_LIST);
                intent.putExtra("json",cmd);
                sendBroadcast(intent);
            }
            //收到要接受文件的命令
            else if(cmd.contains(REQUEST_TYPE_SEND_FILE))
            {
                try {
                   setSaveFilePath(json.getString("path"));
                   System.out.println(json.getString("path"));
                } catch(JSONException e) {
                    e.printStackTrace();
                }
            }
            //对方要获取文件.就给他发文件
            else if(cmd.contains(REQUEST_TYPE_GET_FILE))
            {
                try {
                    sendFile(newFile(json.getString("path")));
                } catch(JSONException e) {
                    e.printStackTrace();
                }
            }
            //收到复制文件的命令,同端之间
            else if(cmd.contains(REQUEST_TYPE_COPY_FILE))
           {
                if(json!=null) {
                    try {
                        File file=new File(json.getString("path"));
                        File newFile=new File(json.getString("new_path"));
                        Utils.copyFile(file,newFile,null);//同端之间复制文件
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    sendContent();
                }
            }
            //客户端连接上
            else if(cmd.contains(REQUEST_TYPE_CONNECTED))
            {
                try {
                    StringremoteIP=json.getString("local_ip");//将对方的ip存为自己的ip
                    remoteHost=remoteIP;
                    noticeClientConnected();//给界面发送通知
                } catch(JSONException e) {
                    e.printStackTrace();
                }
            }
            //断开连接
            else if(cmd.contains(REQUEST_TYPE_DISCONNECT))
            {
                Intent intent=new Intent(NOTICE_TYPE_FILE_CLOSE_ACTIVITY);
                sendBroadcast(intent);
                isServer=false;
            }
        }
    }




后记:有些消息的内容就不发出来了,给了主要思路,剩下的大家都会写了。