Alex - 2025年2月
type: channel
link: https://blog.laialex.com/2025/02/
description: 有的故事值得一直说下去.
items: go语言实现内网穿透(udp版)
items: p2p通信原理及实现
items: Golang用300行代码实现内网穿透
items: golang手把手实现tcp内网穿透代理
items: 物联网8大协议介绍及对比
items: 带你快速入门学习SCSS

go语言实现内网穿透(udp版)
type: item
date: 2025-02-10T14:08:37+08:00
description: 相关内容 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 natPassw…

p2p通信原理及实现
type: item
date: 2025-02-10T13:59:05+08:00
description: 1.简介 当今部署的中间件大多都是在C/S架构上设计的，其中相对隐匿的客户机主动向周知的服务端(拥有静态IP地址和DNS名称)发起链接请求。大多数中间件实现了一种非对称的通讯模型，即内网中的主机可以初始化对外的链接，而外网的主机却不能初始化对内网的链接，除非经过中间件管理员特殊配置。在中间件为常见的NAPT的情况下（也是本文主要讨论的），内网中的客户端没有单独的公网IP地址，而是通过NAPT转换，和其他同一内网用户共享一个公网IP。这种内网主机隐藏在中间件后的不可访问性对于一些客户端 软件如浏览器来说并不是一个问题，因为其只需要初始化对外的链接，从某方面来看反而还对隐私保护有好处。 然而在P2P应用中，内网主机（客户端）需要对另外的终端（Peer）直接建立链接，但是发起者和响应者可能在不同的中间件后面，两者都没有公网IP地址。而外部对NAT公网IP和端口主动的链接或数据都会因内网未请求被丢弃掉。本文讨论的就是如何跨越NAT实现内网主机直接通讯的问题。 2.术语 防火墙（Firewall）： 防火墙主要限制内网和公网的通讯，通常丢弃未经许可的数据包。防火墙会检测(但是不修改)试图进入内网数据包的IP地址和TCP/UDP端口信息。 网络地址转换器（NAT）： NAT不止检查进入数据包的头部，而且对其进行修改，从而实现同一内网中不同主机共用更少的公网IP（通常是一个）。 基本NAT（Basic NAT）： 基本NAT会将内网主机的IP地址映射为一个公网IP，不改变其TCP/UDP端口号。基本NAT通常只有在当NAT有公网IP池的时候才有用。 网络地址-端口转换器（NAPT）： 到目前为止最常见的即为NAPT，其检测并修改出入数据包的IP地址和端口号，从而允许多个内网主机同时共享一个公网IP地址。 锥形NAT（Cone NAT）： 在建立了一对（公网IP，公网端口）和（内网IP，内网端口）二元组的绑定之后，Cone NAT会重用这组绑定用于接下来该应用程序的所有会话（同一内网IP和端口），只要还有一个会话还是激活的。 例如，假设客户端A建立了两个连续的对外会话，从相同的内部端点（10.0.0.1:1234）到两个不同的外部服务端S1和S2。Cone NAT只为两个会话映射了一个公网端点（155.99.25.11:62000），确保客户端端口的“身份”在地址转换的时候保持不变。由于基本NAT和防火墙都不改变数据包的端口号，因此这些类型的中间件也可以看作是退化的Cone NAT。 对称NAT（Symmetric NAT） 对称NAT正好相反，不在所有公网-内网对的会话中维持一个固定的端口绑定。其为每个新的会话开辟一个新的端口。如下图所示： 其中Cone NAT根据NAT如何接收已经建立的（公网IP，公网端口）对的输入数据还可以细分为以下三类： 全锥形NAT（Full Cone NAT） 在一个新会话建立了公网/内网端口绑定之后，全锥形NAT接下来会接受对应公网端口的所有数据，无论是来自哪个（公网）终端。全锥NAT有时候也被称为“混杂”NAT（promiscuous NAT）。 受限锥形NAT（Restricted Cone NAT） 受限锥形NAT只会转发符合某个条件的输入数据包。条件为：外部（源）IP地址匹配内网主机之前发送一个或多个数据包的结点的IP地址。受限NAT通过限制输入数据包为一组“已知的”外部IP地址，有效地精简了防火墙的规则。 端口受限锥形NAT（Port-Restricted Cone NAT） 端口受限锥形NAT也类似，只当外部数据包的IP地址和端口号都匹配内网主机发送过的地址和端口号时才进行转发。端口受限锥形NAT为内部结点提供了和对称NAT相同等级的保护，以隔离未关联的数据。 P2P通信 根据客户端的不同，客户端之间进行P2P传输的方法也略有不同，这里介绍了现有的穿越中间件进行P2P通信的几种技术。 3.1 中继（Relaying） 这是最可靠但也是最低效的一种P2P通信实现。其原理是通过一个有公网IP的服务器中间人对两个内网客户端的通信数据进行中继和转发。如下图所示： 客户端A和客户端B不直接通信，而是先都与服务端S建立链接，然后再通过S和对方建立的通路来中继传递的数据。这钟方法的缺陷很明显，当链接的客户端变多之后，会显著增加服务器的负担，完全没体现出P2P的优势。 3.2 逆向链接（Connection reversal） 第二种方法在当两个端点中有一个不存在中间件的时候有效。例如，客户端A在NAT之后而客户端B拥有全局IP地址，如下图： 客户端A内网地址为10.0.0.1，且应用程序正在使用TCP端口1234。A和服务器S建立了一个链接，服务器的IP地址为18.181.0.31，监听1235端口。NAT A给…

