☰
Java Socket C/S架构路灯控制系统:协议、线程与避坑指南
2026/10/7 16:02:03 网站建设 项目流程

简介:基于C/S架构的Java模拟路灯控制系统源码,面向学习套接字编程、需完成Java课程大作业的同学。系统使用Swing图形界面库搭建客户端界面,以套接字方式实现双向通信,支持远程开关路灯,同时采集温湿度等模拟环境信息,并将数据实时反馈给客户端。压缩包共36个文件,大小约2.16MB,以13个Java源文件为主体,辅以界面运行截图、项目说明文档和工程配置文件,便于导入开发环境后直接阅读源码。已有309人学习,项目将服务端与客户端分离,覆盖对象流序列化传输、用户事件响应、环境数据随机模拟等核心环节,结构清晰。通过该案例可系统理解C/S架构下网络通信的完整流程,掌握图形界面与网络编程结合的实战方式,也可作为课程设计或大作业答辩的可靠参考。

1. 先搞清楚这套源码在解决什么问题:C/S 架构下的 Java 路灯控制系统

拿到这套基于 C/S 架构的 socket 通信 Java 模拟路灯控制系统源码,我建议你先别急着点运行。从表面看,这是典型的 Java 大作业配置:一个服务端监听端口,一个 Swing 客户端远程控制路灯开关,顺带采集温湿度等环境信息。但真正决定你能不能快速跑通、答辩能不能讲清楚的,是藏在代码里的通信协议、线程模型和连接生命周期管理——这三样恰恰是 socket 编程里最容易翻车的地方。这套源码适合两类人:拿它交 Java 课程设计、不想从零写协议和界面的学生,以及想找个完整 C/S 通信案例练手、想搞懂 socket 底层行为的开发者。前者照着跑通改参数,后者可以直接替换模拟传感器和命令解析做二次开发。

2. 先把协议和线程模型定死:C/S 架构下两个影响成败的设计点

2.1 消息格式:为什么用文本帧而不是 Java 对象流

这个项目里,服务端和客户端唯一的沟通渠道是 socket。很多同学一上来就用 ObjectOutputStream 把"命令对象"整体传过去,比如一个 LightControl 对象带个 switchOn 字段。这样做代码是短了,但坑非常大:两端类路径必须完全一致,包前缀不一样反序列化就炸;调试时抓包全是二进制,看不出客户端到底发了什么;以后想换别的语言写客户端,协议得完全重写。所以在这个量级的课程设计里,常见做法是自定义文本帧——把所有命令和响应拼成一行字符串,用分隔符切分字段。

协议定义其实不复杂,关键是先定死,两端按同一张表实现:

帧类型方向格式服务端返回
心跳客户端→服务端PINGPONG
开灯客户端→服务端CMD:LIGHT:ONACK:LIGHT:ON:OK
关灯客户端→服务端CMD:LIGHT:OFFACK:LIGHT:OFF:OK
查询灯状态客户端→服务端CMD:LIGHT:GETDATA:LIGHT:ON 或 OFF
查询温湿度客户端→服务端CMD:ENV:GETDATA:ENV:26.5:60.3
断开客户端→服务端CMD:QUITACK:QUIT:OK

字段统一用冒号分隔,帧尾以换行符结束。为什么选冒号不选逗号:温湿度值里带小数点,逗号在视觉上和十进制小数点容易混淆,而且 String.split(":") 不需要转义,按冒号切最省事。响应帧统一 ACK 或 DATA 开头,错误帧用 ERR 开头,比如 ERR:UNKNOWN_CMD:FOO,这样客户端收到任何不认识的返回都能落到统一的错误分支。

服务端的解析入口就是一个切分方法:

