mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 16:00:43 +00:00
琚帶绔?- 淇 MTU 闂锛氬す MSS + 璺緞 MTU 鎺㈡祴锛堝叕缃戝彲鐢ㄧ巼 48% -> 100%锛?- 澶嶇敤 HTTP 杩炴帴锛堝厤鎺夋瘡娆℃柊寤?TLS 鐨?30% 鎻℃墜澶辫触锛?- 姣忓垎閽熷洖澶存壘灞€鍩熺綉锛岃嚜鍔ㄥ垏鍥炲揩 400 鍊嶇殑璺緞 - 鐢婚潰蹇冭烦甯э細闈欐鏃剁渷娴侀噺锛屽鎴风浠嶇煡閬撴祦鏄椿鐨?- 閿佸睆鏃剁粰鍑哄彲鎿嶄綔鐨勬彁绀? 鎵嬭〃绔?- 鍐呯疆 GTS Root R4 / GlobalSign Root CA锛屼慨 SSLHandshakeException - 杩炴帴閿欒缈昏瘧鎴愪汉璇濓紙璇佷功/鏃堕棿/瑙f瀽/瓒呮椂鍒嗗埆鎻愮ず锛?- 缁忎腑杞繛涓婂悗鐢ㄦ湇鍔″櫒鎶ョ殑鍦板潃鍒锋柊宸查厤瀵硅褰?- 鏂板灞€鍩熺綉杩炴帴椤甸潰銆佹湇鍔″櫒寤惰繜妫€娴? 鏈嶅姟鍣ㄧ - 鏀寔 chunked 璇锋眰浣擄紙淇瘡 16 绉掍竴娆$殑杩炴帴閿欎贡涓庡ぇ闈㈢Н 502锛?- 缃戠粶璋冧紭鑴氭湰 net-tune.sh锛堝紑鏈鸿嚜鍔ㄥ簲鐢級 - 绠$悊椤碉細鎵归噺鍒犻櫎銆佺綉缁滀綋妫€銆佷笅杞藉尯 - 闅ч亾鏀?HTTP/2 + 鍙岃繛鎺? 鏂囨。 - 璁稿彲鍗忚 / 瀹夎鍓嶅繀璇?/ 瀹夎鍚庨』鐭ワ紙浣滆€咃細灏忛害瀛愶級
343 lines
12 KiB
Java
343 lines
12 KiB
Java
package com.lanremote.watch;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.net.SocketTimeoutException;
|
|
import java.nio.charset.Charset;
|
|
import java.util.Arrays;
|
|
import java.util.Map;
|
|
import java.util.concurrent.BlockingQueue;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* 被控端协议客户端(纯 Java,无 Android 依赖)。
|
|
*
|
|
* 之所以保持纯 Java:可以把它编译到桌面 JVM,直接连真实的 Python 被控端做联调,
|
|
* 而不是只靠"看起来应该没问题"。
|
|
*/
|
|
public class RemoteClient {
|
|
|
|
public static final Charset UTF8 = Charset.forName("UTF-8");
|
|
public static final int CHUNK_SIZE = 256 * 1024;
|
|
public static final int DEFAULT_PORT = 8765;
|
|
|
|
/** 事件与断开回调。注意:回调发生在读取线程,UI 层需自行切主线程。 */
|
|
public interface Listener {
|
|
void onEvent(String event, Map<String, Object> message);
|
|
|
|
void onDisconnected(String reason);
|
|
}
|
|
|
|
/**
|
|
* 传输层。局域网直连时是 SocketTransport,跨网络时是 RelayTransport。
|
|
* 抽出来之后协议逻辑完全不用管数据是走 TCP 还是走中转的 HTTPS。
|
|
*/
|
|
private Transport transport;
|
|
private BufferedReader reader;
|
|
private OutputStream writer;
|
|
private Thread readerThread;
|
|
/** 单行上限,和服务端的 MAX_LINE_BYTES 对齐 */
|
|
private static final int MAX_LINE_CHARS = 8 * 1024 * 1024;
|
|
|
|
private volatile boolean running;
|
|
|
|
private final BlockingQueue<Map<String, Object>> replies =
|
|
new LinkedBlockingQueue<Map<String, Object>>();
|
|
private final Object sendLock = new Object();
|
|
|
|
private Listener listener;
|
|
private String serverName = "";
|
|
private String deviceId = "";
|
|
private String deviceName = "手表";
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
public boolean isConnected() {
|
|
return running && transport != null && !transport.isClosed();
|
|
}
|
|
|
|
/** 当前传输方式的描述,界面上用来显示"直连"还是"经中转"。 */
|
|
public String transportName() {
|
|
if (transport == null) {
|
|
return "";
|
|
}
|
|
return transport instanceof RelayTransport ? "中转" : "直连";
|
|
}
|
|
|
|
public RelayTransport relayTransport() {
|
|
return transport instanceof RelayTransport
|
|
? (RelayTransport) transport : null;
|
|
}
|
|
|
|
public String getServerName() {
|
|
return serverName;
|
|
}
|
|
|
|
public void setDevice(String deviceId, String deviceName) {
|
|
this.deviceId = deviceId == null ? "" : deviceId;
|
|
this.deviceName = deviceName == null ? "手表" : deviceName;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 连接与握手
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* 连接并完成握手。
|
|
*
|
|
* @param token 为空则用配对码配对;非空则直接令牌登录(下次打开免配对码)
|
|
* @return 服务端握手回包,含 ok / token / serverName
|
|
*/
|
|
public Map<String, Object> connect(String host, int port, String code, String token,
|
|
Listener listener, int timeoutMs) throws IOException {
|
|
close();
|
|
this.listener = listener;
|
|
this.running = true;
|
|
this.replies.clear();
|
|
|
|
Socket sock = new Socket();
|
|
sock.connect(new InetSocketAddress(host, port), timeoutMs);
|
|
sock.setSoTimeout(0);
|
|
sock.setTcpNoDelay(true);
|
|
return startTransport(new Transport.SocketTransport(sock), code, token,
|
|
listener, timeoutMs);
|
|
}
|
|
|
|
/**
|
|
* 经中转服务器连接(跨网络)。
|
|
*
|
|
* 手表没法直连电脑(两边都在 NAT 后面),所以两边都主动连出去,
|
|
* 由中转按配对码把两条连接对上。中转只转发字节、不认识协议内容,
|
|
* 所以电脑那边的配对和令牌校验照样生效。
|
|
*/
|
|
/** 上次经中转接入时,被控端报的局域网 IP 和端口。 */
|
|
private java.util.List<String> relayLanIps = new java.util.ArrayList<String>();
|
|
private int relayLanPort;
|
|
|
|
public java.util.List<String> relayLanIps() {
|
|
return relayLanIps;
|
|
}
|
|
|
|
public int relayLanPort() {
|
|
return relayLanPort;
|
|
}
|
|
|
|
public Map<String, Object> connectViaRelay(String baseUrl, String code,
|
|
String token, Listener listener,
|
|
int timeoutMs) throws IOException {
|
|
close();
|
|
relayLanIps = new java.util.ArrayList<String>();
|
|
relayLanPort = 0;
|
|
RelayTransport relay = new RelayTransport(baseUrl, code, deviceName);
|
|
if (!relay.attach()) {
|
|
String reason = relay.getLastError();
|
|
if (reason == null || reason.isEmpty()) {
|
|
reason = "连不上中转服务器";
|
|
}
|
|
relay.close();
|
|
// 和局域网那条路保持一致:握手被拒要回 ok=false,不要抛异常。
|
|
// 抛异常的话界面只能显示"连不上",分不清是配对码错了、
|
|
// 设备不在线、还是中转服务器连不上 —— 这三者的处理方式完全不同。
|
|
this.serverName = "";
|
|
return Json.obj("ok", false, "error", reason);
|
|
}
|
|
Map<String, Object> reply =
|
|
startTransport(relay, code, token, listener, timeoutMs);
|
|
if (Json.bool(reply, "ok", false)) {
|
|
// 留给界面判断:要不要切成局域网直连
|
|
relayLanIps = relay.getLanIps();
|
|
relayLanPort = relay.getLanPort();
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
/** 握手 + 起读取线程。局域网和中转共用这一段。 */
|
|
private Map<String, Object> startTransport(Transport channel, String code,
|
|
String token, Listener listener,
|
|
int timeoutMs) throws IOException {
|
|
this.listener = listener;
|
|
this.running = true;
|
|
this.replies.clear();
|
|
this.transport = channel;
|
|
this.reader = new BufferedReader(
|
|
new InputStreamReader(channel.input(), UTF8), 8192);
|
|
this.writer = channel.output();
|
|
|
|
Thread thread = new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
readLoop();
|
|
}
|
|
}, "lanremote-reader");
|
|
thread.setDaemon(true);
|
|
thread.start();
|
|
this.readerThread = thread;
|
|
|
|
Map<String, Object> handshake;
|
|
if (token != null && token.length() > 0) {
|
|
handshake = Json.obj("cmd", "auth", "deviceId", deviceId,
|
|
"deviceName", deviceName, "token", token);
|
|
} else {
|
|
handshake = Json.obj("cmd", "pair", "deviceId", deviceId,
|
|
"deviceName", deviceName, "code", code == null ? "" : code.trim());
|
|
}
|
|
|
|
Map<String, Object> reply;
|
|
try {
|
|
reply = call(handshake, timeoutMs);
|
|
} catch (IOException failed) {
|
|
// 握手失败必须关掉,否则 socket 和读取线程泄漏,
|
|
// 而且 isConnected() 会一直是 true,界面以为连上了。
|
|
close();
|
|
throw failed;
|
|
}
|
|
if (Json.bool(reply, "ok", false)) {
|
|
this.serverName = Json.str(reply, "serverName", "");
|
|
} else {
|
|
close();
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
public void close() {
|
|
running = false;
|
|
try {
|
|
if (transport != null) {
|
|
transport.close();
|
|
}
|
|
} catch (Exception ignored) {
|
|
// 关闭时的异常无需处理
|
|
}
|
|
transport = null;
|
|
reader = null;
|
|
writer = null;
|
|
replies.clear();
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 收发
|
|
// ------------------------------------------------------------------
|
|
|
|
private void readLoop() {
|
|
try {
|
|
// close() 会把 reader 置 null,而 running 判断和 readLine 之间有竞态。
|
|
// 先取到局部变量并判空,避免 NPE —— 读取线程里的未捕获 NPE 会杀掉整个进程。
|
|
BufferedReader localReader = this.reader;
|
|
if (localReader == null) {
|
|
return;
|
|
}
|
|
String line;
|
|
while (running && (line = localReader.readLine()) != null) {
|
|
if (line.length() > MAX_LINE_CHARS) {
|
|
continue; // 对端发超长单行,直接丢弃防止占满内存
|
|
}
|
|
if (line.trim().isEmpty()) {
|
|
continue;
|
|
}
|
|
Map<String, Object> message;
|
|
try {
|
|
message = Json.parseObject(line);
|
|
} catch (Exception malformed) {
|
|
continue; // 单条报文损坏不影响整条连接
|
|
}
|
|
if (message.containsKey("event")) {
|
|
if (listener != null) {
|
|
listener.onEvent(String.valueOf(message.get("event")), message);
|
|
}
|
|
} else {
|
|
replies.offer(message);
|
|
}
|
|
}
|
|
} catch (IOException ignored) {
|
|
// 连接被关闭或网络中断,走下面的统一处理
|
|
} finally {
|
|
boolean notify = running;
|
|
running = false;
|
|
if (notify && listener != null) {
|
|
listener.onDisconnected("连接已断开");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void send(Map<String, Object> message) throws IOException {
|
|
OutputStream out = writer;
|
|
if (out == null) {
|
|
throw new IOException("尚未连接");
|
|
}
|
|
byte[] payload = (Json.write(message) + "\n").getBytes(UTF8);
|
|
synchronized (sendLock) {
|
|
out.write(payload);
|
|
out.flush();
|
|
}
|
|
}
|
|
|
|
/** 发一条命令并等待应答。 */
|
|
public Map<String, Object> call(Map<String, Object> message, long timeoutMs)
|
|
throws IOException {
|
|
if (!isConnected()) {
|
|
throw new IOException("尚未连接");
|
|
}
|
|
send(message);
|
|
return awaitReply(timeoutMs);
|
|
}
|
|
|
|
private Map<String, Object> awaitReply(long timeoutMs) throws IOException {
|
|
try {
|
|
Map<String, Object> reply = replies.poll(timeoutMs, TimeUnit.MILLISECONDS);
|
|
if (reply == null) {
|
|
throw new SocketTimeoutException("等待服务端响应超时");
|
|
}
|
|
return reply;
|
|
} catch (InterruptedException interrupted) {
|
|
Thread.currentThread().interrupt();
|
|
throw new IOException("等待响应被中断");
|
|
}
|
|
}
|
|
|
|
public Map<String, Object> sendString(String cmd, String key, String value, long timeoutMs)
|
|
throws IOException {
|
|
Map<String, Object> message = Json.obj("cmd", cmd);
|
|
message.put(key, value);
|
|
return call(message, timeoutMs);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 分块传输(图片 / 音频 / 文件)
|
|
// ------------------------------------------------------------------
|
|
|
|
public Map<String, Object> sendChunks(String cmd, String name, byte[] blob, long timeoutMs)
|
|
throws IOException {
|
|
int total = blob.length == 0 ? 1 : (blob.length + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
|
for (int index = 0; index < total; index++) {
|
|
int from = index * CHUNK_SIZE;
|
|
int to = Math.min(blob.length, from + CHUNK_SIZE);
|
|
byte[] piece = blob.length == 0 ? new byte[0] : Arrays.copyOfRange(blob, from, to);
|
|
Map<String, Object> message = Json.obj(
|
|
"cmd", cmd,
|
|
"name", name,
|
|
"seq", index,
|
|
"last", index == total - 1,
|
|
"data", B64.encode(piece));
|
|
send(message);
|
|
}
|
|
return awaitReply(timeoutMs);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 便捷封装
|
|
// ------------------------------------------------------------------
|
|
|
|
public boolean ping(long timeoutMs) {
|
|
try {
|
|
return Json.bool(call(Json.obj("cmd", "ping"), timeoutMs), "pong", false);
|
|
} catch (IOException e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|