Golang用300行代码实现内网穿透
type: item
date: 2025-02-07T14:48:44+08:00
description: 我们经常会遇到一个问题，如何将本机的服务暴露到公网上，让别人也可以访问。我们知道，在家上网的时候我们有一个 IP 地址，但是这个 IP 地址并不是一个公网的 IP 地址，别人无法通过一个 IP 地址访问到你的服务，所以在例如：微信接口调试、三方对接的时候，你必须将你的服务部署到一个公网的系统中去，这样太累了。 这个时候，内网穿透就出现了，它的作用就是即使你在家的服务，也能被其人访问到。 今天让我们来用一个最简单的案例学习一下如何用 go 来做一个最简单的内网穿透工具。 整体结构 首先我们用几张图来说明一下我们是如何实现的，说清楚之后再来用代码实现一下。 当前网络情况 我们可以看到，画实线的是我们当前可以访问的，画虚线的是我们当前无法进行直接访问的。 我们现在有的路是： 用户主动访问公网服务器是可以的 内网主动访问公网服务也是可以的 当前我们要做的是想办法能让用户访问到内网服务，所以如果能做到公网服务访问到内网服务，那么用户就能间接访问到内网服务了。 想是这么想的，但是实际怎么做呢？用户访问不到内网服务，那我公网服务器同样访问不到吧。所以我们就需要利用现有的链路来完成这件事。 基本架构 内网，客户端（我们要搞一个） 外网，服务端（我们也要搞一个） 访问者，用户 首先我们需要一个控制通道来传递消息，因为只有内网可以访问公网，公网不知道内网在哪里，所以第一次肯定需要客户端主动告诉服务端我在哪 服务端通过 8007 端口监听用户来的请求 当用户发来请求时，服务端需要通过控制信道告诉客户端，有用户来了 客户端收到消息之后建立隧道通道，主动访问服务端的 8008 来建立 TCP 连接 此时客户端需要同时与本地需要暴露的服务 127.0.0.1:8080 建立连接 连接完成后，服务端需要将 8007 的请求转发到隧道端口 8008 中 客户端从隧道中获得用户请求，转发给内网服务，同时将内网服务的返回信息放入隧道 最终请求流向是，如图中的紫色箭头走向，请求返回是如图中红色箭头走向。 需要理解的是，TCP 一旦建立了连接，双方就都可以向对方发送信息了，所以其实原理很简单，就是利用已有的单向路建立 TCP 连接，从而知道对方的位置信息，然后将请求进行转发即可。 代码实现 工具方法 首先我们先定义三个需要使用的工具方法，还需要定义两个消息编码常量，后面会用到 监听一个地址对应的 TCP 请求 CreateTCPListener 连接一个 TCP 地址 CreateTCPConn 将一个 TCP-A 连接的数据写入另一个 TCP-B 连接，将 TCP-B 连接返回的数据写入 TCP-A 的连接中 Join2Conn （别看这短短 10 几行代码，这就是核心了） 代码语言：javascript 复制 package network import ( "io" "log" "net" ) const ( KeepAlive = "KEEP_ALIVE" NewConnection = "NEW_CONNECTION" ) func CreateTCPListener(addr string) (*net.TCPListener, error) { tcpAddr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return nil, err } tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { return nil, err } return tcpListener, nil } func CreateTCPConn(addr string) (*net.TCPConn, error) { tcpAddr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return nil, err } tcpListener, err := net.DialTCP("tcp",nil, tcpAddr) if err != nil { return nil, err } return tcpListener, nil } func Join2Conn(local *net.TCPConn, remote *net.TCPConn) { go joinConn(local, remote) go joinConn(remote, local) } func joinConn(local *net.TCPConn, remote *net.TCPConn) { defer local.Close() defer remote.Close() _, err := io…

golang手把手实现tcp内网穿透代理
type: item
date: 2025-02-07T08:57:05+08:00
description: 首先还是我们需要一个http服务器，这个http服务器是我们的内网的服务器，也就是说我们需要在外网访问到这个位于内网的http服务器。假设我们内网的ip是127.0.0.1，分配的局域网ip是192.168.1.10，然后http端口是8080 那么显而易见，我们在同一内网环境是可以访问的，直接使用192.168.1.10:8000即可访问到服务器 但是如果不在同一局域网的机器就不行了，需要借助一台公网ip的服务器来做一个透传代理。 内网服务器准备 这里假设你已经安装python2或者python3，打开我们的mac终端或者windows cmd 在python2下输入python -m SimpleHTTPServer python3下输入python -m http.server 这样我们可以快速得到一台http服务器，打开浏览器输入127.0.0.1:8000可以发现是一个文件浏览的http服务器 我们不需要很复杂的http服务器，仅仅用来做测试而已，所以这样是足够的了 服务端代码 控制客户端的监听代码 1.这里选择监听在8009端口，这个tcp服务，主要用来接受客户端的连接请求的，然后发送控制指令给到客户端，请求建立隧道连接的。这里只接受一个客户端的连接请求，如果有多余的会close掉 一旦有客户端连接到8009端口，这个tcp连接是一直保持的，为什么呢？ 因为服务端需要发送控制指令给客户端，所以tcp连接必须一直保持。 然后服务端会每隔两秒发送hi这个消息给到客户端，客户端可以直接忽略掉，因为这个hi只是类似心跳机制的保证。 var cache *net.TCPConn = nil func makeControl() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8009") //打开一个tcp断点监听 tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } fmt.Println("控制端口已经监听") for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { panic(err) } fmt.Println("新的客户端连接到控制端服务进程:" + tcpConn.RemoteAddr().String()) if cache != nil { fmt.Println("已经存在一个客户端连接!") //直接关闭掉多余的客户端请求 tcpConn.Close() } else { cache = tcpConn } go control(tcpConn) } func control(conn *net.TCPConn) { go func() { for { //一旦有客户端连接到服务端的话，服务端每隔2秒发送hi消息给到客户端 //如果发送不出去，则认为链路断了，清除cache连接 _, e := conn.Write(([]byte)("hi\n")) if e != nil { cache = nil } time.Sleep(time.Second * 2) } }() } 对外访问的服务端口监听 假设端口是8007，这里的对外访问的服务端口监听，也就是说假设我们服务器ip是10.18.10.1的话，那么访问10.18.10.1的端口8007，就等于请求内网的127.0.0.1:8000 127.0.0.1:8000就是上面的python服务器 和上面的代码看起来很像，但是用处不一样，上面那个主要目的是控制客户端，要求它建立请求 这里的目的主要是提供真正需要tcp代理透传的服务！ func makeAccept() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8007") tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } defer tcpListener.Close() for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(err) continue } fmt.Println("A client connected 8007:" + tcpConn.RemoteAddr().String()) addConnMathAc…