public static String[] parseFrame(String line) { // 去掉可能残留的 \r,Windows 下 telnet 或 println 会带 \r\n line = line.replace("\r", ""); if (line.isEmpty()) { return new String[0]; } return line.split(":"); // 按冒号切分,数组第 0 位永远是命令类型 }

逻辑说明:先清理 \r 再切分,避免 Windows 换行符混进最后一个字段;空行直接返回空数组,调用方跳过。参数说明:返回数组里下标 0 是命令大类,比如 CMD 或 PING,后面的下标依次是子命令和参数,具体怎么消费在第 3 章讲。

选择按行传输还有一个好处:天然对抗粘包问题。TCP 是字节流,没有消息边界,客户端连发两条命令,服务端一次 read 可能同时读到两条,也可能一条命令被拆成两半到达。但只要约定"一帧一行",读端用 readLine() 等换行符,粘过来的两条会被重新切成两行,半包会在换行符到了之后再返回,逻辑上规避掉了这个经典坑。第 5 章我会专门讲一次这个现象的排查过程。

2.2 线程模型:一连接一线程的取舍与线程池参数

课程设计的并发规模一般不超过 10 个客户端,最朴素的"一连接一线程"完全够用。但直接 new Thread 有两个问题:线程不可控、连接断开后线程不回收。常见做法是用 ExecutorService 包一层,既保留一连接一线程的简单性,又拿到线程复用和回收能力。服务端主循环长这样:

ExecutorService pool = new ThreadPoolExecutor( 2, // 核心线程数 8, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲超过 60 秒回收 new LinkedBlockingQueue<Runnable>(100), // 等待队列容量 new ThreadFactory() { private final AtomicInteger id = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread(r, "light-server-" + id.getAndIncrement()); } } ); try (ServerSocket serverSocket = new ServerSocket(6666)) { System.out.println("[server] listening on 6666"); while (true) { Socket socket = serverSocket.accept(); // 阻塞等待新连接 pool.execute(new ClientHandler(socket)); // 每个连接一个任务 } }

逻辑说明:accept() 阻塞在 6666 端口,每进来一个客户端,就把 Socket 封装成 ClientHandler 丢进线程池执行。线程池核心 2、最大 8、队列 100,超过队列容量时任务会被拒绝——课程设计场景里几乎不会触发。参数说明:核心线程数一般设成 CPU 核数或略低,这里 2 是保守值;最大 8 是因为每个连接同时只有一个阻塞点(readLine),8 个线程能扛 8 个并发连接,再多就要扩到 16 或 32。线程命名工厂是排错利器,服务端日志里出现 light-server-3 就能知道是哪个连接出问题。

为什么不推荐 CachedThreadPool:它最大线程数是 Integer.MAX_VALUE,客户端瞬时大量重连时线程数疯涨,每个线程默认栈 512KB 到 1MB,内存和上下文切换都会被拖垮。而固定线程池 FixedThreadPool 的问题是队列无界,连接超过最大线程数后全部排队,连不上也不报错,排查起来非常玄学。ThreadPoolExecutor 显式给队列上限,至少出问题时有明确报错。

这里有一个容易被忽略的细节:服务端主线程和 ClientHandler 都在同一个 JVM 里,灯开关状态是所有客户端共享的。所以第 3 章那个 lightOn 状态变量必须保证跨线程可见性,这直接决定了该用普通 boolean 还是 volatile——这也是 socket 项目的经典考察点,答辩时基本必问。

3. 服务端落地:灯控命令处理与温湿度采集

3.1 灯控状态共享与命令解析

路灯的核心状态就两个:开和关。这个状态在服务端是全局唯一的,不管哪个客户端发 CMD:LIGHT:ON,灯都应该亮;再发一次 ON,也应该返回成功而不是报错。多个客户端连接线程同时读写这个状态,就需要考虑可见性。常见做法是声明成 volatile boolean,它保证一个线程改了,另一个线程立刻能看到新值;但对于"先查再改"这种复合操作,volatile 不够,得加 synchronized,课程设计到 volatile 这层基本就够了。

ClientHandler 的 run() 方法把连接生命周期和命令处理分开:

public class ClientHandler implements Runnable { private final Socket socket; private static volatile boolean lightOn = false; private final Sensor sensor = new SimulatedSensor(); // 模拟温湿度传感器 public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); PrintWriter out = new PrintWriter( new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) { String line; while ((line = in.readLine()) != null) { String ack = dispatch(parseFrame(line)); if (ack != null) { out.println(ack); } } } catch (IOException e) { System.out.println("[server] connection error: " + e.getMessage()); } finally { closeQuietly(socket); // 无论异常还是正常退出,都释放 socket } } }

逻辑说明:try-with-resources 保证流自动关闭,readLine() 返回 null 表示对端关闭,循环退出后 finally 兜底释放 socket。参数说明:InputStreamReader 和 OutputStreamWriter 都显式用 UTF-8,这是中文协议帧和日志不变成问号的先决条件,后面避坑章还会再强调。

命令解析用 switch 按大类分派:

private String dispatch(String[] parts) { if (parts.length < 2) { return "ERR:EMPTY_CMD"; } String cmd = parts[0] + ":" + parts[1]; // 拼成 CMD:LIGHT 这种形式 switch (cmd) { case "CMD:LIGHT": if (parts.length < 3) { return "ERR:LIGHT_NO_ARG"; } if ("ON".equalsIgnoreCase(parts[2])) { lightOn = true; System.out.println("[server] light -> ON"); return "ACK:LIGHT:ON:OK"; } else if ("OFF".equalsIgnoreCase(parts[2])) { lightOn = false; System.out.println("[server] light -> OFF"); return "ACK:LIGHT:OFF:OK"; } else if ("GET".equalsIgnoreCase(parts[2])) { return "DATA:LIGHT:" + (lightOn ? "ON" : "OFF"); } return "ERR:LIGHT_UNKNOWN_ARG:" + parts[2]; case "CMD:ENV": return handleEnv(parts); default: return "ERR:UNKNOWN_CMD:" + parts[0]; } }

逻辑说明:parseFrame 切出来的 parts[0] 固定是 CMD,parts[1] 是子命令,parts[2] 是操作参数,"CMD" 和 "LIGHT" 拼起来和 case 比较。参数说明:equalsIgnoreCase 让客户端发小写 on/off 也能被识别,减少联调时的大小写翻车;返回的字符串统一由 run() 里的 println 输出,dispatch 只负责构造响应。

这里最容易写错的是把查询灯状态和控制灯状态的返回值混在一起。查询 CMD:LIGHT:GET 必须走 DATA:LIGHT:ON/OFF 返回当前真实状态,不能直接返回"上次操作成功",否则客户端重启后再连接,显示的状态全是缓存,和实际灯的状态对不上,答辩现场演示时很尴尬。

3.2 温湿度采集:模拟传感器与真实接口预留

课程设计没有真实硬件,温湿度常用随机数模拟。但如果在 ClientHandler 里直接 new Random().nextDouble(),以后想接 DHT11 这类真实传感器,就得动命令处理代码。常见做法是先抽一个 Sensor 接口,模拟实现和以后的真实实现都实现同一个接口:

public interface Sensor { /** 返回温湿度,数组 [0] 是温度,[1] 是湿度 */ double[] readEnv(); } public class SimulatedSensor implements Sensor { private static final double TEMP_LOW = 22.0; private static final double TEMP_HIGH = 28.0; private static final double HUMI_LOW = 40.0; private static final double HUMI_HIGH = 70.0; @Override public double[] readEnv() { double temp = ThreadLocalRandom.current().nextDouble(TEMP_LOW, TEMP_HIGH); double humi = ThreadLocalRandom.current().nextDouble(HUMI_LOW, HUMI_HIGH); return new double[] { round1(temp), round1(humi) }; } private double round1(double v) { return Math.round(v * 10) / 10.0; } }

逻辑说明:SimulatedSensor 在 22~28℃、40~70%RH 范围内取随机值并保留一位小数,模拟正常室外的环境波动。参数说明:TEMP_LOW、TEMP_HIGH 等四个边界值就是量程,真实接硬件时换成传感器的量程和读取代码,接口不变,上层不用改。

温湿度查询的响应处理放在 handleEnv:

private String handleEnv(String[] parts) { if (parts.length >= 3 && "GET".equalsIgnoreCase(parts[2])) { double[] env = sensor.readEnv(); String frame = String.format("DATA:ENV:%.1f:%.1f", env[0], env[1]); System.out.println("[server] env -> " + frame); return frame; } return "ERR:ENV_UNKNOWN_ARG"; }

逻辑说明:收到 CMD:ENV:GET 才读一次传感器,组帧后返回;其他参数一律视为未知命令。参数说明:%.1f 控制小数位为 1 位,客户端解析 DATA:ENV:26.5:60.3 时,按冒号切分后下标 2 和 3 直接转 float 就行,不需要额外的精度处理。

如果想让演示效果更"主动",可以加一个定时广播:服务端每 5 秒把当前温湿度推给所有已连接客户端,客户端不查询也能看到数据变化。实现上需要一个线程安全的客户端输出流集合,以及一个全局的 Sensor 实例:

private static final List<PrintWriter> clients = Collections.synchronizedList(new ArrayList<>()); private static final Sensor sensor = new SimulatedSensor(); // 连接建立时 clients.add(out),finally 里 clients.remove(out) java.util.Timer timer = new java.util.Timer("env-broadcast", true); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { double[] env = sensor.readEnv(); String frame = String.format("DATA:ENV:%.1f:%.1f", env[0], env[1]); synchronized (clients) { for (PrintWriter c : clients) { c.println(frame); // 单个客户端写失败不影响广播循环 } } } }, 0, 5000);

逻辑说明:Timer 第二个参数 true 表示 daemon 线程,JVM 退出时不会因为广播线程挂着而无法结束;synchronizedList 的迭代遍历必须手动 synchronized,否则边遍历边被 remove 会抛 ConcurrentModificationException。参数说明:0 是首次执行延迟,5000 是广播周期毫秒数,想改频繁就调小,但不要低于 1000,否则打印刷屏日志。

这份源码里模拟生成温湿度的逻辑就是这一套,答辩时把 Sensor 接口拿出来讲"预留了真实硬件接入点",比单纯说"我生成随机数当温湿度"要加分得多。

4. 客户端落地:远程开关界面与接收线程

4.1 Swing 连接管理:把阻塞 IO 请出事件线程

客户端的界面结构不复杂:IP 输入框、端口输入框、连接按钮、状态标签、路灯开关按钮、温湿度显示标签,最多再加一个日志区。真正的坑不在布局,在 Swing 的线程模型。Swing 是单线程模型,所有 UI 操作必须在事件派发线程(EDT)上执行。如果直接在按钮监听里写 socket.connect(),而目标 IP 不可达,connect 会卡在那儿好几秒,整个窗口拖都拖不动——因为 repaint 事件排不上队。

常见做法是把连接动作丢到子线程,connect 加超时:

private void connectAsync(String host, int port) { connectBtn.setEnabled(false); connectBtn.setText("连接中..."); new Thread(() -> { try { socket = new Socket(); socket.connect(new InetSocketAddress(host, port), 3000); // 3 秒超时 out = new PrintWriter(new OutputStreamWriter( socket.getOutputStream(), StandardCharsets.UTF_8), true); in = new BufferedReader(new InputStreamReader( socket.getInputStream(), StandardCharsets.UTF_8)); startReceiverThread(); // 启动接收线程,见 4.2 SwingUtilities.invokeLater(() -> { stateLabel.setText("已连接"); connectBtn.setText("断开"); }); } catch (IOException e) { SwingUtilities.invokeLater(() -> { stateLabel.setText("连接失败: " + e.getMessage()); connectBtn.setEnabled(true); connectBtn.setText("连接"); }); } }, "connector-thread").start(); }

逻辑说明:connect 放进独立线程,3 秒超时阻止客户端在死 IP 上无限等待;connect 成功后再启动接收线程;所有 UI 更新用 SwingUtilities.invokeLater 切回 EDT。参数说明:3000 是连接超时毫秒数,局域网内可以缩到 1000,跨网段或服务端有防火墙可以放宽到 5000;socket、out、in 都是客户端类的字段,供后面的按钮和接收线程共用。

连接成功后,开灯关灯按钮的监听器就两行:out.println("CMD:LIGHT:ON") 或 out.println("CMD:LIGHT:OFF")。这里不需要在按钮监听里做任何等待,服务端回包由接收线程异步处理。很多新手在这里犯的毛病是点了开灯立刻读 in.readLine() 拿结果——这个 readLine 会阻塞事件线程,而且可能读到的是上一次的残留响应,完全没必要,因为接收线程已经在持续消费响应了。

4.2 响应接收与心跳感知断线

接收线程是整个客户端的核心。它单独跑一个 while 循环,持续 readLine(),读到一行就交给 UI 更新逻辑:

private void startReceiverThread() { receiverThread = new Thread(() -> { String line; try { while (!socket.isClosed() && (line = in.readLine()) != null) { final String msg = line; SwingUtilities.invokeLater(() -> handleServerFrame(msg)); } } catch (IOException e) { if (!socket.isClosed()) { SwingUtilities.invokeLater(() -> stateLabel.setText("连接断开")); } } }, "receiver-thread"); receiverThread.start(); }

逻辑说明:循环条件是"本地 socket 未关且还能读到行",readLine() 返回 null 表示对端关闭,抛 IOException 多半是连接被重置。参数说明:socket.isClosed() 只反映本地关闭状态,对端异常掉线时靠 readLine() 的返回和异常来感知——这正是为什么要配心跳。

收到服务端帧后的分发逻辑:

private void handleServerFrame(String msg) { if (msg.startsWith("DATA:ENV:")) { String[] p = msg.split(":"); envLabel.setText("温度: " + p[2] + " ℃,湿度: " + p[3] + " %RH"); } else if (msg.startsWith("ACK:LIGHT:")) { String[] p = msg.split(":"); lightLabel.setText(p[2].equalsIgnoreCase("ON") ? "灯: 亮" : "灯: 灭"); } else if (msg.startsWith("PONG")) { lastPongTime = System.currentTimeMillis(); } }

逻辑说明:按前缀分发不同帧类型,DATA:ENV:26.5:60.3 切分后 p[2] 是温度、p[3] 是湿度;ACK:LIGHT:ON:OK 切分后 p[2] 直接反映灯的最终状态。参数说明:lastPongTime 必须用 volatile 修饰,因为心跳线程和接收线程是两个线程在读写同一个字段,不加 volatile 可能出现心跳线程读到的永远是旧值,误判断线。

心跳用 java.util.Timer 做定时任务:

private volatile long lastPongTime = System.currentTimeMillis(); java.util.Timer heartbeat = new java.util.Timer("heartbeat", true); heartbeat.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if (out != null) { out.println("PING"); long gap = System.currentTimeMillis() - lastPongTime; if (gap > 15000) { SwingUtilities.invokeLater(() -> stateLabel.setText("心跳超时,正在重连")); // 触发重连逻辑:关闭旧 socket,再走一遍 connectAsync } } } }, 0, 5000);

逻辑说明:每 5 秒发一个 PING,15 秒没收到 PONG 就判定链路失效。参数说明:5000 是心跳发送周期,15000 是超时阈值,阈值一般是发送周期的 2~3 倍,给网络抖动留余量;Timer 的 daemon 标志保证客户端关窗后 JVM 能正常退出。

心跳解决的是"拔网线式"断线。TCP 本身不会在物理链路断掉的瞬间通知应用层,如果没有心跳,客户端会一直显示"已连接",直到下一次真正读写数据才报错。课程设计的演示环境里经常有人直接拔网线换网口,这个细节能救一次答辩。

5. 联调避坑:从连不上到数据错乱的五条血泪记录

5.1 三个运行时翻车现场

第一条:服务端启动直接报 BindException。

现象:运行 Server 主类,控制台立刻抛出 java.net.BindException: Address already in use,端口起不来。原因:上一个服务端进程没退干净,6666 端口还被占用。Windows 下最常见的场景是 IDE 里上一次运行的任务没停掉,又点了一次 Run;或者程序在后台挂着,关闭控制台窗口时 JVM 没有退出。解决:先确认端口被谁占了。Windows 执行 netstat -ano | findstr 6666 拿到 PID,再 taskkill /F /PID 那个 PID;Linux 用 lsof -i:6666。也可以偷懒直接改 ServerSocket 的端口,但答辩前最好养成先查端口的习惯,因为改了端口还得同步改客户端,忘改就是下一个坑。

第二条:客户端显示已连接,但点开关服务端没有任何日志。

现象:连接按钮状态正常,按钮事件也触发了,但服务端控制台一动不动。原因:命令帧没被服务端 readLine 读到,最典型的是发送端用了 print() 而不是 println(),数据还在发送缓冲区里没有换行符,readLine 会一直等;或者命令串里多了尾随空格,比如 "CMD:LIGHT:ON ",parseFrame 切出来 parts[2] 是 "ON ",equalsIgnoreCase 匹配失败。解决:统一用 println 发送;服务端在 dispatch 前加一行 System.out.println("received: [" + line + "]"),把收到的原始帧打出来。括号一框住,尾随空格立刻现形。我一般调 socket 的第一动作就是服务端打印原始行,先确认服务端看到了什么,再去谈解析逻辑对不对。

第三条:界面点了连接就卡死,窗口拖不动。

现象:点连接按钮后整个 Swing 界面变成假死,Windows 上甚至出现无响应的标题。原因:connect 或 readLine 写在了事件派发线程上,阻塞了 EDT,所有 UI 刷新事件全部排不上队。解决:按第 4 章的模式,connect 放子线程、readLine 放接收线程,UI 更新用 SwingUtilities.invokeLater 切回 EDT。这条属于结构性错误,改对了线程模型就再也不会出现,而不是靠换一台性能更好的电脑。

5.2 协议层两个隐蔽坑:粘包半包与编码乱码

第四条:两条命令粘在一起,或者一条命令被切了一半。

现象:服务端 readLine 一次读到 "CMD:LIGHT:ONCMD:ENV:GET" 这种拼接串,或者收到 "CMD:L" 这种半截。原因:TCP 是字节流协议,没有消息边界,底层一次发送可能聚合多条消息,也可能拆分一条消息;这是 TCP 的固有行为,不是代码 bug。解决:把协议定成"一帧一行",换行符就是边界,readLine() 会等到一个完整换行帧才返回,粘在一起的多条会被重新切成多行,半截的会等到换行符到了再返回。前提是发送端绝不能把两条命令拼在一行里,也不能在命令中间加换行。第 2 章的协议表之所以要求帧尾必须换行,就是为了让这一条成立。

第五条:界面显示正常,但日志和标签里的中文全是问号。

现象:温度数字没问题,灯状态和日志里的中文全变成 ??。原因:字符集不统一。Windows 控制台默认 GBK,Java 的 InputStreamReader 不指定字符集就用平台默认编码;服务端按 UTF-8 解码、客户端按 GBK 编码发送数据时,中文必然乱码。解决:两端所有 Reader 和 Writer 都显式指定 StandardCharsets.UTF_8;如果是控制台输出乱码,给 JVM 启动参数加 -Dfile.encoding=UTF-8;IDEA 里把 File Encoding 和 Console Encoding 全部改成 UTF-8。协议帧本身尽量纯英文,中文只出现在日志和界面标签里,这样即使编码配置漏了一处,也不影响命令解析。

注意:以上五条是 socket 课程设计里出现频率最高的联调问题。复现这套源码时如果卡住,按"先看服务端日志原始行、再查监听端口、最后查两端编码"的顺序排查,能少走一半弯路。

6. 进阶玩法:协议扩展与 telnet 自测

6.1 加一个亮度调节命令

基础版只支持开和关,答辩想加分可以加亮度调节。协议表新增一行 CMD:LIGHT:BRIGHT:80,服务端解析时多取一个参数,范围限定 0~100,存到 volatile int。核心改动就一处:dispatch 的 CMD:LIGHT 分支里加一个 else if:

} else if ("BRIGHT".equalsIgnoreCase(parts[2])) { int value = Integer.parseInt(parts[3]); // 从帧里取出亮度值 brightness = Math.max(0, Math.min(100, value)); // 限幅到 0~100 return "ACK:LIGHT:BRIGHT:" + brightness + ":OK"; }

逻辑说明:Integer.parseInt 可能抛 NumberFormatException,可以用 try-catch 包住返回 ERR 帧,这是扩展命令时最容易漏的防御。参数说明:亮度值限幅到 0~100 是防止客户端传 999 把状态搞坏;客户端用下拉框或滑块限制输入范围,服务端再做一次兜底,两层校验。

6.2 用 telnet 在不开界面的情况下验证协议

我强烈建议每次改完协议,先用 telnet 冒充客户端测一遍,再打开 Swing 界面。这一步能把协议问题和界面问题彻底隔离开。Windows 的 telnet 客户端可能要手动开启,Linux 和 Mac 自带。连接命令和手动输入如下:

telnet 127.0.0.1 6666 PING CMD:LIGHT:ON CMD:LIGHT:GET CMD:ENV:GET

正常会依次看到 PONG、ACK:LIGHT:ON:OK、DATA:LIGHT:ON、DATA:ENV:26.5:60.3。如果在这里就断了,说明协议或服务端有问题,根本不用碰客户端代码;如果 telnet 正常但 Swing 客户端不对,那问题一定在客户端的发送或接收线程,排查范围直接缩小一半。从那以后我每次接手 socket 相关的课程设计,都强制自己先用 telnet 把协议层完整走一遍,再打开界面点按钮。这个习惯帮我过滤掉了至少一半的联调问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询