Java下SCTP报文UDP封装实现方案咨询(适配NAT网络)
问题
我需要用SCTP协议传输文件,已编写Java代码,但在4G热点这类重度NAT网络环境下无法正常运行。了解到SCTP UDP封装的相关规范文档,现寻求能将SCTP报文封装进UDP并通过UDP通道发送、实现NAT穿越的Java方案。以下是当前的文件发送代码:
import java.io.*; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.*; import com.sun.nio.sctp.MessageInfo; import com.sun.nio.sctp.SctpChannel; import com.sun.nio.sctp.SctpServerChannel; public class Main { SctpChannel connectionChannelPrimary; SctpChannel connectionChannelSecondary; InetSocketAddress serverSocketAddressPrimary; InetSocketAddress serverSocketAddressSecondary; String directoryPath; public Main() { serverSocketAddressPrimary = new InetSocketAddress(6002); serverSocketAddressSecondary = new InetSocketAddress(6003); } public void setDirectoryPath(String directoryPath) { this.directoryPath = directoryPath; } public String getDirectoryPath() { return directoryPath; } public void establishConnection(int connId) throws IOException { SctpServerChannel sctpServerChannel = SctpServerChannel.open(); if (connId == 0) { sctpServerChannel.bind(serverSocketAddressPrimary); connectionChannelPrimary = sctpServerChannel.accept(); System.out.println("connection established for primary"); } else { sctpServerChannel.bind(serverSocketAddressSecondary); connectionChannelSecondary = sctpServerChannel.accept(); System.out.println("connection established for helper"); } } ArrayList<String> getAllFiles() { File directory = new File(this.directoryPath); ArrayList<String> fileNames = new ArrayList<>(); for (File fileEntry : Objects.requireNonNull(directory.listFiles())) { if (fileEntry.isFile()) { fileNames.add(fileEntry.getName()); } } Collections.sort(fileNames); return fileNames; } public byte[] readFile(String filename) throws IOException { String extraString = "\n\n\n\nNRL\n\n\n"; File file = new File(filename); FileInputStream fl = new FileInputStream(file); ByteBuffer finalBuffer = ByteBuffer.allocate((int) (file.length() + extraString.length())); byte[] arr = new byte[(int) file.length()]; int res = fl.read(arr); if (res < 0) { System.out.println("Error in reading file"); fl.close(); return null; } fl.close(); finalBuffer.put(arr); finalBuffer.put(extraString.getBytes()); byte[] tmp = new byte[extraString.length()]; finalBuffer.position((int) (file.length() - 1)); finalBuffer.get(tmp, 0, tmp.length); return finalBuffer.array(); } public void sendBytes(String filename, int connId) throws IOException { byte[] message = readFile(filename); assert message != null; System.out.println(message.length); int tmp = 0; int cntIndex = 60000; int prevIndex = 0; boolean isBreak = false; while (!isBreak) { byte[] slice; if (prevIndex + 60000 >= message.length) { slice = Arrays.copyOfRange(message, prevIndex, message.length); isBreak = true; } else { slice = Arrays.copyOfRange(message, prevIndex, cntIndex); prevIndex = cntIndex; cntIndex = cntIndex + 60000; } final ByteBuffer byteBuffer = ByteBuffer.allocate(64000); final MessageInfo messageInfo = MessageInfo.createOutgoing(null, 0); byteBuffer.put(slice); byteBuffer.flip(); tmp += slice.length; try { if (connId == 0) connectionChannelPrimary.send(byteBuffer, messageInfo); else connectionChannelSecondary.send(byteBuffer, messageInfo); } catch (Exception e) { e.printStackTrace(); } } System.out.println(tmp); } public static void main(String[] args) throws IOException { String bgFilePath = "/home/iiitd/Desktop/background/"; String fgFilePath = "/home/iiitd/Desktop/foreground/"; Main myObj = new Main(); myObj.setDirectoryPath("/home/iiitd/Desktop/tmp/"); myObj.establishConnection(1); myObj.establishConnection(0); ArrayList<String> files = myObj.getAllFiles(); for (String tmpFile : files) { String cntFilePath = myObj.getDirectoryPath() + tmpFile; myObj.sendBytes(cntFilePath,0); } } }
SCTP UDP封装的Java实现方案
核心思路是把完整的SCTP报文作为UDP的数据载荷发送,同时添加一个极简的封装头部(包含SCTP的源/目的端口),以此绕过NAT对SCTP的限制。
1. 封装核心规则
UDP数据包结构 = [4字节封装头部] + [原始SCTP报文]
- 封装头部:2字节源SCTP端口 + 2字节目的SCTP端口,用于两端识别内部SCTP会话
2. 代码实现
2.1 UDP封装发送器
替换原代码中SctpChannel的发送逻辑,把SCTP报文封装后通过UDP发送:
import java.net.*; import java.nio.ByteBuffer; public class SctpOverUdpSender { private DatagramSocket udpSocket; private InetSocketAddress remoteUdpAddr; private int localSctpPort; private int remoteSctpPort; public SctpOverUdpSender(int localUdpPort, String remoteHost, int remoteUdpPort, int localSctpPort, int remoteSctpPort) throws SocketException { this.udpSocket = new DatagramSocket(localUdpPort); this.remoteUdpAddr = new InetSocketAddress(remoteHost, remoteUdpPort); this.localSctpPort = localSctpPort; this.remoteSctpPort = remoteSctpPort; } // 封装并发送SCTP报文 public void sendSctpPacket(ByteBuffer sctpPacket) throws Exception { // 构建封装头部 ByteBuffer encapsHeader = ByteBuffer.allocate(4); encapsHeader.putShort((short) localSctpPort); encapsHeader.putShort((short) remoteSctpPort); encapsHeader.flip(); // 合并头部与SCTP报文 ByteBuffer udpData = ByteBuffer.allocate(4 + sctpPacket.remaining()); udpData.put(encapsHeader); udpData.put(sctpPacket); udpData.flip(); // 发送UDP数据包 DatagramPacket packet = new DatagramPacket(udpData.array(), udpData.limit(), remoteUdpAddr); udpSocket.send(packet); } public void close() { udpSocket.close(); } }
2.2 UDP解封装接收器
负责接收UDP数据包,剥离封装头部后还原为SCTP报文:
import java.net.*; import java.nio.ByteBuffer; public class SctpOverUdpReceiver implements Runnable { private DatagramSocket udpSocket; private int bufferSize = 65535; private OnSctpPacketReceivedListener listener; public interface OnSctpPacketReceivedListener { void onSctpPacketReceived(ByteBuffer sctpPacket); } public SctpOverUdpReceiver(int localUdpPort, OnSctpPacketReceivedListener listener) throws SocketException { this.udpSocket = new DatagramSocket(localUdpPort); this.listener = listener; } @Override public void run() { byte[] buffer = new byte[bufferSize]; while (!udpSocket.isClosed()) { try { DatagramPacket packet = new DatagramPacket(buffer, buffer.length); udpSocket.receive(packet); // 剥离4字节封装头部,得到原始SCTP报文 ByteBuffer sctpPacket = ByteBuffer.wrap(buffer, 4, packet.getLength() - 4); listener.onSctpPacketReceived(sctpPacket); } catch (Exception e) { if (!udpSocket.isClosed()) { e.printStackTrace(); } } } } public void close() { udpSocket.close(); } }
2.3 适配原代码的关键修改
Java自带的com.sun.nio.sctpAPI无法直接获取原始SCTP报文,需要改用第三方SCTP库(比如jSCTP)来生成原始报文。修改原代码的sendBytes方法:
// 初始化UDP发送器(示例配置,根据实际场景修改) SctpOverUdpSender udpSender = new SctpOverUdpSender(6004, "目标IP", 6005, 6002, 6002); // 替换原SctpChannel.send逻辑:通过jSCTP生成原始SCTP报文后发送 // ByteBuffer sctpPayload = 生成的原始SCTP报文 udpSender.sendSctpPacket(sctpPayload);
3. 注意事项
- NAT会话保持:两端定期发送UDP心跳包,避免NAT会话超时被销毁
- 报文大小控制:UDP报文不能超过MTU(通常1500字节),需对SCTP报文分片后再封装
- 库依赖:必须引入第三方SCTP库来获取原始报文,Java原生API无法满足需求
内容的提问来源于stack exchange,提问作者Keshav Gambhir
相关产品推荐
相关产品推荐

