跳转到内容

五分钟上手

这一页只做一件事:让你在五分钟内跑起来一个真正能收发数据的程序,并且知道每一行代码在做什么。

跑完之后,你会得到一个最小的「回显(echo)服务」:客户端发一句话,服务端原样回一句话。

文件职责关键接口
StringProtocol.java决定「一段字节」在哪里切开Protocol<T>
EchoServer.java收到消息后回写AioQuickServer、MessageProcessor<T>
EchoClient.java连接、发送、接收响应AioQuickClient、AioSession
项要求
JDK8 及以上(框架本身就是用 JDK 8 编译发布的)
构建工具Maven 3.6+
当前版本2.2.0

在 pom.xml 中加入依赖:

pom.xml
<dependency>
<groupId>io.github.smartboot.socket</groupId>
<artifactId>aio-core</artifactId>
<version>2.2.0</version>
</dependency>

如果拉取依赖较慢,可以配置阿里云镜像:

pom.xml
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>

TCP 是字节流,不是消息流。一次 read 可能读到半条消息,也可能读到三条半。所以第一件事是约定:这段字节从哪里切开。

我们采用最常见、也最省事的方案——长度前缀:先写 4 字节的 int 表示正文长度,再写正文。

StringProtocol.java
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 并关闭会话。
EchoServer.java
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()。

EchoClient.java
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();
}
}

先跑 EchoServer.main(),再跑 EchoClient.main()。

服务端控制台:

服务端已启动: tcp://0.0.0.0:8888
收到: hello smart-socket

客户端控制台:

收到响应: hello smart-socket

看到这两行,闭环就打通了。

一条消息从网卡到你的代码,中间经过这几个环节:

┌─────────────── IO 线程(非你的线程)───────────────┐
网卡 ──▶ │ readBuffer 累积字节 │
│ ↓ │
│ Protocol.decode() → 返回 null:继续等 │
│ → 返回对象:交给下一步 │
│ ↓ │
│ MessageProcessor.process(session, msg) ← 你的代码 │
│ ↓ │
│ WriteBuffer.writeInt / write │
└──────┼────────────────────────────────────────────┘
↓
flush() → 进入发送队列 → 异步写回网卡

三个必须理解的结论:

  1. process() 运行在 IO 线程上。它阻塞多久,这条 IO 线程上的其他连接就得等多久。耗时逻辑(查库、调 RPC)请自己丢线程池,别在里面 sleep。
  2. flush() 是必须的。writeInt / write 只是把数据放进本地 chunk 缓冲区;不 flush 就不会进入发送队列。(服务端在每轮读取处理结束后框架会自动 flush 一次,所以上面的服务端代码即使漏写也大概率能跑通——但别依赖这个行为,显式 flush 才是对的。)
  3. 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进阶实战