一、阻塞模式
服务端
public class server {
public static void main(String[] args) throws IOException {
//用 nio 来理解阻塞模式单线程
//ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
//1.创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定监听端口
ssc.bind(new InetSocketAddress(8080));
//3.连接集合
List<SocketChannel> channels = new ArrayList<>();
while (true){
//4.accept 建立与客户端的连接,SocketChannel用来与客户端之间通信
log.debug("connecting...");
SocketChannel sc = ssc.accept();//默认是阻塞方法,线程停止运行
log.debug("connected...",sc);
channels.add(sc);
for (SocketChannel channel : channels){
//5.接收客户端发送的数据
log.debug("before read...",channel);
channel.read(buffer);//读取通道存入Buffer中,read也是阻塞方法
// buffer.flip();
new PrintBuffer(buffer);
buffer.clear();
log.debug("after read...");
}
}
}
}
客户端
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("wating...");
}
}
单线程阻塞时无法很笨!!!
二、非阻塞模式
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//非阻塞模式
null表示返回的sc为空
四种事件类型
- accept 会在有连接请求时触发
- connect 是客户端,连接建立后触发
- read 可读事件
- write 可写事件
三、处理Selector
处理accept
public static void main(String[] args) throws IOException {
//1.创建selector,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();//1.创建服务器
ssc.configureBlocking(false);//非阻塞模式
//2.建立selector和channel的联系(注册)
//SelectionKey 就是将来事情发生后,通过它可以知道事件和哪个channel发生的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//key只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key{}",sscKey);
ssc.bind(new InetSocketAddress(8080));//2.绑定监听端口
while (true) {
//3.select 方法,没有事件发生,线程阻塞,有时间,线程才会恢复。
selector.select();
//4.处理事件,SelectionKey内部包含了所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
log.debug("Key:{}",key);//key是同一个,channel不同
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
log.debug("{}",sc);
}
}
}
同一个key但是不同的channel。
Java Iterator(迭代器)
不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。
迭代器 it 的两个基本操作是 next 、hasNext 和 remove。
- 调用 it.next() 会返回迭代器的下一个元素,并且更新迭代器的状态。
- 调用 it.hasNext() 用于检测集合中是否还有元素。
- 调用 it.remove() 将迭代器返回的元素删除。
四、处理read
while (true) {
//3.select 方法,没有事件发生,线程阻塞,有时间,线程才会恢复。
// 事件未处理时不会阻塞,发生后要不处理,要不取消(key.cancel())
selector.select();
//4.处理事件,SelectionKey内部包含了所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//SelectionKey上处理完不会自动删除处理完的key,因此要自己删除
iterator.remove();
log.debug("Key:{}", key);//key是同一个,channel不同
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
log.debug("{}", sc);
}else if (key.isReadable()){
SocketChannel channel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
channel.read(buffer);
new PrintBuffer(buffer);
}
}
}
会形成两个队列:
- selector
里面放的是不同种类的key - selectedKeys
里面放的是不同种类的事件
SelectionKey key = iterator.next();
//SelectionKey上处理完不会自动删除处理完的key,因此要自己删除
iterator.remove();
selectedKeys处理完事件不会自动删除事件,只会将事件标记为已处理,因此若不自己删除掉迭代器中的事件,则下次继续处理时会返回key是null因此报空指针异常。
因此需要remove方法将已处理过得key移除。
五、处理客户端断开
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer);//如果是正常断开,read返回-1
if (read == -1) {
key.cancel();
} else {
new PrintBuffer(buffer);
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();//因为客户端断开了,因此需要将key取消(从selector集合中删除key)
}
断开selector会处理一次读事件
- 如果是正常断开,read返回-1,在处理read时若返回-1则判断是正常断开,此时将key取消;
- 非正常断开在异常中删除key
六、处理黏包,半包
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
SelectionKey sckey = sc.register(selector, 0, buffer);
sckey.interestOps(SelectionKey.OP_READ);
log.debug("{}", sc);
log.debug("scKey{}", sckey);
} else if (key.isReadable()) {
try {
SocketChannel channel = (SocketChannel) key.channel();
//获取Buffer上的附件
ByteBuffer buffer = (ByteBuffer) key.attachment();//attachemnt:将一个ByteBuffer作为附件关联到key上
if ((int) channel.read(buffer) == -1) {//如果是正常断开,read返回-1
key.cancel();
} else {
spilt(buffer);
if (buffer.position() == buffer.limit()) {
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
key.attach(newBuffer);
}
}
} catch (IOException e) {
e.printStackTrace();
key.cancel();//因为客户端断开了,因此需要将key取消(从selector集合中删除key)
}
}
.register(selector, 0, buffer):第三个参数表示该channel的附件
思想:
- 将buffer作为SelectionKey的附件
- read时获取该附件
- 若该附件充满了buffer(buffer.position() == buffer.limit())表示可能还有没传完的数据
- 建一个新的扩大的newbuffer,将buffer的东西放入
七、处理可写事件
服务器端
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ);
//1.向客户端写入数据
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 5000000; i++) {
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
//2.返回值代表实际写入的字节数
int i = sc.write(buffer);
System.out.println(i);
//3.判断是否还有剩余内容
if (buffer.hasRemaining()) {
//4.令其关注可写事件
sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
//5.剩余未读完的挂到key上
sckey.attach(buffer);
}
} else if (key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
int i = sc.write(buffer);
System.out.println(i);
//6.清理操作
if (!buffer.hasRemaining()) {
key.attach(null);
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
客户端
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
//3.接收数据
int count = 0;
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count += sc.read(buffer);
System.out.println(count);
buffer.clear();
}
}
思想:面对大量数据时,服务器端在第一次写入之后应该令key关注写事件,然后创建有写事件的操作为再重复来写入这些事件。而不应该直接重复写入,占用channel资源。
八、多线程处理事件
@Slf4j
public class MultiThreadServer {
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector boss = Selector.open();
SelectionKey bossKey = ssc.register(boss, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
//1.创建固定数量的work并初始化
Worker worker = new Worker("worker-0");
worker.register();
while (true) {
boss.select();
Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
log.debug("connect....{}", sc.getRemoteAddress());
//2.关联selector
log.debug("before register....{}", sc.getRemoteAddress());
sc.register(worker.selector, SelectionKey.OP_READ, null);
log.debug("after register....{}", sc.getRemoteAddress());
}
}
}
}
static class Worker implements Runnable {
private Thread thread;
private Selector selector;
private String name;
private volatile boolean start = false;
public Worker(String name) {
this.name = name;
}
//初始化线程和selector
public void register() throws IOException {
if (!start) {
thread = new Thread(this, name);
thread.start();
selector = Selector.open();
start = true;
}
}
@Override
public void run() {
while (true) {
try {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
log.debug("read....{}", channel.getRemoteAddress());
channel.read(buffer);
buffer.flip();
new PrintBuffer(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
无法进入worker线程的read
原因:
是阻塞方法,会将selector阻塞使得
无法运行
解决方法(无法根本解决):
1.给select增加延迟
使得sc.register可以先运行
2.将两个靠近
因为是不同线程运行,靠近增大register先运行的概率
九、weakup优化多线程
private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();//在woker中创建线程队列
在woker的register中向队列中添加sc.register并将selector唤醒
//向队列添加了任务,但未立刻执行
queue.add(() -> {
try {
sc.register(selector, SelectionKey.OP_READ, null);//还是在boss中
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
selector.wakeup();//唤醒select
最后在run方法中执行sc.register,以确保执行顺序
selector.select();//worker-0阻塞
Runnable task = queue.poll();
if (task != null) {
task.run();//执行了sc.register(selector, SelectionKey.OP_READ, null);
}
重点:
selector.wakeup和sc.register都是在boss线程中,而selector.select在worker线程中。
但!selector.wakeup相当于给selector.select发了张票,什么时候都可以用,因此三者运行顺序可以改变。
十、多worker
//1.创建固定数量的worker并初始化
Worker workers[] = new Worker[Runtime.getRuntime().availableProcessors()];//自动拿到cpu核心数
for (int i = 0; i < workers.length; i++) {
workers[i] = new Worker("worker"+i);
}
AtomicInteger index = new AtomicInteger();