Merge "添加了dockerfile文件以及修改了部分配置信息"
diff --git a/src/main/java/com/pt/controller/ResourceController.java b/src/main/java/com/pt/controller/ResourceController.java
index caf2050..8e08e16 100644
--- a/src/main/java/com/pt/controller/ResourceController.java
+++ b/src/main/java/com/pt/controller/ResourceController.java
@@ -1,10 +1,8 @@
 package com.pt.controller;
 
-import com.pt.Item.ResourceInfo;
 import com.pt.constant.Constants;
 import com.pt.entity.Resource;
 import com.pt.entity.User;
-import com.pt.service.DownloadService;
 import com.pt.service.ResourceService;
 import com.pt.service.UserService;
 import com.pt.utils.JWTUtils;
@@ -12,10 +10,11 @@
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.ArrayList;
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.io.File;
 
 @RestController
 @RequestMapping("/api/resource")
@@ -25,7 +24,6 @@
     @Autowired
     private ResourceService resourceService;
     private UserService userService;
-    private DownloadService downloadService;
 
     @GetMapping("/list/all")
     public ResponseEntity<?> getAllResources(@RequestHeader("token") String token,
@@ -41,27 +39,7 @@
         if (resources.isEmpty()) {
             return ResponseEntity.noContent().build();
         }
-
-        List<ResourceInfo> resourceInfos = new ArrayList<>();
-        for (Resource resource : resources) {
-            ResourceInfo resourceInfo = new ResourceInfo();
-            resourceInfo.setResourceId(resource.getResourceId());
-            resourceInfo.setName(resource.getName());
-            resourceInfo.setSize(resource.getSize());
-            resourceInfo.setDescription(resource.getDescription());
-            resourceInfo.setAuthor(resource.getAuthor());
-            resourceInfo.setPublishTime(resource.getPublishTime());
-
-            int downloadCount = downloadService.searchByResourceId(resource.getResourceId()).size();
-            int seedCount = 0; // TODO: 需要根据tracker信息计算该资源的种子树
-            resourceInfo.setDownloadCount(downloadCount);
-            resourceInfo.setSeedCount(seedCount);
-            resourceInfos.add(resourceInfo);
-        }
-
-        ans.put("message", "Resources retrieved successfully");
-        ans.put("resources", resourceInfos);
-        return ResponseEntity.ok(ans);
+        return ResponseEntity.ok(resources);
     }
 
     @GetMapping("/list/user")
@@ -89,29 +67,30 @@
                                              @RequestParam("description") String description) {
         Map<String, Object> ans = new HashMap<>();
 
-        if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)){
+        if (!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
             ans.put("result", "Invalid token");
             return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(username);
-        if(user.getLevel() < 2){
+        if (user == null || user.getLevel() < 2) {
             ans.put("result", "Insufficient permissions to publish resources");
             return ResponseEntity.status(403).body(ans);
         }
 
-        /*
-            * TODO: 在这里实现资源发布的逻辑
-            *  需要将资源同步到Tracker服务器
-            *  种子文件需要保存
-         */
-
-        resourceService.publishResource(name, description, username, size);
+        try {
+            resourceService.publishResource(name, description, username, size);
+        } catch (Exception e) {
+            ans.put("result", "Failed to publish resource: " + e.getMessage());
+            return ResponseEntity.status(500).body(ans);
+        }
 
         ans.put("result", "Resource published successfully");
         return ResponseEntity.ok(ans);
     }
 
+
+
     @GetMapping("/get/{resourceId}")
     public ResponseEntity<?> getResourceById(@PathVariable("resourceId") int resourceId,
                                               @RequestHeader("token") String token,
@@ -133,7 +112,7 @@
     @GetMapping("/download/{resourceId}")
     public ResponseEntity<?> downloadResource(@PathVariable("resourceId") int resourceId,
                                               @RequestHeader("token") String token,
-                                              @RequestParam("username") String username) {
+                                              @RequestParam("username") String username) throws IOException {
 
         Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
@@ -141,13 +120,16 @@
             return ResponseEntity.badRequest().body(ans);
         }
 
-        /*
-            * TODO: 在这里实现下载资源的方法
-            *       从本地的种子文件目录获取种子文件
-         */
+        Resource resource = resourceService.getResourceById(resourceId);
+        byte[] file = resourceService.getTorrentFileByResource(resource, username);
+        if (file == null) {
+            return ResponseEntity.notFound().build();
+        }
 
-        // Here you would typically return the file or a download link
-        return ResponseEntity.ok(ans);
+        return ResponseEntity.ok()
+                .header("Content-Type", "application/x-bittorrent")
+                .header("Content-Disposition", "attachment; filename=\"" + resource.getName() + ".torrent\"")
+                .body(file);
     }
 
     @GetMapping("/search")
@@ -176,19 +158,19 @@
         Resource resource = resourceService.getResourceById(resourceId);
 
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.ADMIN) || resource == null || !resource.getAuthor().equals(username)) {
-            ans.put("result", "Invalid token");
+            ans.put("result", "Invalid token or insufficient permissions");
             return ResponseEntity.badRequest().body(ans);
         }
 
