UDP打洞,内网穿透(附JAVA代码)

目录

1.内网穿透概论

1.1 公网IP端口,NAT转发,内网IP端口之间的关系

1.2 NAT转发和UDP打洞原理

1.3 核心代码

1.内网穿透概论

        本人不喜欢长篇大论的讲一些学术用语,最后看的云里雾里的。就用最简单的白话文大概讲一下内网穿透的原理和作用。

1.1 公网IP端口,NAT转发,内网IP端口之间的关系

         要了解和使用内网穿透,首先要明白他们之间是如何交换消息的。

举个例子:在网页上输入www.baidu.com 就可以打开百度,这是因为知道了域名,域名在DNS服务器上映射到了对应的公网IP,域名是和ip地址绑定的,访问www.xxx.com 等于访问 公网IP,可以使用cmd的ping命令去看返回ip地址。

当我们访问了www.baidu.com ,百度的服务器需要知道你的公网ip和端口才能把信息返回给你(最终结果就是浏览器上显示百度页面)

        浏览器输入www.baidu.com到返回百度页面 整个过程的路径

        内网 IP:端口 -> nat(转发)网线的公网 IP:端口 -> 百度服务器公网 IP:端口 ->nat(转发) 网线的公网 IP:端口 -> 内网 IP:端口

其中 内网 和 nat(转发) 每一次请求都会刷新端口  , 内网IP(在cmd 中输入 ipconfig /all 可以查看的ipv4地址 ) 如下图:

 了解网络传输协议的应该都知道 互联网上进行信息传输 需要目标地址,端口和响应地址,端口

这里涉及到一个知识就是nat,因为ipv4地址是有限的,现在运营商的网线大部分是一个公网对应一个区域的用户,简单来说就是你和其他人一起使用一个公网ip。

nat起到一个转发作用,所以你在本地电脑上的地址和端口是不能被外网用户访问的,但是可以被同一个公网ip下的用户访问。可以这样理解一个公网ip对应一个nat。

那么为什么要进行udp打洞呢?因为需要和nat外用户进行信息交换。

前面的内容只是介绍一下大概,下面是重点(仔细看)

1.2 NAT转发和UDP打洞原理

        当两个内网A,B想要互相通信怎么办呢?首先需要知道A,B的nat地址和端口

每次发送信息到百度服务器返回的地址其实是nat的地址和端口,然后由nat转发给你的内网地址和端口并生成记录,记录类似于: (nat地址:端口——内网地址:端口)。 

