Android Tcp Socket 大文件续传

本章节我们学习如何使用 Socket 实现文件上传,并且支持续传

最终效果图

  1. 当文件上传成功后服务器端运行截图

  2. APP 运行效果图


  3. 上传成功后可以看到我们的服务端的项目下生成一个 file 的文件夹,我们可以在这里找到上传的文件

    打开 python.chm.log 可以看到如下内容

    #Thu Apr 05 19:09:36 CST 2018
    length=3618495
    

实现思路

客户端第一次连接时会向服务器发送类似以下的内容

Content-Length = xxx;filename=xxx

服务器收到后立刻解析字符串并且查找文件名为 xxx 的文件是否有上传记录,如果有的话回复已经上传的大小 pos=ddd,否则返回 pos=0 客户端收到文件后再从指定的位置开始上传

sha1 用来解决文件重名问题

服务器端

先实现服务器端接收文件,我们先整理下思路

  1. 支持多用户访问,需要创建一个线程池,为每一个通讯的 socket 单独分配一条独立的线程

  2. 需要自定义一个通讯的 socket 线程

  3. 对客户端发来的协议进行解析,提取 filenamefilelengthsourceid 数据

  4. 根据 filename 查找是否存在文件上传记录

    • 如果不存在,则创建目录和文件

    • 如果存在,且 hash 值相同,则返回已经上传的内容大小 pos

  5. 返回客户端 sourceidpos 从而指示客户端要从哪里开始上传文件

差不多就这样,我们先来实现服务器端

范例

