代码逻辑以及核心代码实现
模块逻辑
客户端:
- Client:主入口函数,启动tcpCLient, 启动udpSearcher,负责从键盘接收输入string, 并用tcp转发;
- UDPSearcher: 主要负责 searchServer, 启动监听线程,发送广播,接收UDP报文,并解析出server bean。
监听线程:DatagramSocket receive DatagramPacket - TCPClient: 负责发起连接,处理读写。 startWith里面用socketChannel,connect, 读写通过继承的connector里面来实现。
服务端
- server: 主函数入口, 启动tcpServer,启动UDPProvider,接收键盘输入并转发给所有的注册客户端。
- UDPProvider:监听指定端口接收UDP报文, 解析UDP报文, 构造回送数据,并回送
- TCPServer:启动selector,channel, 注册 accept事件,启动客户端监听(start函数); 客户端监听: 拿到selector以及里面所有的key,对于accept事件,通过serverChannel.accept拿到对应的socketChannel,然后对每个key。建立一个Client Handler(处理IO)。异步转发接收到的消息,使用线程池。【里面额外有两个函数,一个是server主函数用的功能broadcast,一个是重写的callback的onNewMessageArrived】
代码整体框架
客户端:
- Client.java main客户端入口程序
- UDPSearcher.searchServer广播搜索服务端;
- TCPClient.startWith(info); 启动客户端业务逻辑。提供收发handler
Client.java
import java.io.IOException;
import java.io.BufferedReader; //处理流
import java.io.InputStream; //字节流
import java.io.InputStreamReader; //字符流
public class Client {
public static void main(String[] args) {
//UDP搜索
ServerInfo info = UDPSearcher.searchServer(10000);
System.out.println("Server:" + info);
if (info != null) {
TCPClient tcpClient = null;
try {
//尝试TCP连接
tcpClient = TCPClient.startWith(info);
if (tcpClient == null) {
return;
}
write(tcpClient);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (tcpClient != null) {
tcpClient.exit();
}
}
}
}
private static void write(TCPClient tcpClient) throws IOException {
// 构建键盘输入流
InputStream in = System.in;
BufferedReader input = new BufferedReader(new InputStreamReader(in));
do {
// 键盘读取一行
String str = input.readLine();
// 发送到服务器
tcpClient.send(str);
//实现客户端退出
if ("00bye00".equalsIgnoreCase(str)) {
break;
}
} while (true);
}
}
UDPSearcher
- public static ServerInfo searchServer(int timeout);//总的逻辑函数,里面会调用listen, sendBroadcast()等函数
- private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException;//启动监听线程
- private static void sendBroadcast() //局域网内发送广播的
import java.io.IOException;
import java.net.DatagramPacket;//UDP协议的数据格式c
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;//栅栏
import java.util.concurrent.TimeUnit;
public class UDPSearcher {
private static final int LISTEN_PORT = UDPConstants.PORT_CLIENT_RESPONSE;
/**
UDP搜索,使用receiveCountLatch使当前线程等待 server回送信息
打开监听
发送广播
使当前线程等待receiveLatch之后再继续执行
*/
public static ServerInfo searchServer(int timeout) {
System.out.println("UDPSearcher Started.");
// 成功收到回送的栅栏
CountDownLatch receiveLatch = new CountDownLatch(1);
Listener listener = null;
try {
listener = listen(receiveLatch);
sendBroadcast();
receiveLatch.await(timeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
// 完成
System.out.println("UDPSearcher Finished.");
if (listener == null) {
return null;
}
List<ServerInfo> devices = listener.getServerAndClose();
if (devices.size() > 0) {
return devices.get(0);
}
return null;
}
private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException {
System.out.println("UDPSearcher start listen.");
CountDownLatch startDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTEN_PORT, startDownLatch, receiveLatch);
listener.start();
startDownLatch.await();
return listener;
}
private static void sendBroadcast() throws IOException {
System.out.println("UDPSearcher sendBroadcast started.");
// 作为搜索方,让系统自动分配端口
DatagramSocket ds = new DatagramSocket();
// 构建一份请求数据
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 头部
byteBuffer.put(UDPConstants.HEADER);
// CMD命名
byteBuffer.putShort((short) 1);
// 回送端口信息
byteBuffer.putInt(LISTEN_PORT);
// 直接构建packet
DatagramPacket requestPacket = new DatagramPacket(byteBuffer.array(),
byteBuffer.position() + 1);
// 广播地址
requestPacket.setAddress(InetAddress.getByName("255.255.255.255"));
// 设置服务器端口
requestPacket.setPort(UDPConstants.PORT_SERVER);
// 发送
ds.send(requestPacket);
ds.close();
// 完成
System.out.println("UDPSearcher sendBroadcast finished.");
}
private static class Listener extends Thread {
private final int listenPort;
//有两个Latch一个用来标记监听线程是否已启动,另一个用来标记广播发送后是否收到了回送
private final CountDownLatch startDownLatch;
private final CountDownLatch receiveDownLatch;
private final List<ServerInfo> serverInfoList = new ArrayList<>();
private final byte[] buffer = new byte[128];
private final int minLen = UDPConstants.HEADER.length + 2 + 4;
private boolean done = false;
private DatagramSocket ds = null;
private Listener(int listenPort, CountDownLatch startDownLatch, CountDownLatch receiveDownLatch) {
super();
this.listenPort = listenPort;
this.startDownLatch = startDownLatch;
this.receiveDownLatch = receiveDownLatch;
}
@Override
public void run() {
super.run();
// run开始执行,说明监听线程已启动,通知已启动
startDownLatch.countDown();
try {
// 监听回送端口
ds = new DatagramSocket(listenPort);
// 构建接收实体
DatagramPacket receivePack = new DatagramPacket(buffer, buffer.length);
while (!done) {
// 接收
ds.receive(receivePack);
// 打印接收到的信息与发送者的信息
// 发送者的IP地址
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
byte[] data = receivePack.getData();
boolean isValid = dataLen >= minLen
&& ByteUtils.startsWith(data, UDPConstants.HEADER);
System.out.println("UDPSearcher receive form ip:" + ip
+ "\tport:" + port + "\tdataValid:" + isValid);
if (!isValid) {
// 无效继续
continue;
}
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, UDPConstants.HEADER.length, dataLen);
final short cmd = byteBuffer.getShort();
final int serverPort = byteBuffer.getInt();
if (cmd != 2 || serverPort <= 0) {
System.out.println("UDPSearcher receive cmd:" + cmd + "\tserverPort:" + serverPort);
continue;
}
String sn = new String(buffer, minLen, dataLen - minLen);
ServerInfo info = new ServerInfo(serverPort, ip, sn);
serverInfoList.add(info);
// 成功接收到一份
receiveDownLatch.countDown();
}
} catch (Exception ignored) {
} finally {
close();
}
System.out.println("UDPSearcher listener finished.");
}
private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}
List<ServerInfo> getServerAndClose() {
done = true;
close();
return serverInfoList;
}
}
}
TCPClient
发起连接,处理读写事件
伪异步
startWith 启动, readHandler 处理读
写的话,直接是send函数, client main里面接收键盘输入,然后send发送
改造:
读到直接调用父类的onNewmassageArrived打印即可
参数、构造函数
public class TCPClient {
private final Socket socket;
private final ReadHandler readHandler;
private final PrintStream printStream;
public TCPClient(Socket socket, ReadHandler readHandler) throws IOException {
this.socket = socket;
this.readHandler = readHandler;
this.printStream = new PrintStream(so cket.getOutputStream());
}
public void exit() {
readHandler.exit();
CloseUtils.close(printStream);
CloseUtils.close(socket);
}
启动函数 startWith;负责逻辑调用
public static TCPClient startWith(ServerInfo info) throws IOException {
Socket socket = new Socket();
// 超时时间
socket.setSoTimeout(3000);
// 连接本地,端口2000;超时时间3000ms
socket.connect(new InetSocketAddress(Inet4Address.getByName(info.getAddress()), info.getPort()), 3000);
System.out.println("已发起服务器连接,并进入后续流程~");
System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
try {
//连接建立之后,异步处理读
ReadHandler readHandler = new ReadHandler(socket.getInputStream());
readHandler.start();
//这里返回这个socket, 以便在client main里面接收键盘输入,发送到server
return new TCPClient(socket, readHandler);
} catch (Exception e) {
System.out.println("连接异常");
CloseUtils.close(socket);
}
return null;
}
异步读 readHandler
static class ReadHandler extends Thread {
private boolean done = false;
private final InputStream inputStream;
ReadHandler(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
super.run();
try {
// 得到输入流,用于接收数据
BufferedReader socketInput = new BufferedReader(new InputStreamReader(inputStream));
do {
String str;
try {
// 客户端拿到一条数据
str = socketInput.readLine();
} catch (SocketTimeoutException e) {
continue;
}
if (str == null) {
System.out.println("连接已关闭,无法读取数据!");
break;
}
// 打印到屏幕
System.out.println(str);
} while (!done);
} catch (Exception e) {
if (!done) {
System.out.println("连接异常断开:" + e.getMessage());
}
} finally {
// 连接关闭
CloseUtils.close(inputStream);
}
}
server
Server 主函数入口
public class Server {
public static void main(String[] args) throws IOException {
//设置上下文
IoContext.setup()
.ioProvider(new IoSelectorProvider())
.start();
TCPServer tcpServer = new TCPServer(TCPConstants.PORT_SERVER);
boolean isSucceed = tcpServer.start();
if (!isSucceed) {
System.out.println("Start TCP server failed!");
return;
}
UDPProvider.start(TCPConstants.PORT_SERVER);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String str;
do {
str = bufferedReader.readLine();
tcpServer.broadcast(str);
} while (!"00bye00".equalsIgnoreCase(str));
UDPProvider.stop();
tcpServer.stop();
IoContext.close();
}
TCPServer
构造
public class TCPServer implements ClientHandler.ClientHandlerCallback {
private final int port;
private ClientListener listener;
private List<ClientHandler> clientHandlerList = new ArrayList<>();
private final ExecutorService forwardingThreadPoolExecutor;
private Selector selector;
private ServerSocketChannel server;
public TCPServer(int port) {
this.port = port;
// 转发线程池
this.forwardingThreadPoolExecutor = Executors.newSingleThreadExecutor();
}
启动函数
public boolean start() {
try {
selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
// 设置为非阻塞
server.configureBlocking(false);
// 绑定本地端口
server.socket().bind(new InetSocketAddress(port));
// 注册客户端连接到达监听
server.register(selector, SelectionKey.OP_ACCEPT);
this.server = server;
System.out.println("服务器信息:" + server.getLocalAddress().toString());
// 启动客户端监听
ClientListener listener = this.listener = new ClientListener();
listener.start();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
监听
监听所有注册到当前selector上的accept事件。
1 用iterator拿到所有的事件
2 对每一个事件,判断 是否 isAcceptable, 如果是就绪的 则 new ClientHandler 去处理,这个ClientHandler 再去调用connector
private class ClientListener extends Thread {
private boolean done = false;
@Override
public void run() {
super.run();
Selector selector = TCPServer.this.selector;
System.out.println("服务器准备就绪~");
// 等待客户端连接
do {
// 得到客户端
try {
if (selector.select() == 0) {
if (done) {
break;
}
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
if (done) {
break;
}
SelectionKey key = iterator.next();
iterator.remove();
// 检查当前Key的状态是否是我们关注的
// 客户端到达状态
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 非阻塞状态拿到客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
try {
// 客户端构建异步线程
ClientHandler clientHandler = new ClientHandler(socketChannel, TCPServer.this);
// 添加同步处理
synchronized (TCPServer.this) {
clientHandlerList.add(clientHandler);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端连接异常:" + e.getMessage());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
} while (!done);
System.out.println("服务器已关闭!");
}
广播发送
public synchronized void broadcast(String str) {
for (ClientHandler clientHandler : clientHandlerList) {
clientHandler.send(str);
}
}
转发
@Override
public void onNewMessageArrived(final ClientHandler handler, final String msg) {
// 异步提交转发任务
forwardingThreadPoolExecutor.execute(() -> {
synchronized (TCPServer.this) {
for (ClientHandler clientHandler : clientHandlerList) {
if (clientHandler.equals(handler)) {
// 跳过自己
continue;
}
// 对其他客户端发送消息
clientHandler.send(msg);
}
}
});
ClientHandler
这里 onReceiveNewMessage .对应 父类的打印,和tcpServer的转发。
这里的比客户端实现,要多一个 对所有注册在改服务器的客户端一个转发,重写的
读写业务逻辑
Connector
Connector 其实就是拿到了一个具体的socketChannel 之后要进行下一步的操作;
Sender ,Receiver 是 发送接送的最高抽象;
具体发送接收要做的: 把键盘或者收到的String 分成packet,然后异步发送;反之同理‘
Sender 和 Receiver的 非阻塞有两个两面组成: 一是 使用了读写 两个selector,通过读写时间注册的方式(IoProvider); 二是任务底层具体的读写操作由线程池来做。
packet 缓存,取用,异步发送, 发送成功之后回送。
Connector 就是标记一个唯一的连接。 拿到socketChannel, 然后对channel进行读写操作;
public class Connector implements Closeable, SocketChannelAdapter.OnChannelStatusChangedListener {
private UUID key = UUID.randomUUID(); //唯一标志一个连接
private SocketChannel channel; //连接中得到的
private Sender sender;// 连接可以做的事
private Receiver receiver;
setup 启动函数:
/*
启动的流程:
拿到: SocketChannelAdapter,启动消息的读取
*/
public void setup(SocketChannel socketChannel) throws IOException {
this.channel = socketChannel;
IoContext context = IoContext.get();
//对channel操作的调试器??? sender和receiver 集合成一个抽象
//通过channel对应的 selector 判断是用sender 还是 receiver
SocketChannelAdapter adapter = new SocketChannelAdapter(channel, context.getIoProvider(), this);
this.sender = adapter;
this.receiver = adapter;
readNextMessage();
}
/**
* 开始读取数据
* 注意监听里面实现的是打印和不停的读
*/
private void readNextMessage() {
if (receiver != null) {
try {
//这里就是回调了, 传入的是 接口的 某个方法
receiver.receiveAsync(echoReceiveListener);
} catch (IOException e) {
System.out.println("开始接收数据异常:" + e.getMessage());
}
}
}
private IoArgs.IoArgsEventListener echoReceiveListener = new IoArgs.IoArgsEventListener() {
@Override
public void onStarted(IoArgs args) {
}
@Override
//写 buffer 完成之后
public void onCompleted(IoArgs args) {
// 打印
onReceiveNewMessage(args.bufferString());
// 读取下一条数据
readNextMessage();
}
};
protected void onReceiveNewMessage(String str) {
System.out.println(key.toString() + ":" + str);
}
IoContext
提供给io环境的类, 单例模式, 封装ioProvider的
这个类的 对象 拥有一个 ioProvider的 成员。
==这里有个问题? 为什么要设置成 单例模式呢? ==
每一个 channel 只对应一个 io selector??? 多个线程 都要操作 这个 selector
ublic class IoContext {
//单例
private static IoContext INSTANCE;
private final IoProvider ioProvider;
private IoContext(IoProvider ioProvider) {
this.ioProvider = ioProvider;
}
public IoProvider getIoProvider() {
return ioProvider;
}
public static IoContext get() {
return INSTANCE;
}
//内部类的对象
public static StartedBoot setup() {
return new StartedBoot();
}
//从这里可以看出来究竟在干嘛,
public static void close() throws IOException {
if (INSTANCE != null) {
INSTANCE.callClose();
}
}
// 关闭的是ioProvider, 所以 ioContext 其实就是在 提供一个provider 的封装。
private void callClose() throws IOException {
ioProvider.close();
}
内部类:
public static class StartedBoot {
private IoProvider ioProvider;
private StartedBoot() {
}
public StartedBoot ioProvider(IoProvider ioProvider) {
this.ioProvider = ioProvider;
return this;
}
public IoContext start() {
INSTANCE = new IoContext(ioProvider);
return INSTANCE;
}
}
IoProvider ⭐️
接口:
/**
* 观察者模式:
* 所有的连接都可以通过IoProvider 来注册 读写事件
* 观察SocketChannel的可读/可写状态,通过channel状态的变化来 改变 读写 channel的行为
* 若可读,则通过HandleInputCallback 和 HandleOutputCallback 进行回调
*/
public interface IoProvider extends Closeable {
boolean registerInput(SocketChannel channel, HandleInputCallback callback);
boolean registerOutput(SocketChannel channel, HandleOutputCallback callback);
void unRegisterInput(SocketChannel channel);
void unRegisterOutput(SocketChannel channel);
// 这两个回调 是 实现类中 map 的 value
//实现类中 每个 selectionKey, 都对应一个这个 回调 对象, 在 socketchannel 中完成 发送或者接收之后, 会回调,返回执行的结果。
abstract class HandleInputCallback implements Runnable {
@Override
public final void run() {
canProviderInput();
}
protected abstract void canProviderInput();
}
abstract class HandleOutputCallback implements Runnable {
private Object attach;
@Override
public final void run() {
canProviderOutput(attach);
}
public final void setAttach(Object attach) {
this.attach = attach;
}
protected abstract void canProviderOutput(Object attach);
}
}
实现类:
外面肯定不会出现直接使用这个类, 对外只能看见一个接口 IoProvider 对象, 但接口的实例其实是它的实现类的实例, 也就是 会调用这个 构造, 也就自然而然的开启了 读写的。。。
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 这个类 提供 向selector注册 读写事件, 并处理 读写事件的 操作
* 用法的话: 定义一个这个类的对象, 然后有两个selector成员, 里面有注册的方法, 和读写数据的方法
* 调用顺序 : startRead()-> handleSelection(这里是异步的,read放入了线程池)
*/
public class IoSelectorProvider implements IoProvider {
private final AtomicBoolean isClosed = new AtomicBoolean(false);
/**
* 555555555555555555
* readSelector的锁是否处于某个过程
*/
private final AtomicBoolean inRegInput = new AtomicBoolean(false);
private final AtomicBoolean inRegOutput = new AtomicBoolean(false);
private final Selector readSelector;
private final Selector writeSelector;
// 这里的runnable 对应的是 HandleInputCallback 和HandleOutputCallback
private final HashMap<SelectionKey, Runnable> inputCallbackMap = new HashMap<>();
private final HashMap<SelectionKey, Runnable> outputCallbackMap = new HashMap<>();
private final ExecutorService inputHandlePool;
private final ExecutorService outputHandlePool;
public IoSelectorProvider() throws IOException {
readSelector = Selector.open();
writeSelector = Selector.open();
inputHandlePool = Executors.newFixedThreadPool(4,
new IoProviderThreadFactory("IoProvider-Input-Thread-"));
outputHandlePool = Executors.newFixedThreadPool(4,
new IoProviderThreadFactory("IoProvider-Output-Thread-"));
// 开始输出输入的监听
//就是通过这里实现了读写监听的 启动, 只要初始化 IoProvider 的对象 , 那么它的实现类的构造函数就会自然而然的被调用。
startRead();
startWrite();
}
读写的非阻塞是通过 selector实现的, 也就是这个 其实就是对 读selector的操作
/**
* 111111111111111111
* 开始 读
*/
private void startRead() {
Thread thread = new Thread("Clink IoSelectorProvider ReadSelector Thread") {
@Override
public void run() {
while (!isClosed.get()) {
try {
// 这里有对readSelector的读操作
if (readSelector.select() == 0) {
waitSelection(inRegInput);
continue;
}
/**
为什么一定要有取消注册?
因为 这里 可以看到对channel的读是 用一个线程池来处理的,可以理解为立即就有结果,也就是数组可以很快遍历,没有阻塞
那么如果存在某个 channel 没有完成读操作, 那么下一次循环readSelector.select() == 0 仍然不是0, 也就是没完成的话
这个channel的读,会仍然注册在selector 上, 在这次的循环中,仍然会被加入 线程池。。
如果有的任务执行缓慢, 那么会在线程池大量堆积。。
这当然是要避免的。,所以对于,已经提交到线程池的,要及时取消注册。
*/
Set<SelectionKey> selectionKeys = readSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isValid()) {
//处理 当前 事件的 ,需要声明 当前 究竟是读事件,还是写事件,这很明显是 把可能融于的直接抽象成了一个
//将 执行结果的 返回 存到map中
// 异步 ,用的线程池
handleSelection(selectionKey, SelectionKey.OP_READ, inputCallbackMap, inputHandlePool);
}
}
selectionKeys.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
}
private void startWrite() {
Thread thread = new Thread("Clink IoSelectorProvider WriteSelector Thread") {
@Override
public void run() {
while (!isClosed.get()) {
try {
if (writeSelector.select() == 0) {
//registerSelection有一个唤醒,所以这里要等待上一次的注册监听完成之后,才能继续进行下一次
waitSelection(inRegOutput);
continue;
}
Set<SelectionKey> selectionKeys = writeSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isValid()) {
handleSelection(selectionKey, SelectionKey.OP_WRITE, outputCallbackMap, outputHandlePool);
}
}
selectionKeys.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
}
/**
* 22222222222222222222222222
* 如何 处理 所有的 注册到 selector上的事件
*/
private static void handleSelection(SelectionKey key, int keyOps,
HashMap<SelectionKey, Runnable> map,
ExecutorService pool) {
// 重点,!!!!!!!!!!!!!!!!!!!!!
// 为什么要先取消注册??? 看1的注释
// 取消继续对keyOps的监听
//selector只有注册操作, 看3,没有取消注册,所以这里要自己实现
//各种readyOps(),其实都是int值, 注册事件其实就是叠加ops的int值,要取消事件, 就把值去掉key.readyOps() & ~keyOps
key.interestOps(key.readyOps() & ~keyOps);
Runnable runnable = null;
//拿到当前 key 对应的runnable,然后加入线程池进行异步调度
try {
runnable = map.get(key);
} catch (Exception ignored) {
}
if (runnable != null && !pool.isShutdown()) {
// 异步调度
pool.execute(runnable);
}
}
因为select是不断循环执行的, 若 select 返回0, 有两种可能:
- 一是当前确实没有就绪的事件,那么此时就继续从头扫到尾;
- 还有一种可能就是,select没扫描完,被wakeup了(此时就是注册那里的代码了),这种情况的标志就是locker = true;这里 处理的就是 select扫描了0个就绪的,就被注册那里 wakeup了, 然后就没有了锁。 所以这里select返回0 之后, 判断锁的状态, 如果是 没有锁, 那就wait等待,直到在registerSelection那里 notify, 也就是修改完了, 再继续select。
这部分代码就是处理的这个情况;(也可以理解什么是wakeup了, 就是从不停的扫描的状态中停止, 去注册啥的)
那么还有一种很显然的情况没有处理, 就是 select 不等于0, 但被wakeup了, 此时正确的做法是, select停止等待, 等到notify了, 再把上一次扫描到的两个消费掉, 再继续新一次的扫描。
private static void waitSelection(final AtomicBoolean locker) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (locker) {
if (locker.get()) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
重写接口中的 注册 取消注册等方法
/**
* 3333333333333333333
* 在 SocketChannelAdapter 里面调用
*/
@Override
public boolean registerInput(SocketChannel channel, HandleInputCallback callback) {
// 注册了 channel, 则 当前 channel只要可读,就要进行回调。
/*
这里必须有取消注册的过程, 原因可以看1 的注释
*/
return registerSelection(channel, readSelector, SelectionKey.OP_READ, inRegInput,
inputCallbackMap, callback) != null;
}
@Override
public boolean registerOutput(SocketChannel channel, HandleOutputCallback callback) {
return registerSelection(channel, writeSelector, SelectionKey.OP_WRITE, inRegOutput,
outputCallbackMap, callback) != null;
}
这个一定得记住, 背下来 ⭐️ ⭐️ ⭐️
/**
* 444444444444444444444
* 1 有对readSelector 的读操作,
* 这里也有,这里的是 添加内容, 相当于写, 所以要加锁, 锁住readSelector,或者锁住操作readSelector的代码
* 用原子布尔值当锁, 见5
*
*/
private static SelectionKey registerSelection(SocketChannel channel, Selector selector,
int registerOps, AtomicBoolean locker,
HashMap<SelectionKey, Runnable> map,
Runnable runnable) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (locker) {
// 设置锁定状态
locker.set(true);
try {
// 唤醒当前的selector,让selector不处于select()状态
//先唤醒,避免注册无效, 可以说是二次select?????不懂
selector.wakeup();
SelectionKey key = null;
if (channel.isRegistered()) {
// 查询是否已经注册过
key = channel.keyFor(selector);
if (key != null) {
key.interestOps(key.readyOps() | registerOps);
}
}
if (key == null) {
// 注册selector得到Key
key = channel.register(selector, registerOps);
// 注册回调
map.put(key, runnable);
}
return key;
} catch (ClosedChannelException e) {
return null;
} finally {
// 解除锁定状态
locker.set(false);
try {
// 通知
locker.notify();
} catch (Exception ignored) {
}
}
}
}
取消注册:
/**
* 77777777777777
* @param channel
*/
@Override
public void unRegisterInput(SocketChannel channel) {
unRegisterSelection(channel, readSelector, inputCallbackMap);
}
@Override
public void unRegisterOutput(SocketChannel channel) {
unRegisterSelection(channel, writeSelector, outputCallbackMap);
}
/**
* 66666666666666666666666
* @param channel
* @param selector
* @param map
*/
private static void unRegisterSelection(SocketChannel channel, Selector selector,
Map<SelectionKey, Runnable> map) {
if (channel.isRegistered()) {
SelectionKey key = channel.keyFor(selector);
if (key != null) {
// 取消监听的方法
key.cancel();
map.remove(key);
selector.wakeup();
}
}
}
关闭各种资源:
/**
* 8888888888888888888
*/
@Override
public void close() {
if (isClosed.compareAndSet(false, true)) {
inputHandlePool.shutdown();
outputHandlePool.shutdown();
inputCallbackMap.clear();
outputCallbackMap.clear();
readSelector.wakeup();
writeSelector.wakeup();
CloseUtils.close(readSelector, writeSelector);
}
}
Ioargs
/**
* 封装ByteBuffer
* 实现 channel buffer 和真实要使用的数据的相互转换
*/
public class IoArgs {
private byte[] byteBuffer = new byte[256];
private ByteBuffer buffer = ByteBuffer.wrap(byteBuffer);
public int read(SocketChannel channel) throws IOException {
buffer.clear();
return channel.read(buffer);
}
public int write(SocketChannel channel) throws IOException {
return channel.write(buffer);
}
public String bufferString() {
// 丢弃换行符
return new String(byteBuffer, 0, buffer.position() - 1);
}
// 接口,用来监听 IoArgs的状态, 也就是比如 buffer 到 channel, 开始了吗,完成了吗?
//完成了,应该 回调,也就是这部分会在 调用这块 逻辑 的那部分代码那里使用
// 完成和开始 的回调
public interface IoArgsEventListener {
void onStarted(IoArgs args);
void onCompleted(IoArgs args);
}
}
Sender、Receiver、SocketChannelAdapter
/**
* 异步发送数据, 数据封装到IoArgs args, 发送状态通过IoArgsEventListener来回调
*/
public interface Sender extends Closeable {
boolean sendAsync(IoArgs args, IoArgs.IoArgsEventListener listener) throws IOException;
}
public interface Receiver extends Closeable {
boolean receiveAsync(IoArgs.IoArgsEventListener listener) throws IOException;
}
SocketChannelAdapter
import net.qiujuer.library.clink.core.IoArgs;
import net.qiujuer.library.clink.core.IoProvider;
import net.qiujuer.library.clink.core.Receiver;
import net.qiujuer.library.clink.core.Sender;
import net.qiujuer.library.clink.utils.CloseUtils;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 实现 Sender, Receiver 这两个端口, 实现具体的输入输出调用,也就是调用 ioProvider 的注册读写
*/
public class SocketChannelAdapter implements Sender, Receiver, Cloneable {
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private final SocketChannel channel;
private final IoProvider ioProvider;
//三个回调; channel关闭回调
private final OnChannelStatusChangedListener listener;
//有输入输出的回调用
private IoArgs.IoArgsEventListener receiveIoEventListener;
private IoArgs.IoArgsEventListener sendIoEventListener;
public SocketChannelAdapter(SocketChannel channel, IoProvider ioProvider,
OnChannelStatusChangedListener listener) throws IOException {
this.channel = channel;
this.ioProvider = ioProvider;
this.listener = listener;
channel.configureBlocking(false);
}
重写接口函数
@Override
public boolean receiveAsync(IoArgs.IoArgsEventListener listener) throws IOException {
if (isClosed.get()) {
throw new IOException("Current channel is closed!");
}
receiveIoEventListener = listener;
//ioProvider.registerInput 是注册读写事件的,真正的buffer操作 在 inputCallback中,它实现了runnable, 并被加入了线程池。 在 startRead()中, 线程池异步执行。
return ioProvider.registerInput(channel, inputCallback);
}
@Override
public boolean sendAsync(IoArgs args, IoArgs.IoArgsEventListener listener) throws IOException {
if (isClosed.get()) {
throw new IOException("Current channel is closed!");
}
sendIoEventListener = listener;
// 当前发送的数据附加到回调中
outputCallback.setAttach(args);
return ioProvider.registerOutput(channel, outputCallback);
}
HandlerInputCallback 实现了 runnable, 实现了对于 channel 到buffer的操作。对于每一个注册到selector的key ,都会有一个这样的线程, 然后用线程池来执行。
private final IoProvider.HandleInputCallback inputCallback = new IoProvider.HandleInputCallback() {
@Override
protected void canProviderInput() {
if (isClosed.get()) {
return;
}
IoArgs args = new IoArgs();
IoArgs.IoArgsEventListener listener = SocketChannelAdapter.this.receiveIoEventListener;
if (listener != null) {
listener.onStarted(args);
}
try {
// 具体的读取操作
if (args.read(channel) > 0 && listener != null) {
// 读取完成回调
listener.onCompleted(args);
} else {
throw new IOException("Cannot read any data!");
}
} catch (IOException ignored) {
CloseUtils.close(SocketChannelAdapter.this);
}
}
};
private final IoProvider.HandleOutputCallback outputCallback = new IoProvider.HandleOutputCallback() {
@Override
protected void canProviderOutput(Object attach) {
if (isClosed.get()) {
return;
}
// TODO
sendIoEventListener.onCompleted(null);
}
};
@Override
public void close() throws IOException {
if (isClosed.compareAndSet(false, true)) {
// 解除注册回调
ioProvider.unRegisterInput(channel);
ioProvider.unRegisterOutput(channel);
// 关闭
CloseUtils.close(channel);
// 回调当前Channel已关闭
listener.onChannelClosed(channel);
}
}
Packet 消息分片
Packet
对Packet本身而言,只有type 和 length, 也就是不涉及具体数据, 只有类型长度信息;
更细致的分类是 sendPacket 和receivePacket;
sendPacket 里面有 byte[] 来存放 具体的内容, 和一个标志位标记是否取消
receivePacket 只有save方法, 将包中的内容存下来。
以String类型为例,StringSendPacket 实现String转为要发送的包的类型; StringReceivePacket 收到的Packet转String;
SendDispatcher
之前的IoProvider ,SocketChannelAdapter 是注册和执行的调度, 这个是 包的缓存,接收,发送的调度。
注意这里本来的流程是 connector 里面的sender 和receiver 对象 调用实现类SocketChannelAdapter 里面的sendAsync 和receiveAsync方法, 然后去注册, 在handlerselection里面提交到线程池, 然后在HandleInputCallback中执行。。 接下来就是要把这个过程 换成packet。。
AsyncSendDispatcher
到这里都还和ioArgs 无关,后面就是如何转成IOArgs
position表示的是 当前的下标, total是packet总长度
IoArgs 是 对buffer操作的, 之前只有buffer 和channel, 现在要加一条,packet 和buffer的交互。
这里有个问题。。 Packet 是按照http2的那种吗? 没有看见划分啊,只看到了转成一整个包
修改IoArgs中 读写函数, 改为重载的 readFrom, writeTo方法, 参数分别为 byte数组,也就是Packet,和 channel, 有两个方面的对buffer的操作。
从channel读数据 写入到 buffer中,写之后,要flip才能是翻转成了读模式。
继续AsyncSendDispatcher
原来的Connector 拥有 socketChannelAdapter 作为sender 和 receiver, 现在要改造 发送接收的调度
start -> registerReceive -> receiver. receiveAsync(ioArgs).
回调逻辑
HandleInputCallback & HandleOutputCallback
- 在接口 IoProvider 定义的抽象类
abstract class HandleInputCallback implements Runnable {
@Override
public final void run() {
canProviderInput();
}
protected abstract void canProviderInput();
}
abstract class HandleOutputCallback implements Runnable {
private Object attach;
@Override
public final void run() {
canProviderOutput(attach);
}
public final void setAttach(Object attach) {
this.attach = attach;
}
protected abstract void canProviderOutput(Object attach);
}
- 在 实现类 IoSelectorProvider 的registerInput 方法中, 为 每个 selectionKey 都建立一个 回调对象。
public boolean registerInput(SocketChannel channel, HandleInputCallback callback) {
// 注册了 channel, 则 当前 channel只要可读,就要进行回调。
/*
这里必须有取消注册的过程, 原因可以看1 的注释
*/
return registerSelection(channel, readSelector, SelectionKey.OP_READ, inRegInput,
inputCallbackMap, callback) != null;
}
- 在 接口Sender /Receiver 的实现类 Socket ChannelAdapter 中具体实现业务逻辑, 也就是 发送接收中 实现了回调;
/**
* 回调到底是什么
*
*/
private final IoProvider.HandleInputCallback inputCallback = new IoProvider.HandleInputCallback() {
@Override
protected void canProviderInput() {
if (isClosed.get()) {
return;
}
IoArgs args = new IoArgs();
IoArgs.IoArgsEventListener listener = SocketChannelAdapter.this.receiveIoEventListener;
if (listener != null) {
listener.onStarted(args);
}
try {
// 具体的读取操作
if (args.read(channel) > 0 && listener != null) {
// 读取完成回调
listener.onCompleted(args);
} else {
throw new IOException("Cannot read any data!");
}
} catch (IOException ignored) {
CloseUtils.close(SocketChannelAdapter.this);
}
}
};
private final IoProvider.HandleOutputCallback outputCallback = new IoProvider.HandleOutputCallback() {
@Override
protected void canProviderOutput(Object attach) {
if (isClosed.get()) {
return;
}
// TODO
sendIoEventListener.onCompleted(null);
}
};
//channel关闭的时候的回调
public interface OnChannelStatusChangedListener {
void onChannelClosed(SocketChannel channel);
}
}
//channel关闭的时候的回调
定义在 socketChannelChanged类中的接口
public interface OnChannelStatusChangedListener {
void onChannelClosed(SocketChannel channel);
}
在socketChannelChanged中使用
IoArgs 操作buffer的回调
定义在IoArgs类
// 接口,用来监听 IoArgs的状态, 也就是比如 buffer 到 channel, 开始了吗,完成了吗?
//完成了,应该 回调,也就是这部分会在 调用这块 逻辑 的那部分代码那里使用
// 完成和开始 的回调
public interface IoArgsEventListener {
void onStarted(IoArgs args);
void onCompleted(IoArgs args);
}
实现在Connector
private IoArgs.IoArgsEventListener echoReceiveListener = new IoArgs.IoArgsEventListener() {
@Override
public void onStarted(IoArgs args) {
}
@Override
public void onCompleted(IoArgs args) {
// 打印
onReceiveNewMessage(args.bufferString());
// 读取下一条数据
readNextMessage();
}
};
使用在 SocketChannelSelector的 IoProvider.HandleInputCallback中
private final IoProvider.HandleInputCallback inputCallback = new IoProvider.HandleInputCallback() {
@Override
protected void canProviderInput() {
if (isClosed.get()) {
return;
}
IoArgs args = new IoArgs();
IoArgs.IoArgsEventListener listener = SocketChannelAdapter.this.receiveIoEventListener;
//
if (listener != null) {
listener.onStarted(args);
}
try {
// 具体的读取操作
if (args.read(channel) > 0 && listener != null) {
// 读取完成回调
listener.onCompleted(args);
} else {
throw new IOException("Cannot read any data!");
}
} catch (IOException ignored) {
CloseUtils.close(SocketChannelAdapter.this);
}
}
};
各部分多线程的具体使用
性能分析
Bug 以及困难收获
IoSelectorProvider selector、channel、SelectionKey 之间的并发锁死等待bug
debug模式下, 运行clientTest, 发现 服务端的线程 处于 monitor状态。
在这里插入图片描述
因为select是不断循环执行的, 若 select 返回0, 有两种可能:
- 一是当前确实没有就绪的事件,那么此时就继续从头扫到尾;
- 还有一种可能就是,select没扫描完,被wakeup了(此时就是注册那里的代码了),这种情况的标志就是locker = true;这里 处理的就是 select扫描了0个就绪的,就被注册那里 wakeup了, 然后就没有了锁。 所以这里select返回0 之后, 判断锁的状态, 如果是 没有锁, 那就wait等待,直到在registerSelection那里 notify, 也就是修改完了, 再继续select。
这部分代码就是处理的这个情况;(也可以理解什么是wakeup了, 就是从不停的扫描的状态中停止, 去注册啥的)
那么还有一种很显然的情况没有处理, 就是 select 不等于0, 但被wakeup了, 此时正确的做法是, select停止等待, 等到notify了, 再把上一次扫描到的两个消费掉, 再继续新一次的扫描。
改进:也就是抽取代码解耦
关闭的异常捕获
close() 自带wakeup