nat地址端口和内网地址端口 1对1绑定了(可以画上等于号,每次调用端口都会刷新

因为A,B都是内网,无法直接通信,所以需要一个公网服务器 来记录A,B的nat地址和端口。

然后让A向公网服务器的地址发UDP消息,这样A的NAT上就会留下一条记录 A IP:端口 ——natA 公网IP:端口。

让B向公网服务器的地址发UDP消息,这样B的NAT上就会留下一条记录 B IP:端口 ——natB公网IP:端口。

这样A,B的nat地址和端口都知道了,然后在让A,B互相发送UDP请求 在nat中留下记录 这样 A,B之间就可以通过UDP进行通信了。

重点!!!使用java new DatagramSocket() 一定要使用单例模式,千万不要关闭不然每次nat映射的端口都不一样 ,nat在变化就无法获取正确的地址了 之前踩坑过

1.3 核心代码

import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static spike.controller.CheckSyncStateController.doGet;

/**
 * @PACKAGE_NAME: spike.schedule
 * @NAME: NatSchedule
 * @USER: spike
 * @DATE: 2023/5/12 10:27
 * @PROJECT_NAME: Springcolud_Spike
 */
@Slf4j
@Component
public class NatSchedule {

    @Value("${tarurl}")
    private String TAR_URL;

    @Value("${isserver}")
    private boolean IS_SERVER;

    @Value("${key}")
    private String KEY;

    @Value("${sendport}")
    private Integer SEND_PORT;

    @Value("${receiveport}")
    private Integer RECEIVE_PORT;

    public static DatagramSocket RECEIVE_SOCKET = null;
    public static DatagramSocket SEND_SOCKET = null;

    public static Map<String, Object> NAT_MAP = new HashMap<>();

    //每1分钟执行一次
    @PostConstruct
    @Scheduled(cron = "0/60 * * * * ?")
    private void SendUdp() throws IOException {
        sendUpdMsg();
    }

    //每1秒钟执行一次
    @Scheduled(cron = "*/1 * * * * ?")
    private void receiveUpd() throws IOException {
        receiveUpdMsg();
    }

    public void sendUpdMsg() throws IOException {
        if (!IS_SERVER) {
            if (SEND_SOCKET == null) {
                //清除key值
                doGet("http://" + TAR_URL + "/Nat/clearNatMap?key=" + KEY);
                SEND_SOCKET = new DatagramSocket(SEND_PORT);
                Thread thread = new Thread() {
                    @SneakyThrows
                    @Override
                    public void run() {
                        log.info("开始接收upd请求");
                        responseSend();
                    }
                };
                thread.start();
            }
            sendUpdToServer();
        }
    }

    private void responseSend() throws IOException {
        byte[] bytes = new byte[1024];
        DatagramPacket packet1 = new DatagramPacket(bytes, bytes.length);
        while (true) {
            SEND_SOCKET.receive(packet1);
            String receive = new String(bytes, 0, packet1.getLength(), "utf-8");
            if (receive.contains("client")) {
                String msg = "****************************打洞成功****************************";
                log.info(msg);
            }else {
                log.info("*************************** upd信息开始 ***************************\n {}", receive);
                log.info("*************************** upd信息结束 ***************************");
            }
        }
    }

    private void sendUpdToServer() throws IOException {
        String ipV4 = InetAddress.getLocalHost().getHostAddress();
        String[] ipArr = TAR_URL.split(":");
        // 客户段发送upd请求到服务端(服务端记录 客户端内网ip/外网ip/外网端口)
        String text = KEY + ":" + ipV4 + ":" + SEND_SOCKET.getLocalPort();
        byte[] buf = text.getBytes();
        sendUdp(buf, ipArr[0], RECEIVE_PORT, SEND_SOCKET);
        //查询 相同key的两个客户段nat信息
        String result = doGet("http://" + TAR_URL + "/Nat/getNatMap?key=" + KEY);
        if (result.isEmpty()) {
            return;
        }
        List<String> list = JSON.parseObject(result, List.class);
        // 相同key 需要nat穿透的ip数组
        log.info("键值:{} 下穿透ip数组 {}", KEY, JSON.toJSONString(list));
        String client = "";
        for (String str : list) {
            if (str == null || str.isEmpty()) {
                continue;
            }
            String[] strArr = str.split(":");
            if (strArr[0].equals(ipV4)) {
                client = str;
            }
            if (!strArr[0].equals(ipV4)) {
                //upd打洞
                String[] udpArr = str.split("/")[1].split(":");
                // 2,明确要发送的具体数据。
                String clientMsg = "来自 client " + client + " upd 信息";
                byte[] data = clientMsg.getBytes();
                sendUdp(data, udpArr[0], Integer.valueOf(udpArr[1]), SEND_SOCKET);
            }
        }
    }

    public static void sendUdp(byte[] data, String ip, int port, DatagramSocket datagramSocket) throws IOException {
        // 3,将数据封装成了数据包。
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName(ip), port);
        log.info("向服务端" + ip + ":" + port + "发送upd 信息");
        datagramSocket.send(packet);
    }

    /**
     * 服务端接收upd请求
     *
     * @throws IOException
     */
    public void receiveUpdMsg() throws IOException {
        if (IS_SERVER) {
            //创建一个数据包,用于接收数据
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
            //DatagramSocket(int port)构造数据报套接字并将其绑定到本地主机上的指定端口
            if (RECEIVE_SOCKET == null) {
                RECEIVE_SOCKET = new DatagramSocket(RECEIVE_PORT);
            }
            DatagramSocket ds = RECEIVE_SOCKET;
            //调用DatagramSocket对象的方法接收数据
            ds.receive(dp);
            InetAddress address = dp.getAddress();
            int port = dp.getPort();
            String url = address.getHostAddress() + ":" + port;
            //解析数据包,并把数据在控制台显示
            byte[] data = dp.getData();
            //int getLength()返回要发送的数据的长度或接收到的数据的长度
            int length = dp.getLength();
            String dataString = new String(data, 0, length);
            log.info("\n客户端Nat地址:  **** {}**** \n客户端内网地址: **** {} **** ", url, dataString);
            String[] clientArr = dataString.split(":");
            if (IS_SERVER) {
                String key = clientArr[0];
                putNatMap(key, clientArr[1] + ":" + clientArr[2] + "/" + url);
                log.info("key :{} 映射数组更新完成", KEY);
            }
        }
    }

    private void putNatMap(String key, String value) {
        if (NAT_MAP.get(KEY) == null) {
            List<String> tempArr = new ArrayList<>();
            tempArr.add(value);
            NAT_MAP.put(key, tempArr);
        } else {
            List<String> tempArr = (List<String>) NAT_MAP.get(key);
            String refresh = null;
            for (String str : tempArr) {
                String uuid = StringUtils.substringBeforeLast(str, ":");
                String uuidValue = StringUtils.substringBeforeLast(value, ":");
                if (uuid.equals(uuidValue)) {
                    refresh = str;
                    break;
                }
            }
            if (refresh != null) {
                tempArr.remove(refresh);
            }
            tempArr.add(value);
            NAT_MAP.put(key, tempArr);
        }
    }
}