物联网8大协议介绍及对比
type: item
date: 2025-02-05T14:08:21+08:00
description: 物联网（IoT）技术的发展，离不开多种通信协议的支持。这些协议在数据传输、设备连接和管理等方面起到了至关重要的作用。本文将介绍和对比8种常见的物联网协议，帮助理解它们的特点及适用场景。 MQTT（Message Queuing Telemetry Transport） 概述 MQTT是一种轻量级的消息传输协议，设计用于低带宽和不稳定网络环境下的设备通信。 特点 轻量级：协议头部非常小，适合资源受限的设备。 发布/订阅模型：支持松耦合的消息通信方式。 QoS（服务质量） ：提供三种服务质量等级（0, 1, 2）。 应用场景 适用于物联网设备的远程监控、传感器数据传输等。 CoAP（Constrained Application Protocol） 概述 CoAP是一种专为低功耗、低带宽网络设计的协议，基于REST架构，类似于HTTP。 特点 轻量级：适合资源受限设备。 RESTful架构：支持GET、POST、PUT、DELETE操作。 UDP传输：减少传输开销。 应用场景 适用于智能家居、工业控制等需要简单请求/响应机制的场景。 AMQP（Advanced Message Queuing Protocol） 概述 AMQP是一种开源的标准应用层协议，用于消息中间件，支持复杂的消息传递模式。 特点 可靠性：确保消息的可靠传递。 灵活性：支持多种消息传递模式。 互操作性：不同厂商的实现可以互操作。 应用场景 适用于金融服务、企业消息系统等需要高可靠性和灵活性的场景。 XMPP（Extensible Messaging and Presence Protocol） 概述 XMPP是一种基于XML的协议，最初用于即时消息传递，现也用于物联网设备的通信。 特点 实时性：支持即时通信。 扩展性：通过XMPP扩展协议（XEPs）扩展功能。 安全性：支持TLS加密。 应用场景 适用于聊天应用、实时通知以及物联网设备之间的实时通信。 DDS（Data Distribution Service） 概述 DDS是一种面向实时系统的数据传输协议，支持发布/订阅模式，适合高性能、低延迟的应用。 特点 实时性：高效的数据分发和低延迟。 可扩展性：支持大规模分布式系统。 QoS策略：多种服务质量策略。 应用场景 适用于自动驾驶、航空航天等需要高实时性和高可靠性的场景。 Zigbee 概述 Zigbee是一种低功耗、低数据速率的无线通信协议，主要用于家庭自动化和工业控制。 特点 低功耗：适合电池供电设备。 自组网：支持设备自动加入网络。 安全性：支持AES加密。 应用场景 适用于智能家居、工业自动化等需要低功耗无线通信的场景。 Z-Wave 概述 Z-Wave是一种用于家庭自动化的无线通信协议，具有低功耗、可靠性高的特点。 特点 低功耗：适合长时间运行的设备。 中继功能：设备可以作为中继器，增强信号覆盖。 广泛支持：得到多家厂商支持，设备兼容性好。 应用场景 适用于家庭自动化系统，如智能灯光控制、安全监控等。 Bluetooth Low Energy（BLE） 概述 BLE是一种用于短距离数据通信的低功耗蓝牙技术，适合电池供电的设备。 特点 低功耗：延长电池寿命。 广泛支持：内置于大多数智能手机和移动设备。 适应性强：支持广播和连接模式。 应用场景 适用于可穿戴设备、医疗设备、智能家居等需要短距离低功耗通信的场景。 分析说明表 协议 特点 应用场景 MQTT 轻量级、发布/订阅模型、QoS支持 远程监控、传感器数据传输 CoAP 轻量级、RESTful架构、UDP传输 智能家居、工业控制 AMQP 高可靠性、灵活性、互操作性 金融服务、企业消息系统 XMPP 实时性、扩展性、安全性 聊天应用、实时通知、设备通信 DDS 高实时性、可扩展性、QoS策略 自动驾驶、航空航天 Zigbee 低功耗、自组网、安全性 智能家居、工业自动化 Z-Wave 低功耗、中继功能、广泛支持 家庭自动化系统 BLE 低功耗、广泛支持、适应性强 可穿戴设备、医疗设备、智能家居 总结 物联网协议的选择需要考虑多方面的因素，包括设备资源、网络环境、数据传输要求和应用场景。本文介绍的8种物联网协议各有其优势和适用领域： MQTT和CoAP适合资源受限的设备和网络环境。 AMQP和DDS提供高可靠性和高性能的数据传输，适用于对实时性和可靠性要求较高的场景。 XMPP适用于需要实时通信的应用，如聊天和通知。 Zigbee和Z-Wave专注于低功耗和家庭自动化应用。 BLE适合短距离、低功耗的通信需求，如可穿戴设备和智能家居。 根据具体的应用需求，选择合适的协议可以大幅提升系统的性能和可靠性。希望本文能为您在物联网协议的选择和应用中提供有价值的参考。

