socket通信传输大文件

83 阅读4分钟

1. 公共常量类

public class TransferConstants {
    public static final int MAGIC_NUMBER = 0x12345678;
    public static final int HEADER_SIZE = 20; // 魔数+类型+序列号+长度
    
    // 包类型
    public static final int TYPE_META = 1;
    public static final int TYPE_DATA = 2;
    public static final int TYPE_ACK = 3;
    public static final int TYPE_END = 4;
    
    // 默认分片大小(1MB)
    public static final int DEFAULT_CHUNK_SIZE = 1024 * 1024;
    
    // 超时和重试
    public static final int ACK_TIMEOUT = 5000; // 5秒
    public static final int MAX_RETRY = 3; // 最大重试次数
}

2. 数据包封装类

public class DataPacket {
    public int type;
    public int seq;
    public byte[] data;
    
    public DataPacket(int type, int seq, byte[] data) {
        this.type = type;
        this.seq = seq;
        this.data = data;
    }
    
    // 序列化为字节流
    public byte[] toBytes() {
        ByteBuffer buffer = ByteBuffer.allocate(TransferConstants.HEADER_SIZE + (data != null ? data.length : 0) + 4);
        buffer.putInt(TransferConstants.MAGIC_NUMBER);
        buffer.putInt(type);
        buffer.putInt(seq);
        buffer.putInt(data != null ? data.length : 0);
        if (data != null) {
            buffer.put(data);
        }
        // 计算校验和
        CRC32 crc = new CRC32();
        crc.update(buffer.array(), 0, buffer.position());
        buffer.putInt((int)crc.getValue());
        return buffer.array();
    }
    
    // 从字节流解析
    public static DataPacket fromBytes(byte[] bytes) throws IOException {
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        
        // 校验魔数
        if (buffer.getInt() != TransferConstants.MAGIC_NUMBER) {
            throw new IOException("Invalid magic number");
        }
        
        DataPacket packet = new DataPacket(
            buffer.getInt(), // type
            buffer.getInt(), // seq
            null
        );
        
        int length = buffer.getInt();
        if (length > 0) {
            packet.data = new byte[length];
            buffer.get(packet.data);
        }
        
        // 校验CRC
        CRC32 crc = new CRC32();
        crc.update(bytes, 0, bytes.length - 4);
        int expectedCrc = buffer.getInt();
        if ((int)crc.getValue() != expectedCrc) {
            throw new IOException("CRC check failed");
        }
        
        return packet;
    }
}

3. 文件发送端实现

public class FileSender {
    private Socket socket;
    private File file;
    private TransferListener listener;
    private int chunkSize;
    
    public FileSender(Socket socket, File file, TransferListener listener) {
        this(socket, file, listener, TransferConstants.DEFAULT_CHUNK_SIZE);
    }
    
    public FileSender(Socket socket, File file, TransferListener listener, int chunkSize) {
        this.socket = socket;
        this.file = file;
        this.listener = listener;
        this.chunkSize = chunkSize;
    }
    
    public void send() throws IOException {
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        
        try (FileInputStream fis = new FileInputStream(file)) {
            // 1. 发送元数据
            sendMetadata(dos, file);
            
            // 2. 等待元数据ACK
            if (!waitForAck(dis, 0)) {
                throw new IOException("Metadata ACK timeout");
            }
            
            // 3. 分片发送文件
            byte[] buffer = new byte[chunkSize];
            int bytesRead;
            int seq = 1;
            int totalChunks = (int) Math.ceil((double) file.length() / chunkSize);
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                boolean ackReceived = false;
                int retryCount = 0;
                byte[] chunkData = Arrays.copyOf(buffer, bytesRead);
                
                while (!ackReceived && retryCount < TransferConstants.MAX_RETRY) {
                    try {
                        // 发送数据包
                        sendDataPacket(dos, seq, chunkData);
                        
                        // 更新进度
                        if (listener != null) {
                            listener.onProgress(seq, totalChunks);
                        }
                        
                        // 等待ACK
                        if (waitForAck(dis, seq)) {
                            ackReceived = true;
                            seq++;
                        } else {
                            retryCount++;
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new IOException("Transfer interrupted");
                    }
                }
                
                if (!ackReceived) {
                    throw new IOException("Failed after " + TransferConstants.MAX_RETRY + " retries");
                }
            }
            
            // 4. 发送结束包
            sendEndPacket(dos);
            
            // 5. 等待结束ACK
            if (!waitForAck(dis, 0)) {
                throw new IOException("End ACK timeout");
            }
            
            if (listener != null) {
                listener.onComplete();
            }
        }
    }
    
