?前言
在 【分析】小程序扫码硬件通信方案 有提到手机扫码,会跳到相应的界面发送激活硬件信号。
硬件可以接收到手机发送的激活信号是因为中间有服务器的存在,服务器作为一个中转站会通过手机发送的 SN 号,向指定 SN 号的机子发送激活信号。
其实它整体过程类似于聊天通信,两个支付宝客户端(手机、硬件)之间的通信,手机向支付宝服务器发送消息这个我知道是可以连接到的,毕竟支付宝服务器肯定是公网 IP 。
支付宝服务器向局域网内的硬件发送消息或向手机发送消息,这个着实激起了我的好奇心。
这篇文章就会根据我所查到的资料以及个人的理解来向你介绍它们之间通信的原理和过程。
?服务器与客户端的通信
通过查找资料我大概了解到,客户端在局域网内能接到服务器发送的消息,是通过 NAT 端口映射实现。
?流程图
?分析
当客户端处于一个局域网内,向服务器公网 IP 47.103.70.171:8808 发送 UDP 数据包。
如果使用 netstat -ano
命令去查询客户端与服务器的连接信息,会发现是客户端局域网 IP 192.168.137.191:11616 直连服务器公网 IP 47.103.70.171:8808 。
在服务器查看连接的客户端 IP 时,会发现客户端 IP 地址是 112.26.184.53:29811。
通过 IP138.com 查询到我当前的公网 IP 正好是 112.26.184.53 与服务器上面查看到的客户端 IP 一样。
之所以出现这种情况,是因为端口映射的存在。
像上面所描述,我目前所在的公网 112.26.184.53 将它的端口 29811 与我的客户端 192.168.137.191 的端口 11616 进行了映射,让我的客户端目前可以与外网服务器 47.103.70.171:8808 进行通信,外网的服务器也可以通过 112.26.184.53:29811 将数据传输到我的客户端上面。
或许你也意识到了,我的数据都要经过公网才能发送出去,可能会发生数据劫持,因此需要连接自己可信的网络。
?NAT
Network Address Translation
通过上面的描述你大概清楚了,外网服务器与内网客户端之间的通信是因为端口映射的存在。
在路由器里面都有一个 NAT 功能可以进行端口映射,将连接上这个路由的机器的 IP 进行映射,那么与这个路由同级 IP 就可以对其进行访问。
本来想试一试,奈何我连的是学校的网,身边没有路由器可以尝试,不过相关操作我之前用树莓派做过。
通过上面的描述你可能迷糊了,竟然端口映射需要自己去配置,那么公网又是如何去配置端口映射的❓
可以肯定不是人手动进行配置,这就不得不提到 NAT 端口映射的三种方式:
- 静态地址转换 Static NAT:指定将某个公有 IP 地址的端口与局域网的某个私有 IP 地址的端口进行映射,一对一。
- 动态地址转换 Dynamic NAT:随机将某个公用 IP 地址的端口与局域网的某个私有 IP 地址的端口进行映射,一对一。
- 端口地址转换 Port Address Translation:在动态地址转换基础上进行扩展,实现随机将某个公用 IP 地址的端口与局域网的多个私有 IP 地址的端口进行映射,一对多。
像我学校的局域网就是利用端口多路复用的方式进行端口映射的。因为其一对多的特性,需要使用TCP或者UDP来标识局域网的主机,这样可以有效地隐藏局域网的主机,从而避免外网的攻击。
像路由器手动配置 NAT 端口映射就是一种静态转换的方式。
像花生壳的动态端口进行内网穿透就是一种动态转换的方式。
?验证
如果你有服务器的话,可以简单尝试一下。
我正好之前在阿里买了一个轻量应用服务器,可以用来进行验证。
前提条件:
- 服务器安装了 JRE
需要安装自己的服务器系统版本安装对应版本的 JRE,点击这个链接 https://www.java.com/zh-CN/download/manual.jsp 进入官网网站进行下载。
通过 uname -a
来查看当前服务器系统信息
[root@iZuf6e2wna987wh9vbw55oZ ~]# uname -a
Linux iZuf6e2wna987wh9vbw55oZ 3.10.0-1160.11.1.el7.x86_64 #1 SMP Fri Dec 18 16:34:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
因此我只需要下载 Linux x64 版本的 Java 就可以了,然后将其上传到服务器上面去,当然你也可以在服务器上面通过 wget 命令来下载。
具体安装 Java 的方法可以参考:https://www.cnblogs.com/kakarotto5/p/15917485.html,不过这篇文章是安装 JDK,反正方法都是一样的(下载、解压、配置环境变量)。
JRE 配置好后,就可以使用 Java 命令了。
将编译好的 UDPServer.class 文件上传到服务器上面,因为我安装的是 JRE 没有编译器,所有不能直接上传 UDPServer.java 文件。
随后使用 java UDPServer
运行代码接收一次连接即可。
[root@iZuf6e2wna987wh9vbw55oZ project]# java UDPServer
Mar 12, 2022 3:03:29 PM UDPServer main
INFO: UDPServer's Info: 0.0.0.0/0.0.0.0:8808
当然还需要配置一下服务器的防火墙,将对应 IP 开放。
接下来就可以使用本机的客户端去连接服务器了。
[root@iZuf6e2wna987wh9vbw55oZ project]# java UDPServer
Mar 12, 2022 3:03:29 PM UDPServer main
INFO: UDPServer's Info: 0.0.0.0/0.0.0.0:8808
Mar 12, 2022 3:05:57 PM UDPServer main
INFO: UDPClient's Info: /183.162.54.213:52758 connected!
可以看见 183.162.54.213:52758 客户端正常连接上来了。
如果你想具体查看更多信息,可以将一个比较大的文件上传到服务器上面,服务器再将这个文件返回给客户端。
在这个过程中,你可以使用 tasklist /FI "imagename eq java.exe"
命令和 netstat -ano | findstr "PID号"
命令来查看相应的 UDP 连接。
?完整demo代码
// UDPServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.util.logging.Logger;
public class UDPServer{
public static void main(String[] args) throws IOException {
DatagramSocket datagramSocket = new DatagramSocket(new InetSocketAddress("0.0.0.0", 8808));
byte[] data = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
Logger.getGlobal().info(String.format("UDPServer's Info:\t%s:%s",
datagramSocket.getLocalAddress(), datagramSocket.getLocalPort()));
datagramSocket.receive(datagramPacket);
Logger.getGlobal().info(String.format("UDPClient's Info:\t%s:%s\tconnected!",
datagramPacket.getAddress(), datagramPacket.getPort()));
}
}
// UDPClient.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.util.logging.Logger;
public class UDPClient {
public static void main(String[] args) throws IOException {
DatagramSocket datagramSocket = new DatagramSocket();
Logger.getGlobal().info(String.format("UDPClient's Info:\t%s:%s",
datagramSocket.getLocalAddress(), datagramSocket.getLocalPort()));
byte[] data = "hjhcos".getBytes();
DatagramPacket datagramPacket = new DatagramPacket(data, data.length, new InetSocketAddress("你的服务器公网 IP", 8808));
datagramSocket.send(datagramPacket);
Logger.getGlobal().info("发送完毕!");
}
}
注意事项
- 用客户端发送消息需要指明确切的 IP 地址,不建议使用 0.0.0.0 定义 IP 。
如果你是无意刷到这篇文章并看到这里,希望你给我的文章来一个赞赞??。如果你不同意其中的内容或有什么问题都可以在下方评论区留下你的想法或疑惑,谢谢你的支持!!??