-        /*
-            * TODO: 在这里实现删除资源的方法
-            *  需要同步到Tracker服务器
-         */
-
-
-        resourceService.deleteResource(resourceId);
+        try {
+            resourceService.deleteResource(resourceId);
+        } catch (Exception e) {
+            ans.put("result", "Failed to delete resource: " + e.getMessage());
+            return ResponseEntity.status(500).body(ans);
+        }
 
         ans.put("result", "Resource deleted successfully");
         return ResponseEntity.ok(ans);
     }
+
 }
diff --git a/src/main/java/com/pt/entity/Download.java b/src/main/java/com/pt/entity/Download.java
index 1ae9354..38b955a 100644
--- a/src/main/java/com/pt/entity/Download.java
+++ b/src/main/java/com/pt/entity/Download.java
@@ -14,14 +14,14 @@
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private int downloadId;
 
-    private int resourceId;
+    private String resourceId;
     private String downloader;
     private LocalDateTime downloadTime;
 
     public Download() {
     }
 
-    public Download(int downloadId, int resourceId, String downloader, LocalDateTime downloadTime) {
+    public Download(int downloadId, String resourceId, String downloader, LocalDateTime downloadTime) {
         this.downloadId = downloadId;
         this.resourceId = resourceId;
         this.downloader = downloader;
@@ -34,10 +34,10 @@
     public void setDownloadId(int downloadId) {
         this.downloadId = downloadId;
     }
-    public int getResourceId() {
+    public String getResourceId() {
         return resourceId;
     }
-    public void setResourceId(int resourceId) {
+    public void setResourceId(String resourceId) {
         this.resourceId = resourceId;
     }
     public String getDownloader() {
diff --git a/src/main/java/com/pt/entity/Resource.java b/src/main/java/com/pt/entity/Resource.java
index 2d8567b..9b7da39 100644
--- a/src/main/java/com/pt/entity/Resource.java
+++ b/src/main/java/com/pt/entity/Resource.java
@@ -21,6 +21,7 @@
     private String author;
 
     private String description;
+    private byte[] torrentData;
 
     public Resource() {
     }
@@ -69,6 +70,13 @@
     public void setDescription(String description) {
         this.description = description;
     }
+    public byte[] getTorrentData() {
+        return torrentData;
+    }
+
+    public void setTorrentData(byte[] torrentData) {
+        this.torrentData = torrentData;
+    }
 
     /*
         * 重写toString方法,将资源信息以JSON字符串形式返回
diff --git a/src/main/java/com/pt/entity/TorrentMeta.java b/src/main/java/com/pt/entity/TorrentMeta.java
new file mode 100644
index 0000000..3823181
--- /dev/null
+++ b/src/main/java/com/pt/entity/TorrentMeta.java
@@ -0,0 +1,60 @@
+package com.pt.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.time.LocalDateTime;
+
+@Entity
+public class TorrentMeta {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String filename;
+    private String infoHash;
+    private Long size;
+    private LocalDateTime uploadTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    public String getInfoHash() {
+        return infoHash;
+    }
+
+    public void setInfoHash(String infoHash) {
+        this.infoHash = infoHash;
+    }
+
+    public Long getSize() {
+        return size;
+    }
+
+    public void setSize(Long size) {
+        this.size = size;
+    }
+
+    public LocalDateTime getUploadTime() {
+        return uploadTime;
+    }
+
+    public void setUploadTime(LocalDateTime uploadTime) {
+        this.uploadTime = uploadTime;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/repository/DownloadRepository.java b/src/main/java/com/pt/repository/DownloadRepository.java
index 360a1c0..741f142 100644
--- a/src/main/java/com/pt/repository/DownloadRepository.java
+++ b/src/main/java/com/pt/repository/DownloadRepository.java
@@ -12,7 +12,7 @@
      * @param resourceId the ID of the resource
      * @return a list of downloads associated with the specified resource
      */
-    List<Download> findByResourceId(int resourceId);
+    List<Download> findByResourceId(String resourceId);
 
     /**
      * Finds downloads by the downloader's username.
diff --git a/src/main/java/com/pt/repository/TorrentMetaRepository.java b/src/main/java/com/pt/repository/TorrentMetaRepository.java
new file mode 100644
index 0000000..839fc01
--- /dev/null
+++ b/src/main/java/com/pt/repository/TorrentMetaRepository.java
@@ -0,0 +1,10 @@
+package com.pt.repository;
+
+import com.pt.entity.TorrentMeta;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TorrentMetaRepository extends JpaRepository<TorrentMeta, Long> {
+    TorrentMeta findByInfoHash(String infoHash);
+
+    TorrentMeta findByFilename(String filename);
+}
diff --git a/src/main/java/com/pt/service/DownloadService.java b/src/main/java/com/pt/service/DownloadService.java
index c02aea1..43b80e6 100644
--- a/src/main/java/com/pt/service/DownloadService.java
+++ b/src/main/java/com/pt/service/DownloadService.java
@@ -13,14 +13,12 @@
     @Autowired
     private DownloadRepository downloadRepository;
 
-    public DownloadService(DownloadRepository downloadRepository) {
-        this.downloadRepository = downloadRepository;
+    public void save(Download download) {
+        downloadRepository.save(download);
     }
 
-    public List<Download> searchByResourceId(int resourceId) {
+    public List<Download> getByResourceId(String resourceId) {
         return downloadRepository.findByResourceId(resourceId);
     }
-    /*
-        TODO: 添加下载需要的服务;
-     */
+
 }
diff --git a/src/main/java/com/pt/service/ResourceService.java b/src/main/java/com/pt/service/ResourceService.java
index a91bc7d..1d0e797 100644
--- a/src/main/java/com/pt/service/ResourceService.java
+++ b/src/main/java/com/pt/service/ResourceService.java
@@ -1,12 +1,21 @@
 package com.pt.service;
 
+import com.pt.entity.Download;
 import com.pt.entity.Resource;
+import com.pt.entity.TorrentMeta;
+import com.pt.repository.DownloadRepository;
 import com.pt.repository.ResourceRepository;
+import com.pt.repository.TorrentMetaRepository;
+import com.pt.utils.BencodeUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 @Service
 public class ResourceService {
@@ -14,37 +23,74 @@
     @Autowired
     private ResourceRepository resourceRepository;
 
-    public ResourceService(ResourceRepository resourceRepository) {
-        this.resourceRepository = resourceRepository;
-    }
+    @Autowired
+    private DownloadRepository downloadRepository;
+
+    @Autowired
+    private TorrentService torrentService;
+
+    @Autowired
+    private TorrentMetaRepository torrentMetaRepository;
 
     public List<Resource> getAllResources() {
         return resourceRepository.findAll();
     }
 
-    public void publishResource(String name, String description, String publisher, double size) {
+    public List<Resource> getResourcesByAuthor(String username) {
+        return resourceRepository.findByAuthor(username);
+    }
+
+    // 发布资源时,将种子文件二进制数据保存到数据库
+    public void publishResource(String name, String description, String author, double size) throws Exception {
+        // 保存资源信息到数据库,包括种子文件的二进制数据
         Resource resource = new Resource();
         resource.setName(name);
-        resource.setSize(size);
         resource.setDescription(description);
-        resource.setAuthor(publisher);
+        resource.setAuthor(author);
+        resource.setSize(size);
         resource.setPublishTime(LocalDateTime.now());
+
+        // 生成种子数据 byte[]
+        byte[] torrentData = torrentService.generateTorrentBytes("uploads/" + name);
+        resource.setTorrentData(torrentData);
+
         resourceRepository.save(resource);
     }
 
-    public Resource getResourceById(int resourceId) {
-        return resourceRepository.findById(resourceId).orElse(null);
+    // 获取资源时,返回BLOB字段内容作为torrent文件
+    public byte[] getTorrentFileByResource(Resource resource, String username) {
+        if(resource == null || resource.getTorrentData() == null) return null;
+
+        // 记录下载日志
+        Download download = new Download();
+        download.setResourceId(String.valueOf(resource.getResourceId()));
+        download.setDownloader(username);
+        download.setDownloadTime(LocalDateTime.now());
+        downloadRepository.save(download);
+
+        return resource.getTorrentData();
     }
 
+    public Resource getResourceById(int id) {
+        return resourceRepository.findById(id).orElse(null);
+    }
+
+    public void deleteResource(int id) {
+        Resource resource = getResourceById(id);
+        if (resource != null) {
+            // 删除数据库资源记录
+            resourceRepository.deleteById(id);
+
+            // 删除对应的 TorrentMeta 元信息
+            TorrentMeta meta = torrentMetaRepository.findByFilename(resource.getName());
+            if (meta != null) {
+                torrentMetaRepository.delete(meta);
+            }
+
+        }
+    }
     public List<Resource> searchByQuery(String query) {
         return resourceRepository.findByNameContainingIgnoreCase(query);
     }
 
-    public List<Resource> getResourcesByAuthor(String author) {
-        return resourceRepository.findByAuthor(author);
-    }
-
-    public void deleteResource(int resourceId) {
-        resourceRepository.deleteById(resourceId);
-    }
 }
diff --git a/src/main/java/com/pt/service/TorrentService.java b/src/main/java/com/pt/service/TorrentService.java
new file mode 100644
index 0000000..dcbee69
--- /dev/null
+++ b/src/main/java/com/pt/service/TorrentService.java
@@ -0,0 +1,83 @@
+package com.pt.service;
+
+import com.pt.entity.TorrentMeta;
+import com.pt.repository.TorrentMetaRepository;
+import com.pt.utils.BencodeUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Service
+public class TorrentService {
+
+    //  Tracker 服务器的 announce 地址
+    private static final String ANNOUNCE_URL = "http://localhost:8080/announce";
+    private static final int PIECE_LENGTH = 256 * 1024;
+
+    @Autowired
+    private TorrentMetaRepository torrentMetaRepository;
+
+    public byte[] generateTorrentBytes(String sourceFilePath) throws Exception {
+        File sourceFile = new File(sourceFilePath);
+
+        // 构造 info 字典
+        Map<String, Object> infoDict = new LinkedHashMap<>();
+        infoDict.put("name", sourceFile.getName());
+        infoDict.put("length", sourceFile.length());
+        infoDict.put("piece length", PIECE_LENGTH);
+        infoDict.put("pieces", calcPiecesHashes(sourceFile));
+
+        // 构造完整 torrent 字典
+        Map<String, Object> torrentDict = new LinkedHashMap<>();
+        torrentDict.put("announce", ANNOUNCE_URL);
+        torrentDict.put("info", infoDict);
+
+        // 编码成种子数据字节数组
+        byte[] bencodedTorrent = BencodeUtils.encode(torrentDict);
+
+        // 计算 info_hash 并保存到数据库(如果需要的话)
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        byte[] infoEncoded = BencodeUtils.encode(infoDict);
+        sha1.update(infoEncoded);
+        String infoHash = bytesToHex(sha1.digest());
+
+        TorrentMeta meta = new TorrentMeta();
+        meta.setFilename(sourceFile.getName());
+        meta.setInfoHash(infoHash);
+        meta.setSize(sourceFile.length());
+        meta.setUploadTime(LocalDateTime.now());
+        torrentMetaRepository.save(meta);
+
+        return bencodedTorrent;
+    }
+
+
+    private byte[] calcPiecesHashes(File file) throws Exception {
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        try (InputStream fis = new FileInputStream(file)) {
+            byte[] buffer = new byte[PIECE_LENGTH];
+            ByteArrayOutputStream piecesBuffer = new ByteArrayOutputStream();
+
+            int read;
+            while ((read = fis.read(buffer)) > 0) {
+                sha1.reset();
+                sha1.update(buffer, 0, read);
+                piecesBuffer.write(sha1.digest());
+            }
+            return piecesBuffer.toByteArray();
+        }
+    }
+
+    private String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/utils/BdecodeUtils.java b/src/main/java/com/pt/utils/BdecodeUtils.java
new file mode 100644
index 0000000..c77f8b4
--- /dev/null
+++ b/src/main/java/com/pt/utils/BdecodeUtils.java
@@ -0,0 +1,84 @@
+package com.pt.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+public class BdecodeUtils {
+    public static Object decode(byte[] data) throws IOException {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
+            return decodeNext(in);
+        }
+    }
+
+    private static Object decodeNext(InputStream in) throws IOException {
+        int prefix = in.read();
+        if (prefix == -1) {
+            throw new IOException("Unexpected end of stream");
+        }
+
+        if (prefix >= '0' && prefix <= '9') {
+            // 字符串,回退一个字节给parseString处理
+            in.reset();
+            return parseString(in, prefix);
+        } else if (prefix == 'i') {
+            return parseInteger(in);
+        } else if (prefix == 'l') {
+            return parseList(in);
+        } else if (prefix == 'd') {
+            return parseDict(in);
+        } else {
+            throw new IOException("Invalid bencode prefix: " + (char) prefix);
+        }
+    }
+
+    private static String parseString(InputStream in, int firstDigit) throws IOException {
+        // 读长度前缀
+        StringBuilder lenStr = new StringBuilder();
+        lenStr.append((char) firstDigit);
+        int b;
+        while ((b = in.read()) != -1 && b != ':') {
+            lenStr.append((char) b);
+        }
+        int length = Integer.parseInt(lenStr.toString());
+
+        // 读内容
+        byte[] buf = new byte[length];
+        int read = in.read(buf);
+        if (read < length) throw new IOException("Unexpected end of stream reading string");
+        return new String(buf);
+    }
+
+    private static long parseInteger(InputStream in) throws IOException {
+        StringBuilder intStr = new StringBuilder();
+        int b;
+        while ((b = in.read()) != -1 && b != 'e') {
+            intStr.append((char) b);
+        }
+        return Long.parseLong(intStr.toString());
+    }
+
+    private static List<Object> parseList(InputStream in) throws IOException {
+        List<Object> list = new ArrayList<>();
+        int b;
+        while ((b = in.read()) != 'e') {
+            in.reset();
+            list.add(decodeNext(in));
+        }
+        return list;
+    }
+
+    private static Map<String, Object> parseDict(InputStream in) throws IOException {
+        Map<String, Object> map = new LinkedHashMap<>();
+        int b;
+        while ((b = in.read()) != 'e') {
+            in.reset();
+            String key = (String) decodeNext(in);
+            Object value = decodeNext(in);
+            map.put(key, value);
+        }
+        return map;
+    }
+}
+
diff --git a/src/main/java/com/pt/utils/BencodeUtils.java b/src/main/java/com/pt/utils/BencodeUtils.java
new file mode 100644
index 0000000..2d3850d
--- /dev/null
+++ b/src/main/java/com/pt/utils/BencodeUtils.java
@@ -0,0 +1,107 @@
+package com.pt.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class BencodeUtils {
+
+    // 通用bencode编码接口
+    public static void encode(Object obj, OutputStream out) throws IOException {
+        if (obj instanceof String) {
+            encodeString((String) obj, out);
+        } else if (obj instanceof Number) {
+            encodeInteger(((Number) obj).longValue(), out);
+        } else if (obj instanceof byte[]) {
+            encodeBytes((byte[]) obj, out);
+        } else if (obj instanceof List) {
+            encodeList((List<?>) obj, out);
+        } else if (obj instanceof Map) {
+            encodeMap((Map<String, Object>) obj, out);
+        } else {
+            throw new IllegalArgumentException("Unsupported type: " + obj.getClass());
+        }
+    }
+
+    public static byte[] encode(Object obj) {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            encode(obj, baos);
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void encodeString(String s, OutputStream out) throws IOException {
+        byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
+        out.write(String.valueOf(bytes.length).getBytes(StandardCharsets.US_ASCII));
+        out.write(':');
+        out.write(bytes);
+    }
+
+    private static void encodeBytes(byte[] bytes, OutputStream out) throws IOException {
+        out.write(String.valueOf(bytes.length).getBytes(StandardCharsets.US_ASCII));
+        out.write(':');
+        out.write(bytes);
+    }
+
+    private static void encodeInteger(long value, OutputStream out) throws IOException {
+        out.write('i');
+        out.write(Long.toString(value).getBytes(StandardCharsets.US_ASCII));
+        out.write('e');
+    }
+
+    private static void encodeList(List<?> list, OutputStream out) throws IOException {
+        out.write('l');
+        for (Object item : list) {
+            encode(item, out);
+        }
+        out.write('e');
+    }
+
+    private static void encodeMap(Map<String, Object> map, OutputStream out) throws IOException {
+        out.write('d');
+        List<String> keys = new ArrayList<>(map.keySet());
+        Collections.sort(keys);  // bencode字典必须按key排序
+        for (String key : keys) {
+            encodeString(key, out);
+            encode(map.get(key), out);
+        }
+        out.write('e');
+    }
+
+    // 构造单个compact peer的二进制格式 (4字节IP + 2字节端口)
+    public static byte[] buildCompactPeer(String ip, int port) {
+        try {
+            InetAddress addr = InetAddress.getByName(ip);
+            ByteBuffer buffer = ByteBuffer.allocate(6);
+            buffer.put(addr.getAddress());
+            buffer.putShort((short) port);
+            return buffer.array();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 构造多个compact peer的二进制拼接
+    public static byte[] buildCompactPeers(List<String> ips, List<Integer> ports) {
+        if (ips.size() != ports.size()) throw new IllegalArgumentException("IPs and ports list size mismatch");
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (int i = 0; i < ips.size(); i++) {
+            out.write(buildCompactPeer(ips.get(i), ports.get(i)), 0, 6);
+        }
+        return out.toByteArray();
+    }
+
+    // 构造tracker响应字典,至少包含interval和peers
+    public static byte[] buildTrackerResponse(int interval, byte[] peersCompact) {
+        Map<String, Object> dict = new LinkedHashMap<>();
+        dict.put("interval", interval);
+        dict.put("peers", peersCompact);
+        return encode(dict);
+    }
+}