    private void sendMetadata(DataOutputStream dos, File file) throws IOException {
        JSONObject meta = new JSONObject();
        try {
            meta.put("name", file.getName());
            meta.put("size", file.length());
            meta.put("chunks", (int) Math.ceil((double) file.length() / chunkSize));
        } catch (JSONException e) {
            throw new IOException("Failed to create metadata", e);
        }
        
        DataPacket packet = new DataPacket(
            TransferConstants.TYPE_META,
            0, // 序列号0表示元数据
            meta.toString().getBytes(StandardCharsets.UTF_8)
        );
        dos.write(packet.toBytes());
        dos.flush();
    }
    
    private void sendDataPacket(DataOutputStream dos, int seq, byte[] data) throws IOException {
        DataPacket packet = new DataPacket(
            TransferConstants.TYPE_DATA,
            seq,
            data
        );
        dos.write(packet.toBytes());
        dos.flush();
    }
    
    private void sendEndPacket(DataOutputStream dos) throws IOException {
        DataPacket packet = new DataPacket(
            TransferConstants.TYPE_END,
            0,
            null
        );
        dos.write(packet.toBytes());
        dos.flush();
    }
    
    private boolean waitForAck(DataInputStream dis, int expectedSeq) throws IOException, InterruptedException {
        long startTime = System.currentTimeMillis();
        
        while (System.currentTimeMillis() - startTime < TransferConstants.ACK_TIMEOUT) {
            if (dis.available() > 0) {
                byte[] header = new byte[TransferConstants.HEADER_SIZE];
                dis.readFully(header);
                
                ByteBuffer buffer = ByteBuffer.wrap(header);
                if (buffer.getInt() == TransferConstants.MAGIC_NUMBER) {
                    int type = buffer.getInt();
                    int seq = buffer.getInt();
                    int length = buffer.getInt();
                    
                    if (type == TransferConstants.TYPE_ACK && seq == expectedSeq) {
                        // 读取剩余数据(如果有)和校验和
                        byte[] remaining = new byte[length + 4];
                        dis.readFully(remaining);
                        return true;
                    }
                }
            }
            Thread.sleep(100);
        }
        return false;
    }
    
    public interface TransferListener {
        void onProgress(int currentChunk, int totalChunks);
        void onComplete();
        void onError(String message);
    }
}

4. 文件接收端实现

public class FileReceiver {
    private Socket socket;
    private String saveDir;
    private TransferListener listener;
    
    public FileReceiver(Socket socket, String saveDir, TransferListener listener) {
        this.socket = socket;
        this.saveDir = saveDir;
        this.listener = listener;
    }
    
