Alex

有的故事值得一直说下去.
Home » Latest Posts

如果您下载 Windows 10 Enterprise LTSC 2021 评估版,则没有密钥可以激活它。

第一步:你按 Win + R 打开运行

然后输入:C:\Windows\System32\spp\tokens\skus

第 2 步:您在此处下载 Windows 10 Enterprise LTSC 2021 的 SKU:

下载激活文件 解压密码:www.cnkker.com

然后你解压并复制到 C:\Windows\System32\spp\tokens\skus

第3步:你打开cmd(以管理员身份运行)

然后复制并粘贴这些代码:

cscript.exe %windir%\system32\slmgr.vbs /rilc cscript.exe %windir%\system32\slmgr.vbs /upk >nul 2>&1 cscript.exe %windir%\system32\slmgr.vbs /ckms >nul 2>&1 cscript.exe %windir%\system32\slmgr.vbs /cpky >nul 2>&1 cscript.exe %windir%\system32\slmgr.vbs /ipk M7XTQ-FN8P6-TTKYV-9D4CC-J462D sc config LicenseManager start= auto & net start LicenseManager sc config wuauserv start= auto & net start wuauserv clipup -v -o -altto c:
echo

完成

然后再用HEU_KMS_Activator_v42.0.1等激活工具激活便可

在需要热插拔业务模块、支持灰度发布的系统中,动态加载外部JAR包是提升系统扩展性的核心技术。本文将手把手实现3种动态加载方案,包含可直接运行的SpringBoot代码,并深入分析类加载机制与内存泄漏预防策略。