import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import spike.schedule.NatSchedule;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import static spike.schedule.NatSchedule.RECEIVE_SOCKET;
import static spike.schedule.NatSchedule.SEND_SOCKET;
import static spike.schedule.NatSchedule.sendUdp;

/**
 * @PACKAGE_NAME: com.example.controller
 * @NAME: CheckSyncState
 * @USER: spike
 * @DATE: 2023/5/8 8:57
 * @PROJECT_NAME: SendAlertMsg
 */
@Slf4j
@RestController
@RequestMapping("Nat")
public class CheckSyncStateController {
    @Value("${isserver}")
    private boolean IS_SERVER;

    /**
     * 查询NAT_MAP
     */
    @ApiOperation(value = "查询NAT_MAP", tags = "Nat Upd打洞")
    @RequestMapping(value = "/getNatMap", method = RequestMethod.GET)
    public Object getSyncState(@RequestParam(required = false, value = "key") String key) {
        if (!key.isEmpty()) {
            return NatSchedule.NAT_MAP.get(key);
        }
        return NatSchedule.NAT_MAP;
    }

    /**
     * 清除指定NAT_MAP key
     */
    @ApiOperation(value = "清除指定NAT_MAP key", tags = "Nat Upd打洞")
    @RequestMapping(value = "/clearNatMap", method = RequestMethod.GET)
    public Object clearNatMap(@RequestParam(required = false, value = "key") String key) {
        if (NatSchedule.NAT_MAP.get(key) != null) {
            NatSchedule.NAT_MAP.put(key, null);
            return NatSchedule.NAT_MAP.get(key);
        }
        return NatSchedule.NAT_MAP.get(key);
    }

  
    /**
     * 请求
     */
    @ApiOperation(value = "udp打洞", tags = "CheckSyncStateController 检查同步状态")
    @RequestMapping(value = "/sendToUdp", method = RequestMethod.GET)
    public String sendToUdp(@RequestParam(value = "key") String key, @RequestParam(value = "client_ip") String client_ip, @RequestParam(value = "client_port") Integer client_port) throws IOException {
        DatagramSocket datagramSocket;
        if (IS_SERVER) {
            datagramSocket = RECEIVE_SOCKET;
        } else {
            datagramSocket = SEND_SOCKET;
        }
        sendUdp(key.getBytes(), client_ip, client_port, datagramSocket);
        log.info("GET接口向服务端 {}:{} 发送upd 信息:{}", client_ip, client_port, key);
        return "打洞完成";
    }

    public static String doGet(String httpurl) {
        HttpURLConnection connection = null;
        InputStream is = null;
        BufferedReader br = null;
        String result = null;// 返回结果字符串
        try {
            // 创建远程url连接对象
            URL url = new URL(httpurl);
            // 通过远程url连接对象打开一个连接,强转成httpURLConnection类
            connection = (HttpURLConnection) url.openConnection();
            // 设置连接方式:get
            connection.setRequestMethod("GET");
            // 设置连接主机服务器的超时时间:15000毫秒
            connection.setConnectTimeout(15000);
            // 设置读取远程返回的数据时间:60000毫秒
            connection.setReadTimeout(60000);
            // 发送请求
            connection.connect();
            // 通过connection连接,获取输入流
            if (connection.getResponseCode() == 200) {
                is = connection.getInputStream();
                // 封装输入流is,并指定字符集
                br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                // 存放数据
                StringBuffer sbf = new StringBuffer();
                String temp = null;
                while ((temp = br.readLine()) != null) {
                    sbf.append(temp);
                    sbf.append("\r\n");
                }
                result = sbf.toString();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (null != br) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            connection.disconnect();// 关闭远程连接
        }

        return result;
    }

}
server:
  port: 5001
#服务端公网地址
tarurl: xx.xxx.xx.xxx:5001
#是否服务器
isserver: false
#客户端连接唯一键值
key: 自己定义一个uuid
sendport: 5001
receiveport : 7091

nat内网穿透获取连接服务器的map

http://服务器地址:5001/Nat/getNatMap?key=定义的唯一uuid

返回示例:

[

    "192.168.0.117:50979/x11.xx2.89.xx2:15623"

]

upd发送消息接口

http://内网IPV4:5001/Nat/sendToUdp?client_ip=目标公网ip&client_port=目标的nat端口&key=发送内容

返回示例:

打洞完成

结果展示:

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值