    public void receive() throws IOException {
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        
        // 1. 接收元数据
        DataPacket metaPacket = readPacket(dis);
        if (metaPacket.type != TransferConstants.TYPE_META) {
            throw new IOException("Expected metadata packet");
        }
        
        JSONObject meta;
        try {
            meta = new JSONObject(new String(metaPacket.data, StandardCharsets.UTF_8));
        } catch (JSONException e) {
            throw new IOException("Invalid metadata format", e);
        }
        
        String fileName = meta.getString("name");
        long fileSize = meta.getLong("size");
        int totalChunks = meta.getInt("chunks");
        
        // 发送元数据ACK
        sendAck(dos, 0);
        
        // 准备接收文件
        File file = new File(saveDir, fileName);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            int expectedSeq = 1;
            Set<Integer> receivedChunks = new HashSet<>();
            
            // 2. 接收数据包
            while (true) {
                DataPacket packet = readPacket(dis);
                
                switch (packet.type) {
                    case TransferConstants.TYPE_DATA:
                        if (packet.seq == expectedSeq) {
                            // 写入文件
                            fos.write(packet.data);
                            receivedChunks.add(packet.seq);
                            expectedSeq++;
                            
                            // 更新进度
                            if (listener != null) {
                                listener.onProgress(receivedChunks.size(), totalChunks);
                            }
                        } else if (packet.seq < expectedSeq) {
                            // 重复包,直接ACK
                        }
                        
                        // 发送ACK
                        sendAck(dos, packet.seq);
                        break;
                        
                    case TransferConstants.TYPE_END:
                        // 发送结束ACK
                        sendAck(dos, 0);
                        if (listener != null) {
                            listener.onComplete();
                        }
                        return;
                        
                    default:
                        throw new IOException("Unexpected packet type: " + packet.type);
                }
            }
        }
    }
    
    private DataPacket readPacket(DataInputStream dis) throws IOException {
        byte[] header = new byte[TransferConstants.HEADER_SIZE];
        dis.readFully(header);
        
        ByteBuffer buffer = ByteBuffer.wrap(header);
        if (buffer.getInt() != TransferConstants.MAGIC_NUMBER) {
            throw new IOException("Invalid magic number");
        }
        
        int type = buffer.getInt();
        int seq = buffer.getInt();
        int length = buffer.getInt();
        
        byte[] data = null;
        if (length > 0) {
            data = new byte[length];
            dis.readFully(data);
        }
        
        // 读取并校验CRC
        int crc = dis.readInt();
        CRC32 crc32 = new CRC32();
        crc32.update(header);
        if (data != null) {
            crc32.update(data);
        }
        
        if ((int)crc32.getValue() != crc) {
            throw new IOException("CRC check failed");
        }
        
        return new DataPacket(type, seq, data);
    }
    
    private void sendAck(DataOutputStream dos, int seq) throws IOException {
        DataPacket ack = new DataPacket(
            TransferConstants.TYPE_ACK,
            seq,
            null
        );
        dos.write(ack.toBytes());
        dos.flush();
    }
    
    public interface TransferListener {
        void onProgress(int receivedChunks, int totalChunks);
        void onComplete();
        void onError(String message);
    }
}

三、使用示例

1. 发送文件

new Thread(() -> {
    try {
        Socket socket = new Socket("192.168.1.100", 8888);
        File file = new File("/sdcard/largefile.zip");
        
        FileSender sender = new FileSender(socket, file, new FileSender.TransferListener() {
            @Override
            public void onProgress(int currentChunk, int totalChunks) {
                runOnUiThread(() -> {
                    progressBar.setMax(totalChunks);
                    progressBar.setProgress(currentChunk);
                });
            }
            
            @Override
            public void onComplete() {
                runOnUiThread(() -> Toast.makeText(this, "传输完成", Toast.LENGTH_SHORT).show());
            }
            
            @Override
            public void onError(String message) {
                runOnUiThread(() -> Toast.makeText(this, "错误: " + message, Toast.LENGTH_SHORT).show());
            }
        });
        
        sender.send();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

2. 接收文件

new Thread(() -> {
    try {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = serverSocket.accept();
        
        FileReceiver receiver = new FileReceiver(socket, Environment.getExternalStorageDirectory().getPath(), 
            new FileReceiver.TransferListener() {
                @Override
                public void onProgress(int receivedChunks, int totalChunks) {
                    runOnUiThread(() -> {
                        progressBar.setMax(totalChunks);
                        progressBar.setProgress(receivedChunks);
                    });
                }
                
                @Override
                public void onComplete() {
                    runOnUiThread(() -> Toast.makeText(this, "接收完成", Toast.LENGTH_SHORT).show());
                }
                
                @Override
                public void onError(String message) {
                    runOnUiThread(() -> Toast.makeText(this, "错误: " + message, Toast.LENGTH_SHORT).show());
                }
            });
        
        receiver.receive();
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();