五分钟上手
这一页只做一件事:让你在五分钟内跑起来一个真正能收发数据的程序,并且知道每一行代码在做什么。
跑完之后,你会得到一个最小的「回显(echo)服务」:客户端发一句话,服务端原样回一句话。
你会写出什么
Section titled “你会写出什么”| 文件 | 职责 | 关键接口 |
|---|---|---|
StringProtocol.java | 决定「一段字节」在哪里切开 | Protocol<T> |
EchoServer.java | 收到消息后回写 | AioQuickServer、MessageProcessor<T> |
EchoClient.java | 连接、发送、接收响应 | AioQuickClient、AioSession |
第 0 步:准备环境
Section titled “第 0 步:准备环境”| 项 | 要求 |
|---|---|
| JDK | 8 及以上(框架本身就是用 JDK 8 编译发布的) |
| 构建工具 | Maven 3.6+ |
| 当前版本 | 2.2.0 |
在 pom.xml 中加入依赖:
<dependency> <groupId>io.github.smartboot.socket</groupId> <artifactId>aio-core</artifactId> <version>2.2.0</version></dependency>如果拉取依赖较慢,可以配置阿里云镜像:
<repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/nexus/content/groups/public/</url> </repository></repositories>第 1 步:定义协议
Section titled “第 1 步:定义协议”TCP 是字节流,不是消息流。一次 read 可能读到半条消息,也可能读到三条半。所以第一件事是约定:这段字节从哪里切开。
我们采用最常见、也最省事的方案——长度前缀:先写 4 字节的 int 表示正文长度,再写正文。
import io.github.smartboot.socket.Protocol;import io.github.smartboot.socket.transport.AioSession;
import java.nio.ByteBuffer;import java.nio.charset.StandardCharsets;
public class StringProtocol implements Protocol<String> {
@Override public String decode(ByteBuffer readBuffer, AioSession session) { // 1. 连长度字段都不够 4 字节,等下一批数据 if (readBuffer.remaining() < Integer.BYTES) { return null; }
// 2. 打个标记,读错了好回退 readBuffer.mark(); int length = readBuffer.getInt();
// 3. 长度非法,说明字节流已经对不齐了,直接报错断开 if (length < 0 || length > readBuffer.capacity() - Integer.BYTES) { throw new IllegalStateException("非法的消息长度: " + length); }
// 4. 正文还没到齐,回退到标记位,等下一批数据 if (readBuffer.remaining() < length) { readBuffer.reset(); return null; }
// 5. 正文到齐,取出来 byte[] data = new byte[length]; readBuffer.get(data); return new String(data, StandardCharsets.UTF_8); }}这段代码有三条契约,必须记住,后面所有协议都遵守它:
| 契约 | 含义 |
|---|---|
返回 null | 「这条消息还没收完」,框架保留数据、继续等下一次读取。不是出错,也不是丢弃。 |
| 返回对象 | 成功解出一条完整消息,框架会立刻交给 MessageProcessor.process()。 |
| 抛异常 | 字节流已无法恢复(长度越界等),框架触发 DECODE_EXCEPTION 并关闭会话。 |
第 2 步:写服务端
Section titled “第 2 步:写服务端”import io.github.smartboot.socket.MessageProcessor;import io.github.smartboot.socket.transport.AioQuickServer;import io.github.smartboot.socket.transport.AioSession;import io.github.smartboot.socket.transport.WriteBuffer;
import java.io.IOException;
public class EchoServer {
public static void main(String[] args) throws IOException { // 收到一条完整消息后,在这里处理 MessageProcessor<String> processor = (session, msg) -> { System.out.println("收到: " + msg);
WriteBuffer writeBuffer = session.writeBuffer(); try { byte[] data = msg.getBytes(); writeBuffer.writeInt(data.length); writeBuffer.write(data); writeBuffer.flush(); } catch (IOException e) { // 对端已断开时写会抛 IOException,属于正常情况 System.out.println("回写失败: " + e.getMessage()); } };
// 端口、协议、处理器 —— 三个参数就够启动 AioQuickServer server = new AioQuickServer(8888, new StringProtocol(), processor); server.start();
System.out.println("服务端已启动: tcp://0.0.0.0:8888"); }}start() 不会阻塞,绑定端口后立刻返回。框架内部的 IO 线程是非守护线程,所以 main 执行完 JVM 也不会退出,服务会一直跑着。
想主动停服,调用 server.shutdown()。
第 3 步:写客户端
Section titled “第 3 步:写客户端”import io.github.smartboot.socket.MessageProcessor;import io.github.smartboot.socket.transport.AioQuickClient;import io.github.smartboot.socket.transport.AioSession;import io.github.smartboot.socket.transport.WriteBuffer;
import java.nio.charset.StandardCharsets;
public class EchoClient {
public static void main(String[] args) throws Exception { // 收到服务端响应后,在这里处理 MessageProcessor<String> processor = (session, msg) -> System.out.println("收到响应: " + msg);
AioQuickClient client = new AioQuickClient("localhost", 8888, new StringProtocol(), processor); AioSession session = client.start();
WriteBuffer writeBuffer = session.writeBuffer(); byte[] data = "hello smart-socket".getBytes(StandardCharsets.UTF_8); writeBuffer.writeInt(data.length); writeBuffer.write(data); writeBuffer.flush(); // 不 flush,数据只会躺在本地缓冲区里
// 等响应回来(真实项目里应该用 CountDownLatch / CompletableFuture) Thread.sleep(1000);
client.shutdownNow(); }}第 4 步:运行
Section titled “第 4 步:运行”先跑 EchoServer.main(),再跑 EchoClient.main()。
服务端控制台:
服务端已启动: tcp://0.0.0.0:8888收到: hello smart-socket客户端控制台:
收到响应: hello smart-socket看到这两行,闭环就打通了。
第 5 步:刚才发生了什么
Section titled “第 5 步:刚才发生了什么”一条消息从网卡到你的代码,中间经过这几个环节:
┌─────────────── IO 线程(非你的线程)───────────────┐网卡 ──▶ │ readBuffer 累积字节 │ │ ↓ │ │ Protocol.decode() → 返回 null:继续等 │ │ → 返回对象:交给下一步 │ │ ↓ │ │ MessageProcessor.process(session, msg) ← 你的代码 │ │ ↓ │ │ WriteBuffer.writeInt / write │ └──────┼────────────────────────────────────────────┘ ↓ flush() → 进入发送队列 → 异步写回网卡三个必须理解的结论:
process()运行在 IO 线程上。它阻塞多久,这条 IO 线程上的其他连接就得等多久。耗时逻辑(查库、调 RPC)请自己丢线程池,别在里面sleep。flush()是必须的。writeInt/write只是把数据放进本地 chunk 缓冲区;不 flush 就不会进入发送队列。(服务端在每轮读取处理结束后框架会自动 flush 一次,所以上面的服务端代码即使漏写也大概率能跑通——但别依赖这个行为,显式 flush 才是对的。)AioSession代表一条连接,可以安全地跨线程持有并写数据,这是做服务端推送的基础。
| 现象 | 原因 | 处理 |
|---|---|---|
| 客户端一直收不到响应 | 写了但没 flush() | 补上 flush() |
| 服务端收到乱码 | 两端字节序或字符集不一致 | 统一 getInt() 读、writeInt() 写;字符集显式指定 UTF_8 |
| 单条消息超过读缓冲区(默认 512 字节) | 上面这个简化版会判定长度非法并断开 | 用 FixedLengthFrameDecoder 收大包,见协议设计 |
write 抛 IOException: writeBuffer has closed | 会话已关闭还在写 | 先判断 session.isInvalid(),或直接捕获后忽略 |
启动报 Address already in use | 端口被占用 | 换端口,或先 server.shutdown() |
| 想解决的问题 | 去看 |
|---|---|
搞懂 AioSession、缓冲区、线程模型 | 核心概念 |
| 消息变长、变大、带分隔符,怎么正确拆包 | 协议设计 |
| 线程数、缓冲区、超时怎么配 | 配置详解 |
| 心跳、限流、SSL、监控 | 插件机制 |
| 断线重连、服务端推送、UDP | 进阶实战 |