在某个目录下创建 FileServer.java,比如 d:\dev

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.io.RandomAccessFile;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FileServer {

    private ExecutorService executorService;//线程池
    private int port;//监听端口
    private boolean quit = false;//退出
    private ServerSocket server;
    private Map<Long, FileLog> datas = new HashMap<Long, FileLog>();//存放断点数据

    public static void main(String[] args) throws IOException {

        FileServer fileServ = new FileServer(12345);
        System.out.println("服务器已经运行在" + getServAddr() + ":12345 ,等待客户端发送数据");

        try {
            fileServ.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    // 获取内网端口号
    public static String getServAddr() {

        try {
            Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
            while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();

                // 去除回环接口,子接口,未运行和接口
                if (netInterface.isLoopback() || netInterface.isVirtual() || !netInterface.isUp()) {
                    continue;
                }

                Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = addresses.nextElement();
                    if (ip != null) {
                        // ipv4
                        if (ip instanceof Inet4Address) {
                            if (ip.getHostAddress().startsWith("192") || ip.getHostAddress().startsWith("10")
                                    || ip.getHostAddress().startsWith("172") || ip.getHostAddress().startsWith("169")) {
                                return ip.getHostAddress();
                            }
                        }
                    }
                }
            }
        } catch (SocketException e) {
            System.err.println("Error when getting host ip address"+ e.getMessage());
        }

        return "";
    }




    public FileServer(int port){
        this.port = port;
        //创建线程池,池中具有(cpu个数*50)条线程
        executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 50);
    }

    /**
     * 退出
     */
    public void quit(){
        this.quit = true;
        try {
            server.close();
        } catch (IOException e) {
        }
    }
    /**
     * 启动服务
     * @throws Exception
     */
    public void start() throws Exception{
        server = new ServerSocket(port);
        while(!quit){
            try {
                Socket socket = server.accept();
                //为支持多用户并发访问,采用线程池管理每一个用户的连接请求
                executorService.execute(new SocketTask(socket));
            } catch (Exception e) {
                //  e.printStackTrace();
            }
        }
    }

    private final class SocketTask implements Runnable{
        private Socket socket = null;
        public SocketTask(Socket socket) {
            this.socket = socket;
        }

        public void run() {
            try {
                System.out.println("accepted connection "+ socket.getInetAddress()+ ":"+ socket.getPort());
                PushbackInputStream inStream = new PushbackInputStream(socket.getInputStream());
                //得到客户端发来的第一行协议数据:Content-Length=143253434;filename=xxx.3gp;sourceid=
                //如果用户初次上传文件,sourceid 的值为空。
                String head = StreamTool.readLine(inStream);
                System.out.println(head);
                if(head!=null){
                    //下面从协议数据中提取各项参数值
                    String[] items = head.split(";");
                    String filelength = items[0].substring(items[0].indexOf("=")+1);
                    String filename = items[1].substring(items[1].indexOf("=")+1);
                    String sourceid = items[2].substring(items[2].indexOf("=")+1);
                    long id = System.currentTimeMillis();//生产资源id,如果需要唯一性,可以采用UUID
                    FileLog log = null;
                    if(sourceid!=null && !"".equals(sourceid)){
                        id = Long.valueOf(sourceid);
                        log = find(id);//查找上传的文件是否存在上传记录
                    }
                    File file = null;
                    int position = 0;
                    if(log==null){//如果不存在上传记录,为文件添加跟踪记录
                        String path = new SimpleDateFormat("yyyy/MM/dd/HH/mm").format(new Date());
                        File dir = new File("file/"+ path);
                        if(!dir.exists()) dir.mkdirs();
                        file = new File(dir, filename);
                        if(file.exists()){//如果上传的文件发生重名,然后进行改名
                            filename = filename.substring(0, filename.indexOf(".")-1)+ dir.listFiles().length+ filename.substring(filename.indexOf("."));
                            file = new File(dir, filename);
                        }
                        save(id, file);
                    }else{// 如果存在上传记录,读取已经上传的数据长度
                        file = new File(log.getPath());//从上传记录中得到文件的路径
                        if(file.exists()){
                            File logFile = new File(file.getParentFile(), file.getName()+".log");
                            if(logFile.exists()){
                                Properties properties = new Properties();
                                properties.load(new FileInputStream(logFile));
                                position = Integer.valueOf(properties.getProperty("length"));//读取已经上传的数据长度
                            }
                        }
                    }

                    OutputStream outStream = socket.getOutputStream();
                    String response = "hash="+ id+ ";pos="+ position+ "\r\n";
                    //服务器收到客户端的请求信息后,给客户端返回响应信息:sourceid=1274773833264;position=0
                    //sourceid由服务器端生成,唯一标识上传的文件,position指示客户端从文件的什么位置开始上传
                    outStream.write(response.getBytes());

                    RandomAccessFile fileOutStream = new RandomAccessFile(file, "rwd");
                    if(position==0) fileOutStream.setLength(Integer.valueOf(filelength));//设置文件长度
                    fileOutStream.seek(position);//指定从文件的特定位置开始写入数据
                    byte[] buffer = new byte[1024];
                    int len = -1;
                    int length = position;
                    while( (len=inStream.read(buffer)) != -1){//从输入流中读取数据写入到文件中
                        fileOutStream.write(buffer, 0, len);
                        length += len;
                        Properties properties = new Properties();
                        properties.put("length", String.valueOf(length));
                        FileOutputStream logFile = new FileOutputStream(new File(file.getParentFile(), file.getName()+".log"));
                        properties.store(logFile, null);//实时记录已经接收的文件长度
                        logFile.close();
                    }
                    if(length==fileOutStream.length()) delete(id);
                    fileOutStream.close();
                    inStream.close();
                    outStream.close();
                    file = null;

                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally{
                try {
                    if(socket!=null && !socket.isClosed()) socket.close();
                } catch (IOException e) {}
            }
        }
    }

    public FileLog find(Long sourceid){
        return datas.get(sourceid);
    }
    //保存上传记录
    public void save(Long id, File saveFile){
        //日后可以改成通过数据库存放
        datas.put(id, new FileLog(id, saveFile.getAbsolutePath()));
    }
    //当文件上传完毕,删除记录
    public void delete(long sourceid){
        if(datas.containsKey(sourceid)) datas.remove(sourceid);
    }

    private class FileLog{
        private Long id;
        private String path;
        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
        public String getPath() {
            return path;
        }
        public void setPath(String path) {
            this.path = path;
        }
        public FileLog(Long id, String path) {
            this.id = id;
            this.path = path;
        }
    }
}


class StreamTool {

    public static void save(File file, byte[] data) throws Exception {
        FileOutputStream outStream = new FileOutputStream(file);
        outStream.write(data);
        outStream.close();
    }

    static String readLine(PushbackInputStream in) throws IOException {
        char buf[] = new char[128];
        int room = buf.length;
        int offset = 0;
        int c;
        loop:       while (true) {
            switch (c = in.read()) {
                case -1:
                case '\n':
                    break loop;
                case '\r':
                    int c2 = in.read();
                    if ((c2 != '\n') && (c2 != -1)) in.unread(c2);
                    break loop;
                default:
                    if (--room < 0) {
                        char[] lineBuffer = buf;
                        buf = new char[offset + 128];
                        room = buf.length - offset - 1;
                        System.arraycopy(lineBuffer, 0, buf, 0, offset);

                    }
                    buf[offset++] = (char) c;
                    break;
            }
        }
        if ((c == -1) && (offset == 0)) return null;
        return String.copyValueOf(buf, 0, offset);
    }

    /**
     * 读取流
     * @param inStream
     * @return 字节数组
     * @throws Exception
     */
    public static byte[] readStream(InputStream inStream) throws Exception{
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = -1;
        while( (len=inStream.read(buffer)) != -1){
            outSteam.write(buffer, 0, len);
        }
        outSteam.close();
        inStream.close();
        return outSteam.toByteArray();
    }
}

打开终端或命令行提示符,输入以下命令运行 FileServer.java

javac FileServer.java && java FileServer

如果出现下面的文字则说明服务正常启动

服务器已经运行在 192.168.0.108:12345 ,等待客户端发送数据

你的 ip 可能不同

然后客户端就可以通过 192.168.0.108:12345 访问我们的 File Server 了

客户端

  1. 首先是界面布局,看效果图布局就好

  2. 断点续传需要保存一些数据,要用到 SQLite ,所以创建一个 DBHelper 来创建表,建表语句如下

    CREATE TABLE IF NOT EXISTS uploadlog (_id integer primary key autoincrement, path varchar(20), sourceid varchar(20))
    
  3. 然后还要定一个 Service 用于获取资源 id, 保存文件,删除文件

  4. Activity 中需要一个 handler 来更新上传进度条

实现

  1. 创建一个 空的 Android 项目 cn.twle.android.TCPUploadFile

  2. app 上点击右键,选择 Folder - Assets Folder 创建一个 assets 目录

  3. 下载 /static/download/python.chm 并且放到 assets 目录

  4. 修改 AndroidManifest.xml 中添加权限

    <!-- 往SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- 访问internet权限 -->
    <uses-permission android:name="android.permission.INTERNET"/>
    
  5. 修改 activity_main.xml 创建布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical"
        android:padding="5dp">
    
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="文件名"
            android:textSize="18sp" />
    
        <EditText
            android:id="@+id/edit_fname"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="python.chm" />
    
        <Button
            android:id="@+id/btn_upload"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="上传" />
    
        <Button
            android:id="@+id/btn_stop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="停止" />
    
        <ProgressBar
            android:id="@+id/pgbar"
            style="@android:style/Widget.ProgressBar.Horizontal"
            android:layout_width="fill_parent"
            android:layout_height="40px" />
    
        <TextView
            android:id="@+id/txt_result"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center" />
    </LinearLayout>
    
  6. MainActivity.java 同一目录下创建读写流工具类 StreamHelper.java

    package cn.twle.android.tcpuploadfile;
    
    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.PushbackInputStream;
    
      class StreamHelper {
    
        public static void save(File file, byte[] data) throws Exception {
            FileOutputStream outStream = new FileOutputStream(file);
            outStream.write(data);
            outStream.close();
        }
    
        static String readLine(PushbackInputStream in) throws IOException {
            char buf[] = new char[128];
            int room = buf.length;
            int offset = 0;
            int c;
            loop:       while (true) {
                switch (c = in.read()) {
                    case -1:
                    case '\n':
                        break loop;
                    case '\r':
                        int c2 = in.read();
                        if ((c2 != '\n') && (c2 != -1)) in.unread(c2);
                        break loop;
                    default:
                        if (--room < 0) {
                            char[] lineBuffer = buf;
                            buf = new char[offset + 128];
                            room = buf.length - offset - 1;
                            System.arraycopy(lineBuffer, 0, buf, 0, offset);
    
                        }
                        buf[offset++] = (char) c;
                        break;
                }
            }
            if ((c == -1) && (offset == 0)) return null;
            return String.copyValueOf(buf, 0, offset);
        }
    
        /**
         * 读取流
         * @param inStream
         * @return 字节数组
         * @throws Exception
         */
        public static byte[] readStream(InputStream inStream) throws Exception{
            ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = -1;
            while( (len=inStream.read(buffer)) != -1){
                outSteam.write(buffer, 0, len);
            }
            outSteam.close();
            inStream.close();
            return outSteam.toByteArray();
        }
    }
    
  7. 断点续传需要保存上传的进度,我们使用到数据库,所以定义一个数据库管理类: DBOpenHelper.java

    package cn.twle.android.tcpuploadfile;
    
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    
    public class DBOpenHelper extends SQLiteOpenHelper {
    
        public DBOpenHelper(Context context) {
            super(context, "ms_upload.db", null, 1);
        }
    
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE IF NOT EXISTS uploadlog (_id integer primary key autoincrement, path varchar(20), sourceid varchar(20))");
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
        }
    }
    
  8. 然后是数据库操作类 UploadHelper.java

    package cn.twle.android.tcpuploadfile;
    
    import android.content.Context;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    
    import java.io.File;
    
    public class UploadHelper {
        private DBOpenHelper dbOpenHelper;
    
        public UploadHelper(Context context) {
            dbOpenHelper = new DBOpenHelper(context);
        }
    
        public String getBindId(File file) {
            SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
            Cursor cursor = db.rawQuery("select sourceid from uploadlog where path=?", new String[]{file.getAbsolutePath()});
            if (cursor.moveToFirst()) {
                return cursor.getString(0);
            }
            return null;
        }
    
        public void save(String sourceid, File file) {
            SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
            db.execSQL("insert into uploadlog(path,sourceid) values(?,?)",
                    new Object[]{file.getAbsolutePath(), sourceid});
        }
    
        public void delete(File file) {
            SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
            db.execSQL("delete from uploadlog where path=?", new Object[]{file.getAbsolutePath()});
        }
    }
    
  9. 修改 MainActivity.java

    package cn.twle.android.tcpuploadfile;
    
    import android.content.Context;
    import android.os.Environment;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.ProgressBar;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.io.PushbackInputStream;
    import java.io.RandomAccessFile;
    import java.net.Socket;
    
    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        private EditText edit_fname;
        private Button btn_upload;
        private Button btn_stop;
        private ProgressBar pgbar;
        private TextView txt_result;
    
        private UploadHelper upHelper;
        private boolean flag = true;
    
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                pgbar.setProgress(msg.getData().getInt("length"));
                float num = (float) pgbar.getProgress() / (float) pgbar.getMax();
                int result = (int) (num * 100);
                txt_result.setText(result + "%");
                if (pgbar.getProgress() == pgbar.getMax()) {
                    Toast.makeText(MainActivity.this, "上传成功", Toast.LENGTH_SHORT).show();
                }
            }
        };
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            bindViews();
            upHelper = new UploadHelper(this);
    
            //  assets 下的 python.chm 拷贝到 sd
            File root = Environment.getExternalStorageDirectory();
            File dir = new File(root.getPath() + "/assets");
    
            if ( !dir.exists()) {
                Boolean ok = dir.mkdirs();
            }
    
            String modelFilePath = "python.chm";
            Assets2Sd(this, modelFilePath, dir.getPath() );
        }
    
        private void bindViews() {
            edit_fname = (EditText) findViewById(R.id.edit_fname);
            btn_upload = (Button) findViewById(R.id.btn_upload);
            btn_stop = (Button) findViewById(R.id.btn_stop);
            pgbar = (ProgressBar) findViewById(R.id.pgbar);
            txt_result = (TextView) findViewById(R.id.txt_result);
    
            btn_upload.setOnClickListener(this);
            btn_stop.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_upload:
                    String filename = edit_fname.getText().toString();
    
                    flag = true;
                    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                        File file = new File(Environment.getExternalStorageDirectory().getPath() + "/assets/",filename);
                        if (file.exists()) {
                            pgbar.setMax((int) file.length());
                            uploadFile(file);
                        } else {
                            Toast.makeText(MainActivity.this, "文件并不存在~", Toast.LENGTH_SHORT).show();
                        }
                    } else {
                        Toast.makeText(MainActivity.this, "SD卡不存在或者不可用", Toast.LENGTH_SHORT).show();
                    }
                    break;
                case R.id.btn_stop:
                    flag = false;
                    break;
            }
        }
    
        private void uploadFile(final File file) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        String sourceid = upHelper.getBindId(file);
                        Socket socket = new Socket("192.168.0.108", 12345);
                        OutputStream outStream = socket.getOutputStream();
                        String head = "Content-Length=" + file.length() + ";filename=" + file.getName()
                                + ";sourceid=" + (sourceid != null ? sourceid : "") + "\r\n";
                        outStream.write(head.getBytes());
    
                        PushbackInputStream inStream = new PushbackInputStream(socket.getInputStream());
                        String response = StreamHelper.readLine(inStream);
                        String[] items = response.split(";");
                        String responseSourceid = items[0].substring(items[0].indexOf("=") + 1);
                        String position = items[1].substring(items[1].indexOf("=") + 1);
                        if (sourceid == null) {//如果是第一次上传文件,在数据库中不存在该文件所绑定的资源id
                            upHelper.save(responseSourceid, file);
                        }
                        RandomAccessFile fileOutStream = new RandomAccessFile(file, "r");
                        fileOutStream.seek(Integer.valueOf(position));
                        byte[] buffer = new byte[1024];
                        int len = -1;
                        int length = Integer.valueOf(position);
                        while (flag && (len = fileOutStream.read(buffer)) != -1) {
                            outStream.write(buffer, 0, len);
                            length += len;//累加已经上传的数据长度
                            Message msg = new Message();
                            msg.getData().putInt("length", length);
                            handler.sendMessage(msg);
                        }
                        if (length == file.length()) upHelper.delete(file);
                        fileOutStream.close();
                        outStream.close();
                        inStream.close();
                        socket.close();
                    } catch (Exception e) {
                        Toast.makeText(MainActivity.this, "上传异常~", Toast.LENGTH_SHORT).show();
                    }
                }
            }).start();
        }
    
        /***
         * 调用方式
         *
         * String path = Environment.getExternalStorageDirectory().toString() + "/" + "Tianchaoxiong/useso";
           String modelFilePath = "Model/seeta_fa_v1.1.bin";
           Assets2Sd(this, modelFilePath, path + "/" + modelFilePath);
         *
         * @param context
         * @param fileAssetPath assets中的目录
         * @param SdPath 要复制到sd卡中的目录
         */
        public static void Assets2Sd(Context context, String fileAssetPath, String SdPath){
            //测试把文件直接复制到sd卡中 fileSdPath完整路径
    
            String dstFile = SdPath + "/" + fileAssetPath;
            File file = new File(dstFile);
            if (!file.exists()) {
                try {
    
                    InputStream myInput;
                    OutputStream myOutput = new FileOutputStream(dstFile);
                    myInput = context.getAssets().open(fileAssetPath);
                    byte[] buffer = new byte[1024];
                    int length = myInput.read(buffer);
                    while(length > 0)
                    {
                        myOutput.write(buffer, 0, length);
                        length = myInput.read(buffer);
                    }
    
                    myOutput.flush();
                    myInput.close();
                    myOutput.close();
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
            }
    
        }
    
    }
    

Android 基础教程

关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.