Merge branch 'master' of https://gerrit.lilingkun.com/G12Project
Change-Id: I72535eda24bb4ee2990694006f5e2dc306e31248
diff --git a/src/main/java/com/pt/controller/AdminController.java b/src/main/java/com/pt/controller/AdminController.java
index 1ddfc92..22e2b86 100644
--- a/src/main/java/com/pt/controller/AdminController.java
+++ b/src/main/java/com/pt/controller/AdminController.java
@@ -31,7 +31,9 @@
Admin admin = adminService.findByUsernameAndPassword(username, password);
if (admin != null) {
ans.put("result", "Login successful");
- ans.put("token", JWTUtils.generateToken(username, Constants.UserRole.ADMIN, (int)1.2e8));
+ ans.put("data", Map.of(
+ "token", JWTUtils.generateToken(username, Constants.UserRole.ADMIN, (int)1.2e8)
+ ));
return ResponseEntity.ok().body(ans);
} else {
ans.put("result", "Invalid username or password");
diff --git a/src/main/java/com/pt/controller/CommentController.java b/src/main/java/com/pt/controller/CommentController.java
index a85d131..17dabb3 100644
--- a/src/main/java/com/pt/controller/CommentController.java
+++ b/src/main/java/com/pt/controller/CommentController.java
@@ -72,7 +72,9 @@
List<Comment> comments = commentService.getCommentsByPostId(postId);
ans.put("result", "Comments retrieved successfully");
- ans.put("comments", comments);
+ ans.put("data", Map.of(
+ "comments", comments
+ ));
return ResponseEntity.ok(ans);
}
}
diff --git a/src/main/java/com/pt/controller/PostController.java b/src/main/java/com/pt/controller/PostController.java
index 387cc2d..0ab86ed 100644
--- a/src/main/java/com/pt/controller/PostController.java
+++ b/src/main/java/com/pt/controller/PostController.java
@@ -92,7 +92,9 @@
posts.removeIf(post -> !post.getPublishDate().toString().equals(date));
}
ans.put("result", "Post retrieved successfully");
- ans.put("post", posts);
+ ans.put("data", Map.of(
+ "post", posts
+ ));
return ResponseEntity.ok(ans);
}
diff --git a/src/main/java/com/pt/controller/ResourceController.java b/src/main/java/com/pt/controller/ResourceController.java
index 8e08e16..411e7bc 100644
--- a/src/main/java/com/pt/controller/ResourceController.java
+++ b/src/main/java/com/pt/controller/ResourceController.java
@@ -9,6 +9,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
@@ -60,11 +61,14 @@
}
@PostMapping("/publish")
- public ResponseEntity<?> publishResource(@RequestHeader("token") String token,
- @RequestParam("username") String username,
- @RequestParam("size") double size,
- @RequestParam("name") String name,
- @RequestParam("description") String description) {
+ public ResponseEntity<?> publishResource(
+ @RequestHeader("token") String token,
+ @RequestParam("username") String username,
+ @RequestParam("size") double size,
+ @RequestParam("name") String name,
+ @RequestParam("description") String description,
+ @RequestParam("torrent") MultipartFile torrentFile) {
+
Map<String, Object> ans = new HashMap<>();
if (!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
@@ -79,7 +83,8 @@
}
try {
- resourceService.publishResource(name, description, username, size);
+ // 传入种子文件字节,同时传入资源其他信息
+ resourceService.publishResource(name, description, username, size, torrentFile.getBytes());
} catch (Exception e) {
ans.put("result", "Failed to publish resource: " + e.getMessage());
return ResponseEntity.status(500).body(ans);
@@ -91,6 +96,7 @@
+
@GetMapping("/get/{resourceId}")
public ResponseEntity<?> getResourceById(@PathVariable("resourceId") int resourceId,
@RequestHeader("token") String token,
diff --git a/src/main/java/com/pt/controller/TrackerController.java b/src/main/java/com/pt/controller/TrackerController.java
new file mode 100644
index 0000000..1e0302d
--- /dev/null
+++ b/src/main/java/com/pt/controller/TrackerController.java
@@ -0,0 +1,36 @@
+package com.pt.controller;
+
+import com.pt.service.TrackerService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RestController
+public class TrackerController {
+
+ @Autowired
+ private TrackerService trackerService;
+
+ @GetMapping("/announce")
+ public void announce(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ try {
+ String ip = request.getRemoteAddr();
+ Map<String, String[]> params = request.getParameterMap();
+
+ byte[] bencodedResponse = trackerService.handleAnnounce(params, ip);
+
+ response.setContentType("application/x-bittorrent");
+ response.getOutputStream().write(bencodedResponse);
+ } catch (Exception e) {
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ response.setContentType("text/plain");
+ response.getWriter().write("Tracker internal error: " + e.getMessage());
+ }
+ }
+
+}
+
diff --git a/src/main/java/com/pt/controller/UserController.java b/src/main/java/com/pt/controller/UserController.java
index 3978831..7603f9d 100644
--- a/src/main/java/com/pt/controller/UserController.java
+++ b/src/main/java/com/pt/controller/UserController.java
@@ -43,8 +43,7 @@
userService.save(newUser);
Map<String, Object> ans = new HashMap<>();
- ans.put("success", true);
- ans.put("message", "User registered successfully");
+ ans.put("result", "User registered successfully");
ans.put("data", newUser);
return ResponseEntity.ok().body(ans);
@@ -52,9 +51,9 @@
}
@PostMapping("/login")
- public ResponseEntity<?> loginUser(@RequestBody Map<String, String> request) {
- String username = request.get("username");
- String password = request.get("password");
+ public ResponseEntity<?> loginUser(@RequestParam("username") String username,
+ @RequestParam("password") String password) {
+
if (username == null || password == null) {
return ResponseEntity.badRequest().body("Missing username or password");
@@ -64,16 +63,11 @@
Map<String, Object> ans = new HashMap<>();
if (user != null) {
String token = JWTUtils.generateToken(username, Constants.UserRole.USER, Constants.DEFAULT_EXPIRE_TIME);
- ans.put("success", true);
- ans.put("message", "Login successful");
- ans.put("data", Map.of(
- "token", token,
- "user", user
- ));
+ ans.put("result", "Login successful");
+ ans.put("data", token);
return ResponseEntity.ok().body(ans);
} else {
- ans.put("success", false);
- ans.put("message", "Invalid username or password");
+ ans.put("result", "Invalid username or password");
return ResponseEntity.badRequest().body(ans);
}
}
@@ -82,17 +76,21 @@
public ResponseEntity<?> updateUsername(@RequestHeader("token") String token,
@RequestParam("username") String oldUsername,
@RequestParam("newUsername") String newUsername) {
+ Map<String, Object> ans = new HashMap<>();
if(!JWTUtils.checkToken(token, oldUsername, Constants.UserRole.USER)) {
- return ResponseEntity.badRequest().body("Invalid token");
+ ans.put("result", "Invalid token");
+ return ResponseEntity.badRequest().body(ans);
}
User user = userService.findByUsername(oldUsername);
if (user != null) {
user.setUsername(newUsername);
userService.save(user);
- return ResponseEntity.ok("Username updated successfully");
+ ans.put("result", "Username updated successfully");
+ return ResponseEntity.ok(ans);
} else {
- return ResponseEntity.badRequest().body("User not found");
+ ans.put("result", "User not found");
+ return ResponseEntity.badRequest().body(ans);
}
}
@@ -100,17 +98,21 @@
public ResponseEntity<?> updatePassword(@RequestHeader("token") String token,
@RequestParam("username") String username,
@RequestParam("newPassword") String newPassword) {
+ Map<String, Object> ans = new HashMap<>();
if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
- return ResponseEntity.badRequest().body("Invalid token");
+ ans.put("result", "Invalid token");
+ return ResponseEntity.badRequest().body(ans);
}
User user = userService.findByUsername(username);
if (user != null) {
user.setPassword(newPassword);
userService.save(user);
- return ResponseEntity.ok("Password updated successfully");
+ ans.put("result", "Password updated successfully");
+ return ResponseEntity.ok(ans);
} else {
- return ResponseEntity.badRequest().body("Invalid username or password");
+ ans.put("result", "Invalid username or password");
+ return ResponseEntity.badRequest().body(ans);
}
}
@@ -118,17 +120,22 @@
public ResponseEntity<?> updateEmail(@RequestHeader("token") String token,
@RequestParam("username") String username,
@RequestParam("newEmail") String newEmail) {
+
+ Map<String, Object> ans = new HashMap<>();
if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
- return ResponseEntity.badRequest().body("Invalid token");
+ ans.put("result", "Invalid token");
+ return ResponseEntity.badRequest().body(ans);
}
User user = userService.findByUsername(username);
if (user != null) {
user.setEmail(newEmail);
userService.save(user);
- return ResponseEntity.ok("Email updated successfully");
+ ans.put("result", "Email updated successfully");
+ return ResponseEntity.ok(ans);
} else {
- return ResponseEntity.badRequest().body("User not found");
+ ans.put("result", "User not found");
+ return ResponseEntity.badRequest().body(ans);
}
}
@@ -137,16 +144,20 @@
@RequestParam("username") String username,
@RequestParam("targetUsername") String targetUsername
) {
+ Map<String, Object> ans = new HashMap<>();
if(!JWTUtils.checkToken(token, username, Constants.UserRole.ADMIN)) {
- return ResponseEntity.badRequest().body("Invalid token");
+ ans.put("result", "Invalid token");
+ return ResponseEntity.badRequest().body(ans);
}
User user = userService.findByUsername(targetUsername);
if (user != null) {
userService.deleteById(user.getUid());
- return ResponseEntity.ok("User deleted successfully");
+ ans.put("result", "User deleted successfully");
+ return ResponseEntity.ok(ans);
} else {
- return ResponseEntity.badRequest().body("User not found");
+ ans.put("result", "User not found");
+ return ResponseEntity.badRequest().body(ans);
}
}
@@ -159,26 +170,30 @@
Map<String, Object> ans = new HashMap<>();
ans.put("result", "User list retrieved successfully");
- ans.put("amount", userService.listAll().size());
- ans.put("users", userService.listAll());
+ ans.put("data", Map.of(
+ "amount", userService.listAll().size(),
+ "users", userService.listAll()
+ ));
return ResponseEntity.ok(ans);
}
@GetMapping("/get/info")
public ResponseEntity<?> getUserInfo(@RequestHeader("token") String token,
@RequestParam("username") String username) {
+ Map<String, Object> ans = new HashMap<>();
if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
- return ResponseEntity.badRequest().body("Invalid token");
+ ans.put("result", "Invalid token");
+ return ResponseEntity.badRequest().body(ans);
}
User user = userService.findByUsername(username);
if (user != null) {
- Map<String, Object> ans = new HashMap<>();
ans.put("result", "User info retrieved successfully");
- ans.put("user", user);
+ ans.put("data", user);
return ResponseEntity.ok(ans);
} else {
- return ResponseEntity.badRequest().body("User not found");
+ ans.put("result", "User not found");
+ return ResponseEntity.badRequest().body(ans);
}
}
}
diff --git a/src/main/java/com/pt/entity/PeerInfoEntity.java b/src/main/java/com/pt/entity/PeerInfoEntity.java
new file mode 100644
index 0000000..081df7f
--- /dev/null
+++ b/src/main/java/com/pt/entity/PeerInfoEntity.java
@@ -0,0 +1,66 @@
+package com.pt.entity;
+
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+public class PeerInfoEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String infoHash;
+ private String ip;
+ private int port;
+ private String peerId;
+ private LocalDateTime lastSeen;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getInfoHash() {
+ return infoHash;
+ }
+
+ public void setInfoHash(String infoHash) {
+ this.infoHash = infoHash;
+ }
+
+ public String getIp() {
+ return ip;
+ }
+
+ public void setIp(String ip) {
+ this.ip = ip;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public LocalDateTime getLastSeen() {
+ return lastSeen;
+ }
+
+ public void setLastSeen(LocalDateTime lastSeen) {
+ this.lastSeen = lastSeen;
+ }
+
+ public String getPeerId() {
+ return peerId;
+ }
+
+ public void setPeerId(String peerId) {
+ this.peerId = peerId;
+ }
+}
diff --git a/src/main/java/com/pt/entity/TorrentMeta.java b/src/main/java/com/pt/entity/TorrentMeta.java
index 3823181..2c690f7 100644
--- a/src/main/java/com/pt/entity/TorrentMeta.java
+++ b/src/main/java/com/pt/entity/TorrentMeta.java
@@ -17,6 +17,7 @@
private String infoHash;
private Long size;
private LocalDateTime uploadTime;
+ private byte[] torrentData;
public Long getId() {
return id;
@@ -57,4 +58,12 @@
public void setUploadTime(LocalDateTime uploadTime) {
this.uploadTime = uploadTime;
}
+
+ public byte[] getTorrentData() {
+ return torrentData;
+ }
+
+ public void setTorrentData(byte[] torrentData) {
+ this.torrentData = torrentData;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/pt/repository/PeerInfoRepository.java b/src/main/java/com/pt/repository/PeerInfoRepository.java
new file mode 100644
index 0000000..565204c
--- /dev/null
+++ b/src/main/java/com/pt/repository/PeerInfoRepository.java
@@ -0,0 +1,9 @@
+package com.pt.repository;
+
+import com.pt.entity.PeerInfoEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.List;
+
+public interface PeerInfoRepository extends JpaRepository<PeerInfoEntity, Long> {
+ List<PeerInfoEntity> findByInfoHash(String infoHash);
+}
diff --git a/src/main/java/com/pt/service/ResourceService.java b/src/main/java/com/pt/service/ResourceService.java
index 1d0e797..d85315e 100644
--- a/src/main/java/com/pt/service/ResourceService.java
+++ b/src/main/java/com/pt/service/ResourceService.java
@@ -6,16 +6,11 @@
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 {
@@ -40,9 +35,11 @@
return resourceRepository.findByAuthor(username);
}
- // 发布资源时,将种子文件二进制数据保存到数据库
- public void publishResource(String name, String description, String author, double size) throws Exception {
- // 保存资源信息到数据库,包括种子文件的二进制数据
+ public void publishResource(String name, String description, String author, double size, byte[] torrentBytes) throws Exception {
+ // 解析并保存torrent元信息
+ TorrentMeta meta = torrentService.parseAndSaveTorrent(torrentBytes);
+
+ // 保存资源信息,并关联torrent信息
Resource resource = new Resource();
resource.setName(name);
resource.setDescription(description);
@@ -50,10 +47,8 @@
resource.setSize(size);
resource.setPublishTime(LocalDateTime.now());
- // 生成种子数据 byte[]
- byte[] torrentData = torrentService.generateTorrentBytes("uploads/" + name);
- resource.setTorrentData(torrentData);
-
+ resource.setTorrentData(torrentBytes);
+ // 这里可以保存torrent文件路径,或直接存数据库,依据你的设计
resourceRepository.save(resource);
}
diff --git a/src/main/java/com/pt/service/TorrentService.java b/src/main/java/com/pt/service/TorrentService.java
index dcbee69..1fdb700 100644
--- a/src/main/java/com/pt/service/TorrentService.java
+++ b/src/main/java/com/pt/service/TorrentService.java
@@ -2,77 +2,62 @@
import com.pt.entity.TorrentMeta;
import com.pt.repository.TorrentMetaRepository;
-import com.pt.utils.BencodeUtils;
+import com.pt.utils.BencodeCodec;
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.List;
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();
+ public TorrentMeta parseAndSaveTorrent(byte[] torrentBytes) throws Exception {
+ Map<String, Object> torrentDict = (Map<String, Object>) BencodeCodec.decode(torrentBytes);
+ if (!torrentDict.containsKey("info")) {
+ throw new IllegalArgumentException("Invalid torrent file: missing 'info' dictionary");
}
+
+ Map<String, Object> infoDict = (Map<String, Object>) torrentDict.get("info");
+
+ // 计算 info_hash
+ byte[] infoEncoded = BencodeCodec.encode(infoDict);
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ byte[] infoHashBytes = sha1.digest(infoEncoded);
+ String infoHash = bytesToHex(infoHashBytes);
+
+ // 获取文件名,大小等
+ String name = (String) infoDict.get("name");
+ long length = 0;
+ if (infoDict.containsKey("length")) {
+ length = ((Number) infoDict.get("length")).longValue();
+ } else if (infoDict.containsKey("files")) {
+ long totalLength = 0;
+ List<Map<String, Object>> files = (List<Map<String, Object>>) infoDict.get("files");
+ for (Map<String, Object> file : files) {
+ totalLength += ((Number) file.get("length")).longValue();
+ }
+ length = totalLength;
+ }
+
+ // 保存到数据库
+ TorrentMeta meta = new TorrentMeta();
+ meta.setFilename(name);
+ meta.setInfoHash(infoHash);
+ meta.setSize(length);
+ meta.setUploadTime(LocalDateTime.now());
+ meta.setTorrentData(torrentBytes);
+
+ torrentMetaRepository.save(meta);
+ return meta;
}
+
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
@@ -80,4 +65,4 @@
}
return sb.toString();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/pt/service/TrackerService.java b/src/main/java/com/pt/service/TrackerService.java
new file mode 100644
index 0000000..7167ff3
--- /dev/null
+++ b/src/main/java/com/pt/service/TrackerService.java
@@ -0,0 +1,76 @@
+package com.pt.service;
+
+import com.pt.entity.TorrentMeta;
+import com.pt.repository.TorrentMetaRepository;
+import com.pt.utils.BencodeCodec;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Service
+public class TrackerService {
+
+ private final Map<String, List<PeerInfo>> torrentPeers = new ConcurrentHashMap<>();
+
+ @Autowired
+ private TorrentMetaRepository torrentMetaRepository;
+
+ public byte[] handleAnnounce(Map<String, String[]> params, String ipAddress) {
+ try {
+ if (!params.containsKey("info_hash") || !params.containsKey("peer_id") || !params.containsKey("port")) {
+ return BencodeCodec.encode(Map.of("failure reason", "Missing required parameters"));
+ }
+
+ String infoHash = decodeParam(params.get("info_hash")[0]);
+ TorrentMeta meta = torrentMetaRepository.findByInfoHash(infoHash);
+ if (meta == null) {
+ return BencodeCodec.encode(Map.of("failure reason", "Invalid info_hash"));
+ }
+
+ String peerId = decodeParam(params.get("peer_id")[0]);
+ int port = Integer.parseInt(params.get("port")[0]);
+
+ PeerInfo peer = new PeerInfo(ipAddress, port, peerId);
+
+ torrentPeers.computeIfAbsent(infoHash, k -> new CopyOnWriteArrayList<>());
+ List<PeerInfo> peers = torrentPeers.get(infoHash);
+
+ boolean exists = peers.stream().anyMatch(p -> p.peerId.equals(peerId));
+ if (!exists) {
+ peers.add(peer);
+ }
+
+ List<String> ips = peers.stream().map(p -> p.ip).toList();
+ List<Integer> ports = peers.stream().map(p -> p.port).toList();
+ byte[] peerBytes = BencodeCodec.buildCompactPeers(ips, ports);
+
+ return BencodeCodec.buildTrackerResponse(1800, peerBytes);
+ } catch (Exception e) {
+ return BencodeCodec.encode(Map.of("failure reason", "Internal server error"));
+ }
+ }
+
+ private String decodeParam(String raw) {
+ return new String(raw.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
+ }
+
+ private static class PeerInfo {
+ String ip;
+ int port;
+ String peerId;
+
+ public PeerInfo(String ip, int port, String peerId) {
+ this.ip = ip;
+ this.port = port;
+ this.peerId = peerId;
+ }
+ }
+}
+
diff --git a/src/main/java/com/pt/utils/BencodeCodec.java b/src/main/java/com/pt/utils/BencodeCodec.java
new file mode 100644
index 0000000..2117d43
--- /dev/null
+++ b/src/main/java/com/pt/utils/BencodeCodec.java
@@ -0,0 +1,216 @@
+package com.pt.utils;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class BencodeCodec {
+
+ /* ------------- 编码部分 ------------- */
+
+ 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');
+ }
+
+ /* ------------- 解码部分 ------------- */
+
+ public static Object decode(byte[] data) throws IOException {
+ try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
+ in.mark(data.length);
+ 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");
+ }
+
+ in.mark(1024);
+
+ if (prefix >= '0' && prefix <= '9') {
+ in.reset();
+ return parseString(in);
+ } 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) throws IOException {
+ StringBuilder lenStr = new StringBuilder();
+ int b;
+ while ((b = in.read()) != -1 && b != ':') {
+ if (b < '0' || b > '9') {
+ throw new IOException("Invalid string length character: " + (char) b);
+ }
+ lenStr.append((char) b);
+ }
+ if (b == -1) {
+ throw new IOException("Unexpected end of stream reading string length");
+ }
+ int length = Integer.parseInt(lenStr.toString());
+
+ byte[] buf = new byte[length];
+ int offset = 0;
+ while (offset < length) {
+ int read = in.read(buf, offset, length - offset);
+ if (read == -1) {
+ throw new IOException("Unexpected end of stream reading string data");
+ }
+ offset += read;
+ }
+
+ return new String(buf, StandardCharsets.UTF_8);
+ }
+
+ 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);
+ }
+ if (b == -1) {
+ throw new IOException("Unexpected end of stream reading integer");
+ }
+ return Long.parseLong(intStr.toString());
+ }
+
+ private static List<Object> parseList(InputStream in) throws IOException {
+ List<Object> list = new ArrayList<>();
+ int b;
+ while (true) {
+ in.mark(1);
+ b = in.read();
+ if (b == -1) {
+ throw new IOException("Unexpected end of stream reading list");
+ }
+ if (b == 'e') {
+ break;
+ }
+ 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 (true) {
+ in.mark(1);
+ b = in.read();
+ if (b == -1) {
+ throw new IOException("Unexpected end of stream reading dictionary");
+ }
+ if (b == 'e') {
+ break;
+ }
+ in.reset();
+ String key = (String) decodeNext(in);
+ Object value = decodeNext(in);
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ /* ------------- 其他辅助方法 ------------- */
+
+ // 构造单个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);
+ }
+}