一、动态加载的应用场景 ‌电商平台‌:双十一期间动态加载营销活动模块 ‌风控系统‌:实时更新风控规则引擎 ‌物联网平台‌:按需加载设备协议解析器 ‌SaaS系统‌:客户定制化功能插件 二、核心技术难点 技术挑战 解决方案 类冲突问题 自定义ClassLoader隔离 资源释放 弱引用+卸载检测 依赖管理 Maven Shade插件 Spring Bean动态注册 GenericApplicationContext 三、方案一:URLClassLoader基础实现(完整代码)

  1. 动态JAR加载工具类 public class JarLoader { private static final Map<String, URLClassLoader> LOADER_CACHE = new ConcurrentHashMap<>();

    // 加载指定路径的JAR包 public static Class<?> loadClass(String jarPath, String className) throws Exception { URL[] urls = { new URL("file:" + jarPath) }; URLClassLoader loader = new URLClassLoader(urls, JarLoader.class.getClassLoader()); LOADER_CACHE.put(jarPath, loader); return loader.loadClass(className); }

    // 卸载JAR包 public static void unloadJar(String jarPath) throws Exception { URLClassLoader loader = LOADER_CACHE.remove(jarPath); if (loader != null) { loader.close(); System.gc(); // 帮助回收类信息 } } }

  2. 动态服务调用示例 @RestController public class PluginController {

    @GetMapping("/execute") public String executePlugin(@RequestParam String jarPath) throws Exception { Class<?> pluginClass = JarLoader.loadClass(jarPath, "com.example.PluginImpl"); Plugin plugin = (Plugin) pluginClass.newInstance(); return plugin.execute(); }

    // 接口定义 public interface Plugin { String execute(); } }

  3. 测试JAR包结构

编译插件JAR

javac -d ./ PluginImpl.java jar cvf plugin-demo.jar com/example/PluginImpl.class

插件实现类

package com.example; public class PluginImpl implements Plugin { public String execute() { return "插件执行成功!"; } } 四、方案二:Spring集成方案(动态注册Bean)

  1. 自定义类加载器 public class PluginClassLoader extends URLClassLoader { public PluginClassLoader(URL[] urls) { super(urls, ClassLoader.getSystemClassLoader().getParent()); }

    @Override public Class<?> loadClass(String name) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 优先从插件加载类 try { return findClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name); } } } }

  2. Bean动态注册器 @Component public class PluginRegistry {

    @Autowired private GenericApplicationContext applicationContext;

    private final Map<String, PluginClassLoader> loaders = new ConcurrentHashMap<>();

    public void registerPlugin(String jarPath) throws Exception { URL jarUrl = new File(jarPath).toURI().toURL(); PluginClassLoader loader = new PluginClassLoader(new URL[]{jarUrl});

     // 扫描JAR包中的Spring组件
     ClassPathScanningCandidateComponentProvider scanner = 
         new ClassPathScanningCandidateComponentProvider(true);
     scanner.addIncludeFilter(new AssignableTypeFilter(Plugin.class));
    
     for (BeanDefinition bd : scanner.findCandidateComponents("com.example")) {
         String className = bd.getBeanClassName();
         Class<?> clazz = loader.loadClass(className);
         applicationContext.registerBean(clazz);
     }
    
     loaders.put(jarPath, loader);
    

    } }

  3. 热更新接口 @RestController public class PluginAdminController {

    @Autowired private PluginRegistry pluginRegistry;

    @PostMapping("/plugin/load") public String loadPlugin(@RequestParam String path) { pluginRegistry.registerPlugin(path); return "插件加载成功"; }

    @PostMapping("/plugin/unload") public String unloadPlugin(@RequestParam String path) { pluginRegistry.unregisterPlugin(path); return "插件卸载成功"; } } 五、方案三:企业级热部署架构 graph TD A[管理后台] -->|上传JAR| B(Gateway) B --> C{安全校验} C -->|通过| D[版本管理] C -->|拒绝| E[审计告警] D --> F[类加载隔离] F --> G[服务注册] G --> H[流量切换] H --> I[旧版本卸载]

  4. 完整热部署流程 签名验证(防止恶意JAR) 依赖冲突检查 版本回滚机制 流量灰度切换

  5. 内存泄漏防护代码 public class PluginManager { private final Map<String, WeakReference<ClassLoader>> loaders = new WeakHashMap<>();

    public void loadPlugin(String jarPath) throws Exception { URLClassLoader loader = new URLClassLoader(new URL[]{new File(jarPath).toURI().toURL()}) { @Override protected void finalize() throws Throwable { close(); // GC时自动关闭 super.finalize(); } }; loaders.put(jarPath, new WeakReference<>(loader)); }

    // 定期检测无效引用 @Scheduled(fixedRate = 60000) public void cleanLoaders() { loaders.entrySet().removeIf(entry -> entry.getValue().get() == null); } } 六、生产环境注意事项 ‌安全防护

// 启用SecurityManager System.setSecurityManager(new PluginSecurityManager());

// 自定义权限策略 class PluginSecurityManager extends SecurityManager { @Override public void checkExit(int status) { throw new SecurityException("禁止调用System.exit()"); } } ‌性能监控

// 使用Micrometer监控类加载 Metrics.addRegistry(new SimpleMeterRegistry());

Timer.Sample sample = Timer.start(); Class<?> clazz = loader.loadClass(className); sample.stop(Metrics.timer("plugin.load.time")); 依赖隔离‌ 使用Maven Shade插件重写依赖:

org.apache.maven.plugins maven-shade-plugin com.google.guava myplugin.com.google.guava 七、总结与资源 ‌三种方案对比‌:

方案 优点 缺点 适用场景 URLClassLoader 实现简单 依赖冲突风险高 快速验证场景 Spring集成 支持Bean动态注册 需要处理上下文隔离 中小型插件系统 企业级架构 支持灰度发布 实现复杂度高 大型分布式系统

原文链接:https://blog.csdn.net/wx19930913/article/details/146243947

在 MySQL 中,如果你想使用字段值作为运算公式并动态计算结果,可以通过存储过程、函数或动态 SQL 实现。以下是几种常见方法:

方法 1:使用存储过程 存储过程可以动态执行 SQL 语句,适合处理复杂的动态公式。

sql 复制 DELIMITER //

CREATE PROCEDURE CalculateFormula(IN formula VARCHAR(255), OUT result DECIMAL(10,2)) BEGIN SET @sql = CONCAT('SELECT ', formula, ' INTO @result'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET result = @result; END //

DELIMITER ;

-- 调用存储过程 CALL CalculateFormula('10 * 5 + 2', @result); SELECT @result; 方法 2:使用用户定义函数 (UDF) 如果需要频繁使用某个公式,可以创建自定义函数。

sql 复制 DELIMITER //

CREATE FUNCTION CalculateFormula(formula VARCHAR(255)) RETURNS DECIMAL(10,2) BEGIN DECLARE result DECIMAL(10,2); SET @sql = CONCAT('SELECT ', formula, ' INTO @result'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET result = @result; RETURN result; END //

DELIMITER ;

-- 使用函数 SELECT CalculateFormula('10 * 5 + 2') AS result; 方法 3:直接在查询中使用动态 SQL 如果公式存储在表中,可以在查询中动态执行。

sql 复制 -- 假设表中有 formula 字段 CREATE TABLE formulas ( id INT PRIMARY KEY, formula VARCHAR(255) );

INSERT INTO formulas (id, formula) VALUES (1, '10 * 5 + 2');

-- 动态执行公式 SELECT id, formula, (SELECT @result := NULL; SET @sql = CONCAT('SELECT ', formula, ' INTO @result'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; @result) AS result FROM formulas; 注意事项 安全性:动态 SQL 容易受到 SQL 注入攻击,需确保公式来源可信。

性能:频繁使用动态 SQL 可能影响性能,建议优化或缓存结果。

错误处理:动态 SQL 可能因公式错误导致执行失败,需添加错误处理机制。

通过这些方法,你可以在 MySQL 中动态计算字段值作为公式的结果。

开发环境使用prisma 1、开发环境使用prisma非常的简单,只需要按照教程安装prisma,然后执行npx prisma init --datasource-provider sqlite 即可。脚本会生成schema文件 在schema文件中编写自己的模型。 开发环境中执行 bash 代码解读复制代码npx prisma migrate dev --name xxxx

可以看到在我们的prisma目录下会有个迁移文件夹migrations,里面会生成我们的sql。这时候我们可以直接使用@prisma/client typescript 代码解读复制代码import { PrismaClient } from '@prisma/client' const prismaInstance = new PrismaClient()

可以用prismaInstance实例来进行数据库操作咯,这时候如果你使用的是ts并且安装了prisma的vs code插件你就会发现它的语法提示非常人性化,如果你之前有orm的基础就可以直接上手。 在生产环境中使用 一般情况下开发环境有nodejs环境,全局安装了prisma我们只需要把migrations文件夹拷贝到开发环境并且执行下面命令就行。 bash 代码解读复制代码npx prisma migrate deploy

在electron中你使用electron-builder打包要怎么做才能正常使用prisma呢? 1、首先你得把migrations文件夹拷贝到安装包中,在package.json中使用extraResources来将我们所需要的migrations文件移入到安装包中。 js 代码解读复制代码"build": { "extraResources": [ { "from": "prisma/migrations", "to": "resources/prisma/migrations" }, { "from": "prisma/schema.prisma", "to": "resources/prisma/schema.prisma" } ] }

这样打包后如果你打开 join(process.resourcesPath,'resources/prisma')这个路径就会发现migrations文件夹已经被移入,然后我们要执行npx prisma migrate delploy,但是在我们的electron生产环境中没有npx命令该怎么做,这时候我们就需要使用prisma提供的二进制来进行操作了。 根据不同的环境prisma会提供不同的二进制,比如我这边是mac m1,那么你就要在schema.prisma中加入以下内容,prisma运行过程中就会去下载drawin-arm64的二进制 js 代码解读复制代码generator client { provider = "prisma-client-js" binaryTargets = ["native", "darwin-arm64"] }

下载完你就会发现node_modules/@prisma/engines下会多出两个二进制文件

libquery是sql引擎,没有它就不能进行数据库操作。 schema就是我们刚才说的执行npx prisma migrate deploy的,可以理解为全局安装的prisma就是执行的这个二进制。 这样我们就可以直接使用二进制来执行我们的命令咯,前提是我们把二进制拷贝到了安装包内,你也可以使用extraResources属性,但我这边并没有写,因为electron一般会对文件都打包进asar二进制中,但是如果有些三方库的二进制打入二进制会出现一些意想不到的问题,所以可以在package.json设置asarUnpack,意味着不用吧这些文件进行二进制打包。 js 代码解读复制代码"build":{ "asarUnpack": [ "/node_modules/prisma//", "/node_modules/@prisma//" ] }

我们怎么拿到那些不需要被打包的二进制呢?我们代码中怎么写呢,路径是什么呢?代码如下 typescript 代码解读复制代码const appUnpackedPath = app.getAppPath().replace('app.asar', 'app.asar.unpacked') const schemePath = electronIsDev ? resolve(__dirname, '../node_modules/@prisma/engines/schema-engine-darwin-arm64') : resolve(appUnpackedPath, './node_modules/@prisma/engines/schema-engine-darwin-arm64')

上面是对开发环境进行了判断,读者可以自行处理,appUnkpackedPath就是我们的二进制存放路径。 我们可以开发编写一套统一的处理代码,这套也是我从github某篇issue上直接拿下来的。 typescript 代码解读复制代码export async function runPrisma(command: string[]) { try { const exitCode = await new Promise((r, _) => { const prismaPath = resolve(electronIsDev ? resolve(__dirname, '../node_modules') : resolve(appUnpackedPath, './node_modules'), 'prisma', 'build', 'index.js') logger.info(process.cwd(), 'cwd') const child = fork(prismaPath, command, { cwd: electronIsDev ? process.cwd() : join(appUserPath, 'resources'), env: { ...process.env, DATABASE_URL: process.env.DATABASE_URL, PRISMA_QUERY_ENGINE_LIBRARY: queryPath, PRISMA_SCHEMA_ENGINE_BINARY: schemePath, }, stdio: 'pipe', })

  child.on('message', (msg) => {
    logger.info(msg)
  })

  child.on('error', (err) => {
    logger.error('Child process got error:', err)
  })

  child.on('close', (code) => {
    logger.info(code)
    r(code)
  })

  child.stdout?.on('data', function (data) {
    // console.log(data)

    logger.info('prisma: ', data.toString())
  })

  child.stderr?.on('data', function (data) {
    logger.error('prisma: ', data.toString())
  })
})

if (exitCode !== 0) {
  throw Error(`command ${command} failed with exit code ${exitCode}`)
}

return exitCode

} catch (e) { logger.error(e) throw e } }

我们执行只需要传入参数进行即可

要注意我们的命令执行路径,也就是cwd参数要传入正确,要确保路径下面的文件内容格式符合prisma要求,prisma/migrations/xx类似这样即可

ts 代码解读复制代码runPrisma(['migrate', 'deploy'])

上面代码中还有个queryPath,这个path是用来获取执行sql的引擎的,如果没有他我们就不能进行数据库操作,开发环境不需要我们指定引擎的二进制,但是在electron中需要我们来指定一下,有两种方法一种是使用环境变量process.env.PRISMA_QUERY_ENGINE_BINARY = queryPath,但是这种方法我这一直没成功,所以在github上找到了一种隐藏api方式来直接指定 ts 代码解读复制代码const prismaInstance = new PrismaClient({ __internal: { engine: { binaryPath: queryPath, }, }, } as any)

这个__internal是一个非标准api,但是使用了发现能正常执行了。如果不指定就会一直报错 Can not spawn Query Engine when using Electron 结语 最后寻找感兴趣开发小工具的小伙伴一起来开发,目前我给这个产品取名Ew Box,Everything You Want,技术栈使用如下:

vite electron react,react-router prisma sqlite tailwindcss shadcn/ui koa

对以上技术感兴趣,还有一些不错的idea的小伙伴都可以加入哟,后续可以使用你感兴趣的任何技术栈都可以集成其中 产品没有盈利目的,都是些简单的小功能点,做一款适合自己的小工具,就单纯想扩展或者提升技术面的小伙伴可以尝试加入哟 目前产品功能有压缩图片/密码箱/todo日历等功能,有想法的朋友可以一起加入哟。

相关内容 node.js实现内网穿透: https://www.jianshu.com/p/d2d4f8bff599 kotlin实现内网穿透: https://www.jianshu.com/p/c8dc095c758e

最大设计连接数: 65535

前面写了个udp转tcp再转udp的工具, 打算用它和tcp内网穿透结合使用 来实现udp内网穿透, 但是在实际使用中发现存在网速较慢的问题, 初步判断为运营商网络问题(使用http下载也一样, 使用单线程只能达到1MB/S内, 3条就可以达到10MB/S. 上传没有问题). 这个问题没法解决就只好再写个udp版. 本来想用udp打洞写的, 但是有一个网络不支持... 只好用服务器转发写, 但是仍然存在一个小问题, 暂时不打算解决. 结尾会讲.

实现代码 服务端: package main

import ( "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/pem" "errors" "fmt" "log" "net" "os" "strings" "sync" "time" )

var ( privateKeyStr = "-----BEGIN PRIVATE KEY-----\n" + "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJYBf37uy0sVXyxb\n" + "bMDjvQzxv/ke3UWCSkYhUd8e+MjGHeT8A9V9aVemg3qUogND/Pgtlz6bTd9p5H+q\n" + "OCXnrZbSwdAN7O/9x3zhRzaEOH+9OJ8vpF80DuatbdqqJXFPiDO+nfbufNyT8n+b\n" + "N9UXISAVv7Nay+vTEySD401czDDzAgMBAAECgYAE9u26TLrrtDxfInN5+s+R8xpQ\n" + "a2YVW9eLdKTaBpNjSbNJldGmqiznWrp1PyARjZl8uT2NM+Si5UVLuF19W6qSC1Tw\n" + "bn2brBSsFsufKQff0XXRsD8OqKT5h5/PlRATYgisZonD/v0SPfDHROcNCFiOelEv\n" + "kKZAJgJS3vghZx5jCQJBAMTb4esPOApK+6wXlfVShvRiac9UWW9KBYLwpqya5Cjd\n" + "X/QUGVwywMFkNgRnBz7yWdJd1zuXfuI77N87BXYOE68CQQDDEjfaKCyZJ73RfNJo\n" + "ED5DRfZXm86RPBDlZznJ4shjNVbk1sGClYAk0WuAHeIZJVpm1HpME6NSCfumqeOq\n" + "z1P9AkAQu+xJcgK+hT89ksexkfFc5ty9vhrYJf+v8MsKUyRgAOl+MxMwzjOqfN1G\n" + "pIduJ2XRRx7btvYXPybUlwzQy0OLAkBIexlznt/LXH/kOcv4TKjF2FYLAWKEhlwE\n" + "0REg2Xn5mtUZnE40lhYSGBoodXIQQ9fOQ37Zi6ZwkjMGHzPvwK+FAkAe9gHRMI2u\n" + "lzwVp/AQntBMXmw92IULIlfRmfV1jDBYuT0JHUUGZqfCrz+iDW8Ot24QBzLbwKxJ\n" + "JRrmXbUxObmC\n" + "-----END PRIVATE KEY-----" natDispatcherAddr = ":8989" natPasswords = []string{"yzh"} timeOut = time.Second * 60 * 2 serverMap = map[string]*Server{} mapMutex sync.Mutex clientIndex = 0 maxLength = 0 )

func main() { log.Println("入参: " + strings.Join(os.Args[1:], " ")) if len(os.Args) == 2 { natDispatcherAddr = os.Args[1] } log.SetFlags(log.LstdFlags | log.Lshortfile) startServer() }

func Start(natDispatcherAddrStr string, natPasswordsArr []string) { natDispatcherAddr = natDispatcherAddrStr natPasswords = natPasswordsArr startServer() }

func startServer() { log.Println("NatU分发服务地址: " + natDispatcherAddr) go func() { for { time.Sleep(time.Second * 60) println() log.Println("natU转发Size:", len(serverMap), ",maxLength:", maxLength) mapMutex.Lock() for _, value := range serverMap { log.Println("natU转发中:", value.toString()) } mapMutex.Unlock() } }() privateKey, err := loadPrivateKey(privateKeyStr) if err != nil { log.Panicln(err) }

listenerAddr, err := net.ResolveUDPAddr("udp", natDispatcherAddr)
if err != nil {
    log.Println(err)
    return
}
network := "udp"
if listenerAddr.IP.To4() != nil {
    network = "udp4"
} else if listenerAddr.IP.To16() != nil {
    network = "udp6"
}
listenerConn, err := net.ListenUDP(network, listenerAddr)
if err != nil {
    log.Println(err)
    return
}
defer listenerConn.Close()
buffer := make([]byte, 1024*64)
for {
    // log.Println("监听消息")
    n, clientAddr, err := listenerConn.ReadFromUDP(buffer)
    if err != nil {
        log.Println(err)
        if errors.Is(err, net.ErrClosed) {
            break
        }
        time.Sleep(time.Second)
        continue
    }
    if n > maxLength {
        maxLength = n
    }
    if n < 1 {
        log.Println("异常消息:", clientAddr)
        continue
    }
    data := make([]byte, n)
    copy(data, buffer)

    cmd := data[0]
    // log.Println("收到消息:", cmd, clientAddr, "=>", listenerConn.LocalAddr())

    switch {
    case cmd == 1:
        // log.Println("心跳数据")
        realData := data[1:]
        // 鉴权
        value := handleNatUAuth(realData, privateKey, clientAddr, listenerConn)
        listenerConn.SetWriteDeadline(time.Now().Add(time.Second * 3))
        listenerConn.WriteToUDP([]byte{value}, clientAddr)
    case cmd == 0:
        // 真实数据
        cIndex := int(data[1])*256 + int(data[2])
        realData := data[3:]
        // log.Println("转发到:", cIndex)
        handleRealData(cIndex, realData)
    }
}

}

func handleRealData(cIndex int, realData []byte) { servers := []*Server{} mapMutex.Lock() for _, server := range serverMap { servers = append(servers, server) } mapMutex.Unlock() for _, server := range servers { server.clientMutex.Lock() for _, value := range server.clientMap { if value.index == cIndex { // log.Println("转成功:", cIndex, value.clientAddr) value.openConn.SetWriteDeadline(time.Now().Add(time.Second * 3)) value.openConn.WriteToUDP(realData, value.clientAddr) value.lastLime = time.Now() break } } server.clientMutex.Unlock() } }

func handleNatUAuth(cmdData []byte, privateKey *rsa.PrivateKey, clientAddr *net.UDPAddr, listenerConn *net.UDPConn) byte { // decryptedText, err := rsa.DecryptPKCS1v15(nil, privateKey, cmdData) decryptedText, err := rsa.DecryptOAEP(sha1.New(), nil, privateKey, cmdData, nil) if err != nil { log.Println("DecryptPKCS1v15 err", err) return 3 } info := string(decryptedText) infos := strings.Split(info, "-") if len(infos) < 2 { log.Println("infos error", infos) return 4 } version := 1 pwdOk := false for _, v := range natPasswords { if v == infos[1] { version = 1 pwdOk = true break } } for _, v := range natPasswords { if v == infos[0] { version = 2 pwdOk = true break } } if !pwdOk { log.Println("密码错误", infos) return 5 } var openAddrs []string switch version { case 1: openAddrs = infos[:1] case 2: openAddrs = infos[1:] default: openAddrs = infos[:1] } // log.Println("openAddrs:", openAddrs)

for index, address := range openAddrs {
    server := getServer(address, index, version, clientAddr, strings.Join(openAddrs, "-"), listenerConn)
    if server == nil {
        log.Println("getServer nil")
        return 6
    }
}
return 1

}

func getServer(address string, index int, version int, clientAddr *net.UDPAddr, openAddrsStr string, listenerConn *net.UDPConn) *Server { addr := strings.Split(address, ":") if len(addr) < 2 { log.Println("address error", address) return nil } port := addr[len(addr)-1] mapMutex.Lock() defer mapMutex.Unlock() server := serverMap[port] if server != nil { if server.address != address { log.Println("端口重复", server.address, address) return nil } if server.openAddrsStr != openAddrsStr { log.Println("openAddrsStr不同", server.openAddrsStr, openAddrsStr) return nil } if server.version != version { log.Println("版本不同", server.version, version) return nil } } else { listenerAddr, err := net.ResolveUDPAddr("udp", address) if err != nil { log.Println(err) return nil } network := "udp" if listenerAddr.IP.To4() != nil { network = "udp4" } else if listenerAddr.IP.To16() != nil { network = "udp6" } openConn, err := net.ListenUDP(network, listenerAddr) if err != nil { log.Println(err) return nil } log.Println("开放地址:", address, "对方index:", index, "版本:", version) server = &Server{address: address, index: index, version: version, createTime: time.Now(), openConn: openConn, openAddrsStr: openAddrsStr, clientMap: map[string]*Client{}} serverMap[port] = server go server.accept(port, listenerConn) } server.clientAddr = clientAddr server.openConn.SetReadDeadline(time.Now().Add(timeOut)) return server }

type Server struct { openAddrsStr string index int version int createTime time.Time address string openConn *net.UDPConn clientAddr *net.UDPAddr clientMap map[string]*Client clientMutex sync.Mutex }

type Client struct { index int lastLime time.Time clientAddr *net.UDPAddr openConn *net.UDPConn }

func (server *Server) accept(port string, listenerConn net.UDPConn) { buffer := make([]byte, 102464) for { n, clientAddr, err := server.openConn.ReadFromUDP(buffer) if err != nil { log.Println(err) break } if n > maxLength { maxLength = n } if n < 1 { log.Println("空消息:", clientAddr, port) continue } data := make([]byte, n) copy(data, buffer) // log.Println("转发消息:", len, clientAddr, "=>", server.index)

    server.clientMutex.Lock()
    client := server.clientMap[clientAddr.String()]
    if client == nil {
    getClient:
        for {
            clientIndex++
            index := clientIndex
            for key, value := range server.clientMap {
                if value.index == index {
                    log.Println("index无效:", index)
                    continue getClient
                }
                if time.Since(value.lastLime) >= timeOut {
                    delete(server.clientMap, key)
                }
            }
            client = &Client{index: index, clientAddr: clientAddr, openConn: server.openConn}
            server.clientMap[clientAddr.String()] = client
            break
        }
    }
    client.lastLime = time.Now()
    server.clientMutex.Unlock()
    listenerConn.SetWriteDeadline(time.Now().Add(time.Second * 3))
    listenerConn.WriteToUDP(append([]byte{byte(10 + server.index), byte(client.index / 256), byte(client.index % 256)}, data...), server.clientAddr)
}
server.openConn.Close()
mapMutex.Lock()
delete(serverMap, port)
mapMutex.Unlock()
log.Println("释放端口:", port)

}

func (server *Server) toString() string { ms := time.Since(server.createTime).Milliseconds() s := ms / 1000 m := s / 60 h := m / 60 runTime := fmt.Sprintf("%d天%d时%d分%d秒", h/24, h%24, m%60, s%60) return fmt.Sprintf("%s=>%s, index: %d, version: %d, %s", server.address, server.clientAddr, server.index, server.version, runTime) }

func loadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) { block, _ := pem.Decode([]byte(privateKeyStr)) if block == nil { return nil, fmt.Errorf("解码私钥失败") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } privateKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("非法私钥文件") } return privateKey, nil }

客户端: package main

import ( "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/pem" "errors" "fmt" "log" "net" "os" "strings" "sync" "time" )

var ( publicKeyStr = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWAX9+7stLFV8sW2zA470M8b/5\nHt1FgkpGIVHfHvjIxh3k/APVfWlXpoN6lKIDQ/z4LZc+m03faeR/qjgl562W0sHQ\nDezv/cd84Uc2hDh/vTifL6RfNA7mrW3aqiVxT4gzvp327nzck/J/mzfVFyEgFb+z\nWsvr0xMkg+NNXMww8wIDAQAB\n-----END PUBLIC KEY-----\n"

natDispatcherAddr = "127.0.0.1:8989"
natPassword       = "yzh"
rateInterval      = time.Second * 15
timeOut           = time.Second * 60 * 2
natMapArr         = []string{
    ":1701-192.168.3.25:1701",
    ":11771-127.0.0.1:1771",
    ":11772-127.0.0.1:1772",
    ":11773-127.0.0.1:1773",
}
errMap = map[int]string{
    2: "config error or port used",
    3: "DecryptPKCS1v15 err",
    4: "infos error",
    5: "密码错误",
    6: "getServer nil",
    7: "",
    8: "",
    9: "",
}
listenerConn *net.UDPConn
serverAddr   *net.UDPAddr
mapMutex     sync.Mutex
forwardMap   = map[int]*net.UDPConn{}
natSuccess   = false
maxLength    = 0

)

func main() { log.Println("入参: " + strings.Join(os.Args[1:], " ")) if len(os.Args) == 2 { natDispatcherAddr = os.Args[1] } log.SetFlags(log.LstdFlags | log.Lshortfile) startClient() }

func Start(natDispatcherAddrStr string, natPasswordStr string, natMapStr []string) { natDispatcherAddr = natDispatcherAddrStr natPassword = natPasswordStr natMapArr = natMapStr startClient() }

func startClient() { log.Println("NatU分发服务地址: " + natDispatcherAddr) publicKey, err := loadPublicKey(publicKeyStr) if err != nil { log.Panicln(err) } natParams := []string{natPassword} // 通过index找到需要转发的位置 localServerAddrs := []string{} for _, v := range natMapArr { mapArr := strings.Split(v, "-") natServerOpenAddr := mapArr[0] localServerAddr := mapArr[1] log.Println("NatU服务器开放地址: "+natServerOpenAddr, "本地服务地址: "+localServerAddr) natParams = append(natParams, natServerOpenAddr) localServerAddrs = append(localServerAddrs, localServerAddr) } //密码, 开放端口 natInfo, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(strings.Join(natParams, "-")), nil) if err != nil { log.Panicln(err) } go func() { for { log.Println("startNat") creatClient(localServerAddrs) listenerConn = nil serverAddr = nil natSuccess = false time.Sleep(time.Second) } }()

go func() {
    for {
        if listenerConn == nil || serverAddr == nil {
            time.Sleep(time.Second)
            continue
        }
        // log.Println("主动发消息:", listenerConn.LocalAddr(), "=>", serverAddr)
        data := append([]byte{1}, natInfo...)
        listenerConn.SetWriteDeadline(time.Now().Add(time.Second * 5))
        listenerConn.WriteToUDP(data, serverAddr)
        time.Sleep(rateInterval)
    }
}()
for {
    time.Sleep(time.Second * 60)
    println()
    log.Println("natU转发Size:", len(natMapArr), ",maxLength:", maxLength, ",natSuccess:", natSuccess)
    for _, value := range natMapArr {
        log.Println("natU转发中:", strings.ReplaceAll(value, "-", "=>"))
    }
}

}

func creatClient(localServerAddrs []string) { var err error serverAddr, err = net.ResolveUDPAddr("udp", natDispatcherAddr) if err != nil { log.Println(err) return } listenerAddr, err := net.ResolveUDPAddr("udp", ":0") if err != nil { log.Println(err) return } listenerConn, err = net.ListenUDP("udp", listenerAddr) if err != nil { log.Println(err) return } defer listenerConn.Close() buffer := make([]byte, 1024*64) for { listenerConn.SetReadDeadline(time.Now().Add(timeOut)) n, clientAddr, err := listenerConn.ReadFromUDP(buffer) if err != nil { log.Println(err) break } if n > maxLength { maxLength = n } if n < 1 { log.Println("异常消息:", clientAddr) continue } data := make([]byte, n) copy(data, buffer)

    // if clientAddr.Port != serverAddr.Port || clientAddr.IP.String() != serverAddr.IP.String() {
    if clientAddr.Port != serverAddr.Port {
        log.Println("异常消息:", serverAddr, clientAddr)
        continue
    }
    cmd := data[0]
    // log.Println("收到响应:", cmd, clientAddr, "=>", listenerConn.LocalAddr())
    switch {
    case cmd == 1:
        if !natSuccess {
            natSuccess = true
            log.Println("natU建立成功")
        }
        // log.Println("心跳数据")
    case cmd > 1 && cmd < 10:
        errMsg := errMap[int(cmd)]
        if errMsg == "" {
            errMsg = "config error"
        }
        log.Panicln(cmd, errMsg, natMapArr)
    case cmd >= 10:
        forwardAddress := localServerAddrs[data[0]-10]
        clinetIndex := int(data[1])*256 + int(data[2])
        realData := data[3:]
        // log.Println("转发到:", forwardAddress)
        handleClientRequest(clientAddr, realData, forwardAddress, clinetIndex)
    }
}

}

func handleClientRequest(clientAddr net.UDPAddr, clientData []byte, forwardAddress string, clinetIndex int) { if clientAddr == nil { return } clientAddrString := clientAddr.String() mapMutex.Lock() defer mapMutex.Unlock() forwardConn := forwardMap[clinetIndex] if forwardConn == nil { forwardAddr, err := net.ResolveUDPAddr("udp", forwardAddress) if err != nil { log.Println(err) return } forwardConn, err = net.DialUDP("udp", nil, forwardAddr) if err != nil { log.Println(err) return } infoStr := clientAddrString + "=>" + forwardAddress + "=>" + forwardConn.LocalAddr().String() + "=>" + forwardConn.RemoteAddr().String() log.Println("添加udp转发:" + infoStr) forwardMap[clinetIndex] = forwardConn buffer := make([]byte, 102464) go func() { defer forwardConn.Close() forwardSuccess := false for { forwardConn.SetReadDeadline(time.Now().Add(timeOut)) n, serverAddr, err := forwardConn.ReadFromUDP(buffer) if err != nil { log.Println(err) if errors.Is(err, net.ErrClosed) { break } if nerr, ok := err.(net.Error); ok && nerr.Timeout() { break } time.Sleep(time.Second) continue } // if serverAddr.Port != forwardAddr.Port || serverAddr.IP.String() != forwardAddr.IP.String() { if serverAddr.Port != forwardAddr.Port { log.Println("异常消息:", serverAddr.String(), forwardAddr.String()) continue } if n > maxLength { maxLength = n } if !forwardSuccess { forwardSuccess = true log.Println("udp转发成功:", serverAddr.String(), n, clientAddrString) } // log.Println("服务端消息:", serverAddr.String(), len, clientAddrString) data := make([]byte, n) copy(data, buffer) listenerConn.SetWriteDeadline(time.Now().Add(time.Second * 5)) listenerConn.WriteToUDP(append([]byte{0, byte(clinetIndex / 256), byte(clinetIndex % 256)}, data...), clientAddr) } log.Println("移除udp:" + infoStr) mapMutex.Lock() delete(forwardMap, clinetIndex) mapMutex.Unlock() }() } // log.Println("客户端消息:", clientAddrString, len(clientData)) forwardConn.SetWriteDeadline(time.Now().Add(time.Second * 5)) forwardConn.Write(clientData) }

func loadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) { block, _ := pem.Decode([]byte(publicKeyStr)) if block == nil { return nil, fmt.Errorf("解码公钥失败") } key, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, err } publicKey, ok := key.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("非法公钥文件") } return publicKey, nil }

存在的问题: 客户端fd00::2给fd00::5发消息 服务端知道是fd00::2发来的, 但是不知道是哪个ip接收的, 也无法控制使用哪个ip回消息, 测试中发现服务端可能会用fd00::6发消息给fd00::2, 在部分网络下这个消息是发送不过去的(这也是我没用打洞法的原因), 问题点就在这里. 解决方案也很简单, 分别监听每个ip, 但是需要监听设备ip的变化, 不想这样做. 不知道有没有大佬有更好的解决方案

画个草图好理解些:

image.png

作者:今天i你好吗 链接:https://www.jianshu.com/p/c3191a36ee84 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Life is fantastic
🥕 More