带你快速入门学习SCSS
type: item
date: 2025-02-05T10:23:41+08:00
description: 使用变量; sass让人们受益的一个重要特性就是它为css引入了变量。你可以把反复使用的css属性值 定义成变量，然后通过变量名来引用它们，而无需重复书写这一属性值。或者，对于仅使用过一 次的属性值，你可以赋予其一个易懂的变量名，让人一眼就知道这个属性值的用途。 sass使用$符号来标识变量(老版本的sass使用!来标识变量。改成$是多半因为!highlight-color看起来太丑了。)，比如$highlight-color和$sidebar-width。为什么选择$ 符号呢？因为它好认、更具美感，且在CSS中并无他用，不会导致与现存或未来的css语法冲突。 1.1 变量声明 和 使用 sass变量的声明和css属性的声明很像： // 定义变量名 值是属性值 下面写样式的时候可以直接用变量名替代 $highlight-color: #F90; $nav-color: #F90; nav { $width: 100px; width: $width; color: $nav-color; } //编译后 nav { width: 100px; color: #F90; } 再看几个例子 $highlight-color: #F90; $highlight-border: 1px solid $highlight-color; .selected { border: $highlight-border; } //编译后 .selected { border: 1px solid #F90; } 2. 嵌套css规则 在Sass中，你可以像俄罗斯套娃那样在规则块中嵌套规则块。sass在输出css时会帮你把这些嵌套规则处理好，避免你的重复书写。 #content { article { h1 { color: #333 } p { margin-bottom: 1.4em } } aside { background-color: #EEE } } /* 编译后 */ #content article h1 { color: #333 } #content article p { margin-bottom: 1.4em } #content aside { background-color: #EEE } 大多数情况下这种简单的嵌套都没问题，但是有些场景下不行，比如你想要在嵌套的选择器 里边立刻应用一个类似于：hover的伪类。为了解决这种以及其他情况，sass提供了一个特殊结 构&。 2.1 父选择器的标识符&; 最常见的一种情况是当你为链接之类的元素写：hover这种伪类时，你并不希望以后代选择器的方式连接。比如说，下面这种情况sass就无法正常工作： article a { color: blue; :hover { color: red } } 正确做法 article a { color: blue; &:hover { color: red } } 2.2 群组选择器的嵌套 在使用css编写样式代码的时候,会遇到这样的场景. 比如 要选中一组中不同的元素 .container h1, .container h2, .container h3 { margin-bottom: .8em } 我们需要在每个元素前面都加上父级容器,这样代码量就多了,而且看起来也不整洁. 不过sass 嵌套特性在这种场景也非常有用.就上面的代码sass 是这样做的 .container { h1, h2, h3 {margin-bottom: .8em} } 处理这种群组选择器规则嵌套上的强大能力，正是sass在减少重复敲写方面的贡献之一。尤其在当嵌套级别达到两层甚至三层以上时，与普通的css编写方式相比，只写一遍群组选择器大大减少了工作量。 3. 插值语句 #{} 通过 #{} 插值语句可以在选择器或属性名中使用变量： $name: foo; $attr: border; p.#{$name} { #{$attr}-color: blue; } 编译为 p.foo { border-color: blue; } 4. @at-root Sass的@at-root指令可以让你在样式表中跳出父级选择器，直接回到根选择器的层级。这对于重构或者修改已有规则时非常方便。 下面是一个示例代码： .parent { color: blue; @at-root .new-rule { color: red; } } 在上述代码中，.new-rule将会输出为顶级规则，而不是.parent .child .new-rule，因此它的颜色是红色而不是蓝色。这个指令可以用于在具体选择器外部重新定义一些属性并保持样式层次结构的清晰性。 编译后的CSS代码如下所示：…