Netty学习笔记二

二. 网络编程

1. 阻塞模式

阻塞主要表现为:
  • 连接时阻塞
  • 读取数据时阻塞
缺点:
  • 阻塞单线程在没有连接时会阻塞等待连接的到达,连接到了以后,要进行读取数据,如果没有数据,还要阻塞等待数据的到达。
  • 如果在等待连接期间其他线程发来了数据,依然读取不到数据,只能等待新的连接到达后,再次遍历所有channel的时候才能读取。
  • 如果在一个客户端连接上以后,迟迟不发送数据,那么下一个客户端来的时候,想请求连接就只能阻塞的等待,等待第一个客户端发送数据到服务器端,并处理完成,才会和新的客户端建立连接。
期望
  • 没有连接时不阻塞等待,继续向下运行
  • 没有数据时不阻塞等待,继续向下运行
  • 于是产生了下面的非阻塞模式(本例为单线程)
代码示例

等待连接

建立第一个连接,等待读取数据

读取第一个连接传输过来的数据

读取数据结束后,再次发送一条数据,这时会阻塞等待,因为在等待新的连接到来,如果没有新连接,会被阻塞在accept()方法。

再来一个客户端进行连接,这次 第一个客户端第二次发送的数据才会被读取

Server代码

package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
@Slf4j(topic = "c.Demo")
public class BIOServerDemo {/*** 使用NIO来理解阻塞模式 单线程* **/public static void main(String[] args) throws IOException {// 创建一个Socket服务器ServerSocketChannel serverChannel = ServerSocketChannel.open();// 绑定端口 用来客户端请求的端口serverChannel.bind(new InetSocketAddress(8080));// 创建一个Buffer缓冲区用来存储客户端发来的数据(从Channel中读取的数据)ByteBuffer buffer = ByteBuffer.allocate(16);List<SocketChannel> channelList = new ArrayList<>();while (true) {// accept 建立与客户连接  SocketChannel用来和客户端之间通信传输数据log.debug("等待新的连接.....");SocketChannel channel = serverChannel.accept(); // 阻塞方法,线程停止运行channelList.add(channel);log.debug("接受连接---> {}", channel);for (SocketChannel socketChannel : channelList) {log.debug("等待接受数据......{}", socketChannel);// 从channel中读取数据写入到buffer中socketChannel.read(buffer); // 阻塞方法,线程停止运行log.debug("接收到的数据为下面.....{}", socketChannel);// 切换成读模式buffer.flip();// 打印读取到的数据debugRead(buffer);// 切换成写模式 等待下一次写入buffer.clear();}}}
}

Client代码

package com.sunyang.netty.study;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式
//        channel.write(StandardCharsets.UTF_8.encode("hello"));System.out.println("waiting....");}
}
15:03:01.743 [main] c.Demo - 等待新的连接.....
15:03:08.175 [main] c.Demo - 接受连接---> java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64644]
15:03:08.177 [main] c.Demo - 等待接受数据......
15:03:28.175 [main] c.Demo - 接收到的数据为下面
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
15:03:28.194 [main] c.Demo - 等待新的连接.....
15:03:28.194 [main] c.Demo - 接受连接---> java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64650]
15:03:28.195 [main] c.Demo - 等待接受数据......

2. 非阻塞模式

与阻塞单线程比较
  • 阻塞单线程在没有连接时会阻塞等待连接的到达,连接到了以后,要进行读取数据,如果没有数据,还要阻塞等待数据的到达。
  • 但是非阻塞不需要,没有连接时,也能马上返回结果null,然后继续向下运行,不会阻塞。读取数据时,没有数据到达会立即返回0,不会阻塞等待,然后继续向下运行。
缺点
  • 单线程非阻塞,CPU空转消耗CPU资源,

  • 虽然是非阻塞,但只不过是在没有连接到来时,或者没有数据读取时,他们不会阻塞在方法上等待连接或者等待数据的到达,而是会继续向下运行。什么时候有连接或者数据到达时可以马上进行处理(当然前提是当前没有其他要处理业务。)否则也会阻塞。

  • 但是同一时间还是只能干一件事,如果在处理一个客户端发送来的数据时,其他客户端依然要阻塞等待。

期望
  • 解决CPU空转

    • 让有连接事件发生时,或者有读取事件发生时,我们再去处理。
    • 这样就有了selector。就很好的解决了这个问题。
CPU空转
package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
@Slf4j(topic = "c.Demo")
public class NIOServerDemo {public static void main(String[] args) throws IOException {// 创建一个Socket服务器ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.configureBlocking(false); // 设置Socket服务器为非阻塞,这个控制的就是accept()方法为非阻塞// 绑定端口 用来客户端请求的端口serverChannel.bind(new InetSocketAddress(8080));// 创建一个Buffer缓冲区用来存储客户端发来的数据(从Channel中读取的数据)ByteBuffer buffer = ByteBuffer.allocate(16);List<SocketChannel> channelList = new ArrayList<>();while (true) {// accept 建立与客户连接  SocketChannel用来和客户端之间通信传输数据log.debug("等待新的连接.....");SocketChannel channel = serverChannel.accept(); // 变为非阻塞,没有连接时返回null,然后继续向下运行。log.debug("接受连接---> {}", channel);if (channel != null) {channel.configureBlocking(false); // 设置socketChanel为非阻塞,也就是socketChannel.read(buffer); 为非阻塞。channelList.add(channel);}for (SocketChannel socketChannel : channelList) {log.debug("等待接受数据......{}", socketChannel);// 从channel中读取数据写入到buffer中socketChannel.read(buffer); // 变为非阻塞,没有数据时返回0。继续向下运行log.debug("接收到的数据为下面.....{}", socketChannel);// 切换成读模式buffer.flip();// 打印读取到的数据debugRead(buffer);// 切换成写模式 等待下一次写入buffer.clear();}}}
}
15:54:32.948 [main] c.Demo - 等待新的连接.....
15:54:32.948 [main] c.Demo - 接受连接---> null
15:54:32.948 [main] c.Demo - 等待接受数据......java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
15:54:32.948 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [0]15:54:32.948 [main] c.Demo - 等待新的连接.....
15:54:32.948 [main] c.Demo - 接受连接---> null
15:54:32.948 [main] c.Demo - 等待接受数据......java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
15:54:32.948 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [0]15:54:32.948 [main] c.Demo - 等待新的连接.....
15:54:32.948 [main] c.Demo - 接受连接---> null
15:54:32.948 [main] c.Demo - 等待接受数据......java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
15:54:32.948 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [0]15:54:32.948 [main] c.Demo - 等待新的连接.....
15:54:32.948 [main] c.Demo - 接受连接---> null
15:54:32.948 [main] c.Demo - 等待接受数据......java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
15:54:32.948 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60681]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [0]

关掉程序后,CPU利用率直线下降

非阻塞Debug

Server代码

package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
@Slf4j(topic = "c.Demo")
public class NIOServerDemo {public static void main(String[] args) throws IOException {// 创建一个Socket服务器ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.configureBlocking(false); // 设置Socket服务器为非阻塞,这个控制的就是accept()方法为非阻塞// 绑定端口 用来客户端请求的端口serverChannel.bind(new InetSocketAddress(8080));// 创建一个Buffer缓冲区用来存储客户端发来的数据(从Channel中读取的数据)ByteBuffer buffer = ByteBuffer.allocate(16);List<SocketChannel> channelList = new ArrayList<>();while (true) {// accept 建立与客户连接  SocketChannel用来和客户端之间通信传输数据SocketChannel channel = serverChannel.accept(); // 变为非阻塞,没有连接时返回null,然后继续向下运行。if (channel != null) {log.debug("接受连接---> {}", channel);channel.configureBlocking(false); // 设置socketChanel为非阻塞,也就是socketChannel.read(buffer); 为非阻塞。channelList.add(channel);}for (SocketChannel socketChannel : channelList) {// 从channel中读取数据写入到buffer中int read = socketChannel.read(buffer); // 变为非阻塞,没有数据时返回0。继续向下运行if(read > 0) {log.debug("接收到的数据为下面.....{}", socketChannel);// 切换成读模式buffer.flip();// 打印读取到的数据debugRead(buffer);// 切换成写模式 等待下一次写入buffer.clear();log.debug("读取数据结束....{}", socketChannel);}}}}
}

Client代码

package com.sunyang.netty.study;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式
//        channel.write(StandardCharsets.UTF_8.encode("hello"));System.out.println("waiting....");}
}
16:04:17.832 [main] c.Demo - 接受连接---> java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57533]
16:04:31.430 [main] c.Demo - 接受连接---> java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57539]
16:04:33.599 [main] c.Demo - 接受连接---> java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61458]
16:05:40.907 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57533]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
16:05:40.926 [main] c.Demo - 读取数据结束....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57533]
16:06:07.297 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57539]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [3]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 73 75 6e                                        |sun             |
+--------+-------------------------------------------------+----------------+
16:06:07.298 [main] c.Demo - 读取数据结束....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:57539]
16:06:09.729 [main] c.Demo - 接收到的数据为下面.....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61458]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [4]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 61 6e 67                                     |yang            |
+--------+-------------------------------------------------+----------------+
16:06:09.729 [main] c.Demo - 读取数据结束....java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61458]

3. selector

好处

  • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
  • 让这个线程能够被充分利用
  • 节约了线程的数量
  • 减少了线程上下文切换

创建

Selector selector = Selector.open();

绑定 Channel 事件

也称之为注册事件,绑定的事件 selector 才会关心

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);
  • channel 必须工作在非阻塞模式
  • FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
  • 绑定的事件类型可以有
    • connect - 客户端连接成功时触发
    • accept - 服务器端成功接受连接时触发
    • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

select 何时不阻塞

  • 事件发生时

    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时
  • 调用 selector.wakeup()
  • 调用 selector.close()
  • selector 所在线程 interrupt

3.1 水平触发(条件触发)

如果有事件发生而不去处理,则会陷入非阻塞,一直死循环。

package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.*;
import java.util.Iterator;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
@Slf4j(topic = "c.Demo")
public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("register key: {}", sscSelectionKey);while(true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理selector.select();// 有事件发生,处理事件// 获取注册到selector中所有的key// 拿到所有可用事件集合Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 迭代所有的keywhile (iterator.hasNext()) {SelectionKey key = iterator.next();log.debug("key: {}", key);// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件//              SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channel
//                ServerSocketChannel channel = (ServerSocketChannel) selectableChannel;
//                // 然后接受创建连接。
//                SocketChannel socketChannel = channel.accept();
//                log.debug("建立连接的socketChannel---->{}", socketChannel);}}}
}
21:07:25.153 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.153 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.153 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.159 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.159 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.159 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
21:07:25.160 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@33e5ccce
.....

3.2 selectionkey

selector对象中存在两个存储key的集合,一个是用来存储注册到selector的key的集合。

  • HashSet<SelectionKey> keys = new HashSet();
    

一个是用来存储发生事件的key的集合

  • Set<SelectionKey> selectedKeys = new HashSet();
    

当事件发生时,会将发生事件的key从keys集合中拷贝一个到sekectedKeys中,然后处理完事件之后,会将事件删除,但是key还留在selectedKeys集合中,他只会自己往里加key,但不会自己删除key,需要手动去删除,否则下一次遍历,还会遍历到这个key但是却获取不到该key所管理的事件,所以会报空指针。

package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
@Slf4j(topic = "c.Demo")
public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("serverSocketChannel register accept key: {}", sscSelectionKey);while (true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理// select 在事件发生后,但未处理时,他不会阻塞// 所以事件发生后,要么处理,要么取消,不能置之不理selector.select();// 有事件发生,处理事件// 获取注册到selector中所有的keyIterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // accept, readlog.debug("selectedKeys 大小: {}", selector.selectedKeys().size());// 迭代所有的key// 拿到所有可用事件集合// 增强for循环不能再遍历时删除。while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理完key 要手动删除 开始删,结束删都可以,因为这个key已经拿到了。iterator.remove();log.debug("key: {}", key);// 判断key的类型if (key.isAcceptable()) { // accept事件// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channelServerSocketChannel channel = (ServerSocketChannel) selectableChannel;// 然后接受创建连接。SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("建立连接的socketChannel---->{}", socketChannel);// 注册到selector  并设置感兴趣的事件为read(可读事件)SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, null);log.debug("socketChannel register read key: {}", scSelectionKey);} else if (key.isReadable()) { // read事件ByteBuffer buffer = ByteBuffer.allocate(16); // 准备一个缓冲区,用于存放channel读取的数据// 获取 发生读事件的socketChannel,因为可能会有很多客户端(socketChannel)发生读事件,要确定是哪个SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;socketChannel.read(buffer);// 切换读模式buffer.flip();debugRead(buffer);}// 如果不想处理,可以取消,不然会陷入非阻塞,一直循环
//                key.cancel();}}}
}

3.4 处理客户端断开

 else if (key.isReadable()) { // read事件try {ByteBuffer buffer = ByteBuffer.allocate(16); SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;// 如果是正常断开,read的方法返回值是-1,如果有数据就继续处理// 非阻塞模式下没有数据为0,但是因为有selector,没有数据也不会触发读事件,所以页进不到这里int read = socketChannel.read(buffer);if (read == -1) {// 客户端正常关闭key.cancel();} else {// 切换读模式buffer.flip();debugRead(buffer);}} catch (IOException e) {// 如果不抛出异常,客户端断开连接后,服务器也会报错退出,// 因为客户端在断开连接后,会发出一个读事件,又因为读事件内容为空,所以会报错,// 但是捕获后异常后我们还要对产生的这个key的读事件做一个处理,如果不处理,就会陷入非阻塞状态,死循环。// 因为抛出异常后,等于没对这个读事件做处理,所以会再次产生一个读事件,即使之前已经删除了e.printStackTrace();// 正确处理完可以直接删除,如果没有处理,就删除,他还会在产生一个新的事件// 所以,需要将key取消,()// 客户端异常关闭key.cancel();}}

3.5 消息边界问题

拆包,半包,粘包

消息边界产生原因

代码示例产生原因
package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;import static com.sunyang.netty.study.ByteBufferUtil.debugAll;
import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
@Slf4j(topic = "c.Demo")
public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("serverSocketChannel register accept key: {}", sscSelectionKey);while (true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理// select 在事件发生后,但未处理时,他不会阻塞// 所以事件发生后,要么处理,要么取消,不能置之不理
//            log.debug("select 前 keys大小:{}", selector.keys().size());
//            log.debug("select 前 selectedKeys 大小: {}", selector.selectedKeys().size());selector.select();
//            log.debug("select 后 keys大小:{}", selector.keys().size());
//            log.debug("select 后 selectedKeys 大小: {}", selector.selectedKeys().size());// 有事件发生,处理事件// 获取注册到selector中所有的keyIterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // accept, read
//            log.debug("遍历 keys大小:{}", selector.keys().size());
//            log.debug("遍历 selectedKeys 大小: {}", selector.selectedKeys().size());// 迭代所有的key// 拿到所有可用事件集合// 增强for循环不能再遍历时删除。while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理完key 要手动删除 开始删,结束删都可以,因为这个key已经拿到了。iterator.remove();log.debug("key: {}", key);// 判断key的类型if (key.isAcceptable()) { // accept事件// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channelServerSocketChannel channel = (ServerSocketChannel) selectableChannel;// 然后接受创建连接。SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("建立连接的socketChannel---->{}", socketChannel);// 注册到selector  并设置感兴趣的事件为read(可读事件)//SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, null);log.debug("socketChannel register read key: {}", scSelectionKey);
//                    log.debug("remove() 前 keys 大小: {}", selector.keys().size());
//                    log.debug("remove() 前 selectedKeys 大小: {}", selector.selectedKeys().size());
//                    iterator.remove();
//                    log.debug("remove() 后 keys 大小: {}", selector.keys().size());
//                    log.debug("remove() 后 selectedKeys 大小: {}", selector.selectedKeys().size());} else if (key.isReadable()) { // read事件try {ByteBuffer buffer = ByteBuffer.allocate(16); // 准备一个缓冲区,用于存放channel读取的数据// 获取 发生读事件的socketChannel,因为可能会有很多客户端(socketChannel)发生读事件,要确定是哪个SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;// 如果是正常断开,read的方法返回值是-1,如果有数据就继续处理// 非阻塞模式下没有数据为0,但是因为有selector,没有数据也不会触发读事件,所以页进不到这里int read = socketChannel.read(buffer);if (read == -1) {key.cancel();} else {// 切换读模式
//                            buffer.flip();
//                            debugRead(buffer);// 替换成之前写好的split(buffer);}} catch (IOException e) {// 如果不抛出异常,客户端断开连接后,服务器也会报错退出,// 因为客户端在断开连接后,会发出一个读事件,又因为读事件内容为空,所以会报错,// 但是捕获后异常后我们还要对产生的这个key的读事件做一个处理,如果不处理,就会陷入非阻塞状态,死循环。// 因为抛出异常后,等于没对这个读事件做处理,所以会再次产生一个读事件,即使之前已经删除了e.printStackTrace();// 正确处理完可以直接删除,如果没有处理,就删除,他还会在产生一个新的事件// 所以,需要将key取消,()
//                        log.debug("cancel() 前 keys 大小: {}", selector.keys().size());
//                        log.debug("cancel() 前 selectedKeys 大小: {}", selector.selectedKeys().size());key.cancel();
//                        log.debug("cancel() 后 keys 大小: {}", selector.keys().size());
//                        log.debug("cancel() 后 selectedKeys 大小: {}", selector.selectedKeys().size());}}// 如果不想处理,可以取消,不然会陷入非阻塞,一直循环
//                key.cancel();}}}private static void split(ByteBuffer source) {// 切换成读模式source.flip();for (int i = 0; i < source.limit(); i++) {// 找到一条完整消息 以后会有更高效的方法,这里要一个字节一个字节去遍历一条消息的结束。浪费时间和资源if (source.get(i) == '\n') {int length = i + 1 - source.position();// 把这条完整消息存入新的ByteBufferByteBuffer target = ByteBuffer.allocate(length);// 从source读,向target写for (int j = 0; j < length; j++) {target.put(source.get());}debugAll(target);}}// 切换成写模式 但是不能用clear 因为clear会从头写,那么未读取完的部分就会被丢弃,所以得用compacct()source.compact();}
}
package com.sunyang.netty.study;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式channel.write(StandardCharsets.UTF_8.encode("0123456789123456hello\nworld\n"));System.in.read();
//        channel.close();
//        System.out.println("waiting....");}
}
14:36:02.117 [main] c.Demo - serverSocketChannel register accept key: sun.nio.ch.SelectionKeyImpl@2357d90a
14:38:25.604 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2357d90a
14:38:25.604 [main] c.Demo - 建立连接的socketChannel---->java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:61890]
14:38:25.605 [main] c.Demo - socketChannel register read key: sun.nio.ch.SelectionKeyImpl@2145433b
14:38:25.605 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2145433b
14:38:25.605 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2145433b
+--------+-------------------- all ------------------------+----------------+
position: [6], limit: [6]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 0a                               |hello.          |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [6], limit: [6]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 77 6f 72 6c 64 0a                               |world.          |
+--------+-------------------------------------------------+----------------+
解决办法

  • 一种是客户端和服务器协商一个固定的消息长度,数据包大小一样,这样,服务器就可以按预定长度读取,但是如果消息很短,用不了这些,那么他还需要补齐,所以缺点就是浪费带宽
  • 另一种思路就是按分隔符拆分,但是这种方式就要遍历每一个字节,来找到分隔符,缺点是效率很低。
  • TLV和LTV格式,T :Type类型,L:Length长度,V:Value数据, 类型和长度已知的情况下,就可以方便获取信息大小,分配合适的buffer,缺点是buffer需要提前分配,如果内容过大,则影响server吞吐量。
扩容代码
  • #mermaid-svg-FH0jDWNfCwTBHDJZ .label{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);fill:#333;color:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .label text{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .node rect,#mermaid-svg-FH0jDWNfCwTBHDJZ .node circle,#mermaid-svg-FH0jDWNfCwTBHDJZ .node ellipse,#mermaid-svg-FH0jDWNfCwTBHDJZ .node polygon,#mermaid-svg-FH0jDWNfCwTBHDJZ .node path{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-FH0jDWNfCwTBHDJZ .node .label{text-align:center;fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .node.clickable{cursor:pointer}#mermaid-svg-FH0jDWNfCwTBHDJZ .arrowheadPath{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-svg-FH0jDWNfCwTBHDJZ .flowchart-link{stroke:#333;fill:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .edgeLabel{background-color:#e8e8e8;text-align:center}#mermaid-svg-FH0jDWNfCwTBHDJZ .edgeLabel rect{opacity:0.9}#mermaid-svg-FH0jDWNfCwTBHDJZ .edgeLabel span{color:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .cluster rect{fill:#ffffde;stroke:#aa3;stroke-width:1px}#mermaid-svg-FH0jDWNfCwTBHDJZ .cluster text{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-svg-FH0jDWNfCwTBHDJZ .actor{stroke:#ccf;fill:#ECECFF}#mermaid-svg-FH0jDWNfCwTBHDJZ text.actor>tspan{fill:#000;stroke:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .actor-line{stroke:grey}#mermaid-svg-FH0jDWNfCwTBHDJZ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .messageLine1{stroke-width:1.5;stroke-dasharray:2, 2;stroke:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ #arrowhead path{fill:#333;stroke:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .sequenceNumber{fill:#fff}#mermaid-svg-FH0jDWNfCwTBHDJZ #sequencenumber{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ #crosshead path{fill:#333;stroke:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .messageText{fill:#333;stroke:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .labelBox{stroke:#ccf;fill:#ECECFF}#mermaid-svg-FH0jDWNfCwTBHDJZ .labelText,#mermaid-svg-FH0jDWNfCwTBHDJZ .labelText>tspan{fill:#000;stroke:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .loopText,#mermaid-svg-FH0jDWNfCwTBHDJZ .loopText>tspan{fill:#000;stroke:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .loopLine{stroke-width:2px;stroke-dasharray:2, 2;stroke:#ccf;fill:#ccf}#mermaid-svg-FH0jDWNfCwTBHDJZ .note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-FH0jDWNfCwTBHDJZ .noteText,#mermaid-svg-FH0jDWNfCwTBHDJZ .noteText>tspan{fill:#000;stroke:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .activation0{fill:#f4f4f4;stroke:#666}#mermaid-svg-FH0jDWNfCwTBHDJZ .activation1{fill:#f4f4f4;stroke:#666}#mermaid-svg-FH0jDWNfCwTBHDJZ .activation2{fill:#f4f4f4;stroke:#666}#mermaid-svg-FH0jDWNfCwTBHDJZ .mermaid-main-font{font-family:"trebuchet ms", verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .section{stroke:none;opacity:0.2}#mermaid-svg-FH0jDWNfCwTBHDJZ .section0{fill:rgba(102,102,255,0.49)}#mermaid-svg-FH0jDWNfCwTBHDJZ .section2{fill:#fff400}#mermaid-svg-FH0jDWNfCwTBHDJZ .section1,#mermaid-svg-FH0jDWNfCwTBHDJZ .section3{fill:#fff;opacity:0.2}#mermaid-svg-FH0jDWNfCwTBHDJZ .sectionTitle0{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .sectionTitle1{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .sectionTitle2{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .sectionTitle3{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .sectionTitle{text-anchor:start;font-size:11px;text-height:14px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .grid .tick{stroke:#d3d3d3;opacity:0.8;shape-rendering:crispEdges}#mermaid-svg-FH0jDWNfCwTBHDJZ .grid .tick text{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .grid path{stroke-width:0}#mermaid-svg-FH0jDWNfCwTBHDJZ .today{fill:none;stroke:red;stroke-width:2px}#mermaid-svg-FH0jDWNfCwTBHDJZ .task{stroke-width:2}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText{text-anchor:middle;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText:not([font-size]){font-size:11px}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-svg-FH0jDWNfCwTBHDJZ .task.clickable{cursor:pointer}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163 !important;font-weight:bold}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText0,#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText1,#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText2,#mermaid-svg-FH0jDWNfCwTBHDJZ .taskText3{fill:#fff}#mermaid-svg-FH0jDWNfCwTBHDJZ .task0,#mermaid-svg-FH0jDWNfCwTBHDJZ .task1,#mermaid-svg-FH0jDWNfCwTBHDJZ .task2,#mermaid-svg-FH0jDWNfCwTBHDJZ .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutside0,#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutside2{fill:#000}#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutside1,#mermaid-svg-FH0jDWNfCwTBHDJZ .taskTextOutside3{fill:#000}#mermaid-svg-FH0jDWNfCwTBHDJZ .active0,#mermaid-svg-FH0jDWNfCwTBHDJZ .active1,#mermaid-svg-FH0jDWNfCwTBHDJZ .active2,#mermaid-svg-FH0jDWNfCwTBHDJZ .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-svg-FH0jDWNfCwTBHDJZ .activeText0,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeText1,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeText2,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeText3{fill:#000 !important}#mermaid-svg-FH0jDWNfCwTBHDJZ .done0,#mermaid-svg-FH0jDWNfCwTBHDJZ .done1,#mermaid-svg-FH0jDWNfCwTBHDJZ .done2,#mermaid-svg-FH0jDWNfCwTBHDJZ .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-svg-FH0jDWNfCwTBHDJZ .doneText0,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneText1,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneText2,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneText3{fill:#000 !important}#mermaid-svg-FH0jDWNfCwTBHDJZ .crit0,#mermaid-svg-FH0jDWNfCwTBHDJZ .crit1,#mermaid-svg-FH0jDWNfCwTBHDJZ .crit2,#mermaid-svg-FH0jDWNfCwTBHDJZ .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCrit0,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCrit1,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCrit2,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCrit0,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCrit1,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCrit2,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-svg-FH0jDWNfCwTBHDJZ .milestone{transform:rotate(45deg) scale(0.8, 0.8)}#mermaid-svg-FH0jDWNfCwTBHDJZ .milestoneText{font-style:italic}#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCritText0,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCritText1,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCritText2,#mermaid-svg-FH0jDWNfCwTBHDJZ .doneCritText3{fill:#000 !important}#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCritText0,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCritText1,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCritText2,#mermaid-svg-FH0jDWNfCwTBHDJZ .activeCritText3{fill:#000 !important}#mermaid-svg-FH0jDWNfCwTBHDJZ .titleText{text-anchor:middle;font-size:18px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ g.classGroup text{fill:#9370db;stroke:none;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family);font-size:10px}#mermaid-svg-FH0jDWNfCwTBHDJZ g.classGroup text .title{font-weight:bolder}#mermaid-svg-FH0jDWNfCwTBHDJZ g.clickable{cursor:pointer}#mermaid-svg-FH0jDWNfCwTBHDJZ g.classGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-FH0jDWNfCwTBHDJZ g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5}#mermaid-svg-FH0jDWNfCwTBHDJZ .classLabel .label{fill:#9370db;font-size:10px}#mermaid-svg-FH0jDWNfCwTBHDJZ .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .dashed-line{stroke-dasharray:3}#mermaid-svg-FH0jDWNfCwTBHDJZ #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #compositionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #aggregationStart{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #aggregationEnd{fill:#ECECFF;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #dependencyStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #dependencyEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ #extensionEnd{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ .commit-id,#mermaid-svg-FH0jDWNfCwTBHDJZ .commit-msg,#mermaid-svg-FH0jDWNfCwTBHDJZ .branch-label{fill:lightgrey;color:lightgrey;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .pieTitleText{text-anchor:middle;font-size:25px;fill:#000;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .slice{font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ g.stateGroup text{fill:#9370db;stroke:none;font-size:10px;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ g.stateGroup text{fill:#9370db;fill:#333;stroke:none;font-size:10px}#mermaid-svg-FH0jDWNfCwTBHDJZ g.statediagram-cluster .cluster-label text{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ g.stateGroup .state-title{font-weight:bolder;fill:#000}#mermaid-svg-FH0jDWNfCwTBHDJZ g.stateGroup rect{fill:#ECECFF;stroke:#9370db}#mermaid-svg-FH0jDWNfCwTBHDJZ g.stateGroup line{stroke:#9370db;stroke-width:1}#mermaid-svg-FH0jDWNfCwTBHDJZ .transition{stroke:#9370db;stroke-width:1;fill:none}#mermaid-svg-FH0jDWNfCwTBHDJZ .stateGroup .composit{fill:white;border-bottom:1px}#mermaid-svg-FH0jDWNfCwTBHDJZ .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px}#mermaid-svg-FH0jDWNfCwTBHDJZ .state-note{stroke:#aa3;fill:#fff5ad}#mermaid-svg-FH0jDWNfCwTBHDJZ .state-note text{fill:black;stroke:none;font-size:10px}#mermaid-svg-FH0jDWNfCwTBHDJZ .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.7}#mermaid-svg-FH0jDWNfCwTBHDJZ .edgeLabel text{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .stateLabel text{fill:#000;font-size:10px;font-weight:bold;font-family:'trebuchet ms', verdana, arial;font-family:var(--mermaid-font-family)}#mermaid-svg-FH0jDWNfCwTBHDJZ .node circle.state-start{fill:black;stroke:black}#mermaid-svg-FH0jDWNfCwTBHDJZ .node circle.state-end{fill:black;stroke:white;stroke-width:1.5}#mermaid-svg-FH0jDWNfCwTBHDJZ #statediagram-barbEnd{fill:#9370db}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-cluster rect{fill:#ECECFF;stroke:#9370db;stroke-width:1px}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-cluster rect.outer{rx:5px;ry:5px}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-state .divider{stroke:#9370db}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-state .title-state{rx:5px;ry:5px}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-cluster.statediagram-cluster .inner{fill:white}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-cluster.statediagram-cluster-alt .inner{fill:#e0e0e0}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-cluster .inner{rx:0;ry:0}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-state rect.basic{rx:5px;ry:5px}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#efefef}#mermaid-svg-FH0jDWNfCwTBHDJZ .note-edge{stroke-dasharray:5}#mermaid-svg-FH0jDWNfCwTBHDJZ .statediagram-note rect{fill:#fff5ad;stroke:#aa3;stroke-width:1px;rx:0;ry:0}:root{--mermaid-font-family: '"trebuchet ms", verdana, arial';--mermaid-font-family: "Comic Sans MS", "Comic Sans", cursive}#mermaid-svg-FH0jDWNfCwTBHDJZ .error-icon{fill:#522}#mermaid-svg-FH0jDWNfCwTBHDJZ .error-text{fill:#522;stroke:#522}#mermaid-svg-FH0jDWNfCwTBHDJZ .edge-thickness-normal{stroke-width:2px}#mermaid-svg-FH0jDWNfCwTBHDJZ .edge-thickness-thick{stroke-width:3.5px}#mermaid-svg-FH0jDWNfCwTBHDJZ .edge-pattern-solid{stroke-dasharray:0}#mermaid-svg-FH0jDWNfCwTBHDJZ .edge-pattern-dashed{stroke-dasharray:3}#mermaid-svg-FH0jDWNfCwTBHDJZ .edge-pattern-dotted{stroke-dasharray:2}#mermaid-svg-FH0jDWNfCwTBHDJZ .marker{fill:#333}#mermaid-svg-FH0jDWNfCwTBHDJZ .marker.cross{stroke:#333}:root { --mermaid-font-family: "trebuchet ms", verdana, arial;} #mermaid-svg-FH0jDWNfCwTBHDJZ {color: rgba(0, 0, 0, 0.75);font: ;} 客户端1 服务器 ByteBuffer1 ByteBuffer2 发送 01234567890abcdef3333\r 第一次 read 存入 01234567890abcdef 扩容 拷贝 01234567890abcdef 第二次 read 存入 3333\r 01234567890abcdef3333\r 客户端1 服务器 ByteBuffer1 ByteBuffer2
  • package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;import static com.sunyang.netty.study.ByteBufferUtil.debugAll;
    import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
    @Slf4j(topic = "c.Demo")
    public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("serverSocketChannel register accept key: {}", sscSelectionKey);while (true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理// select 在事件发生后,但未处理时,他不会阻塞// 所以事件发生后,要么处理,要么取消,不能置之不理
    //            log.debug("select 前 keys大小:{}", selector.keys().size());
    //            log.debug("select 前 selectedKeys 大小: {}", selector.selectedKeys().size());selector.select();
    //            log.debug("select 后 keys大小:{}", selector.keys().size());
    //            log.debug("select 后 selectedKeys 大小: {}", selector.selectedKeys().size());// 有事件发生,处理事件// 获取注册到selector中所有的keyIterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // accept, read
    //            log.debug("遍历 keys大小:{}", selector.keys().size());
    //            log.debug("遍历 selectedKeys 大小: {}", selector.selectedKeys().size());// 迭代所有的key// 拿到所有可用事件集合// 增强for循环不能再遍历时删除。while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理完key 要手动删除 开始删,结束删都可以,因为这个key已经拿到了。iterator.remove();log.debug("key: {}", key);// 判断key的类型if (key.isAcceptable()) { // accept事件// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channelServerSocketChannel channel = (ServerSocketChannel) selectableChannel;// 然后接受创建连接。SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("建立连接的socketChannel---->{}", socketChannel);// 注册到selector  并设置感兴趣的事件为read(可读事件)// 在注册时添加附件 可以解决消息边界问题,但不完美,如果数据很大会一直扩容,数据少时不会缩减,不能动态调整,Netty可以// 附件 设置一个SocketChannel专属的Buffer,可以解决局部buffer变量再数据大时,第一次读取数据丢失的情况,然后,通过扩容拷贝的方式将数据组合起来。ByteBuffer buffer = ByteBuffer.allocate(16);SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, buffer);
    //                    SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, null); // 测试没有专属buffer时使用log.debug("socketChannel register read key: {}", scSelectionKey);
    //                    log.debug("remove() 前 keys 大小: {}", selector.keys().size());
    //                    log.debug("remove() 前 selectedKeys 大小: {}", selector.selectedKeys().size());
    //                    iterator.remove();
    //                    log.debug("remove() 后 keys 大小: {}", selector.keys().size());
    //                    log.debug("remove() 后 selectedKeys 大小: {}", selector.selectedKeys().size());} else if (key.isReadable()) { // read事件try {//                        ByteBuffer buffer = ByteBuffer.allocate(16); // 准备一个缓冲区,用于存放channel读取的数据 , 将局部变量转为附件,作为channel的专属bufferByteBuffer buffer = (ByteBuffer) key.attachment();// 获取 发生读事件的socketChannel,因为可能会有很多客户端(socketChannel)发生读事件,要确定是哪个SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;// 如果是正常断开,read的方法返回值是-1,如果有数据就继续处理// 非阻塞模式下没有数据为0,但是因为有selector,没有数据也不会触发读事件,所以页进不到这里int read = socketChannel.read(buffer);if (read == -1) {key.cancel();} else {// 切换读模式
    //                            buffer.flip();
    //                            debugRead(buffer);// 替换成之前写好的split(buffer);// 判断 如果position 和 limit 相等,说明没有找到\n 消息分隔符,这时说明一条消息没有结束,buffer不够大了,需要扩容。// 这时就需要对原buffer进行扩容拷贝if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);// 切换成读模式buffer.flip();newBuffer.put(buffer);// 将新buffer 与key 绑定。key.attach(newBuffer);}}} catch (IOException e) {// 如果不抛出异常,客户端断开连接后,服务器也会报错退出,// 因为客户端在断开连接后,会发出一个读事件,又因为读事件内容为空,所以会报错,// 但是捕获后异常后我们还要对产生的这个key的读事件做一个处理,如果不处理,就会陷入非阻塞状态,死循环。// 因为抛出异常后,等于没对这个读事件做处理,所以会再次产生一个读事件,即使之前已经删除了e.printStackTrace();// 正确处理完可以直接删除,如果没有处理,就删除,他还会在产生一个新的事件// 所以,需要将key取消,()
    //                        log.debug("cancel() 前 keys 大小: {}", selector.keys().size());
    //                        log.debug("cancel() 前 selectedKeys 大小: {}", selector.selectedKeys().size());key.cancel();
    //                        log.debug("cancel() 后 keys 大小: {}", selector.keys().size());
    //                        log.debug("cancel() 后 selectedKeys 大小: {}", selector.selectedKeys().size());}}// 如果不想处理,可以取消,不然会陷入非阻塞,一直循环
    //                key.cancel();}}}private static void split(ByteBuffer source) {// 切换成读模式source.flip();for (int i = 0; i < source.limit(); i++) {// 找到一条完整消息 以后会有更高效的方法,这里要一个字节一个字节去遍历一条消息的结束。浪费时间和资源if (source.get(i) == '\n') {int length = i + 1 - source.position();// 把这条完整消息存入新的ByteBufferByteBuffer target = ByteBuffer.allocate(length);// 从source读,向target写for (int j = 0; j < length; j++) {target.put(source.get());}debugAll(target);}}// 切换成写模式 但是不能用clear 因为clear会从头写,那么未读取完的部分就会被丢弃,所以得用compacct()source.compact();}
    }
    
  • package com.sunyang.netty.study;import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
    public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式channel.write(StandardCharsets.UTF_8.encode("0123456789123456hello\nworld\n"));System.in.read();
    //        channel.close();
    //        System.out.println("waiting....");}
    }
    
  • 15:01:42.590 [main] c.Demo - serverSocketChannel register accept key: sun.nio.ch.SelectionKeyImpl@2357d90a
    15:01:56.811 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2357d90a
    15:01:56.811 [main] c.Demo - 建立连接的socketChannel---->java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:64006]
    15:01:56.811 [main] c.Demo - socketChannel register read key: sun.nio.ch.SelectionKeyImpl@35083305
    15:01:56.813 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@35083305
    15:01:56.813 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@35083305
    +--------+-------------------- all ------------------------+----------------+
    position: [22], limit: [22]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 30 31 32 33 34 35 36 37 38 39 31 32 33 34 35 36 |0123456789123456|
    |00000010| 68 65 6c 6c 6f 0a                               |hello.          |
    +--------+-------------------------------------------------+----------------+
    +--------+-------------------- all ------------------------+----------------+
    position: [6], limit: [6]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 77 6f 72 6c 64 0a                               |world.          |
    +--------+-------------------------------------------------+----------------+
ButeBuffer 大小分配问题
  • 每个channel 都需要记录可能被切分的消息,因为ByteBuffer不能被多个channel共同使用,且是线程不安全的(这里的例子都是单线程,所以不存在线程不安全的问题,但是,存在不能被多个channel共同使用),因此需要为每个channel维护一个专属的独立的ByteBuffer 就可以用附件的形式来实现。
  • ByteBuffer不能太大,比如一个ByteBuffer 1MB的话,要支持百万连接就要1TB的内存,因此需要设计大小可变的ByteBuffer
    • 一种思路是首先分配一个较小的buffer,例如4K,如果发现容量不够,在进行相应的扩容,将4K的buffer内容拷贝到扩容后的buffer中,优点是消息连续容易处理,缺点是数据拷贝耗费性能。
    • 另一种思路就是用多个连续数组组成Buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别就是消息存储不连续,解析复杂,优点是避免了拷贝引起的性能损耗。

3.6 可写事件

  • 一次性写入

    • package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.Buffer;
      import java.nio.ByteBuffer;
      import java.nio.channels.SelectableChannel;
      import java.nio.channels.SelectionKey;
      import java.nio.channels.Selector;
      import java.nio.channels.ServerSocketChannel;
      import java.nio.channels.SocketChannel;
      import java.nio.charset.StandardCharsets;
      import java.util.Iterator;/*** @Author: sunyang* @Date: 2021/8/18* @Description:*/
      @Slf4j(topic = "c.Demo")
      public class SelectorWriteServer {public static void main(String[] args) throws IOException {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);SelectionKey scSelectionKey = socketChannel.register(selector, 0);StringBuilder builder = new StringBuilder();for (int i = 0; i < 30000000; i++) {builder.append("a");}ByteBuffer buffer = StandardCharsets.UTF_8.encode(builder.toString());// 有的网卡缓冲区比较小的情况会出现不能一次性将数据发送过去的现象,会分多次发送,// 所以为了性能,让缓冲区满了的时候去执行别的操作,等缓冲区有空间时再来执行。while (buffer.hasRemaining()) {int write = socketChannel.write(buffer);System.out.println(write);}// 第一次没读完的,等待缓冲区有空间再触发可写事件。再进行读取
      //                    if (buffer.hasRemaining()) {//                        // 注册可写事件
      //                        scSelectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
      //                        // 把未写完的buffer挂在key的附件上。
      //                        scSelectionKey.attach(buffer);
      //                    }
      //                } else if (key.isWritable()) {//                    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
      //                    SocketChannel socketChannel = (SocketChannel) key.channel();
      //                    int write = socketChannel.write(byteBuffer);
      //                    System.out.println(write);
      //                    // 清理操作
      //                    if (!byteBuffer.hasRemaining()) {//                        // 清除掉附件,让垃圾回收回收掉不用的Buffer
      //                        key.attach(null);
      //                        // 写完之后清除掉关注的可写事件
      //                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
      //                    }}}}}
      }
      
    • 4194272
      3014633
      0
      4063201
      0
      4718556
      2490349
      0
      0
      2621420
      0
      0
      2621420
      0
      0
      0
      0
      0
      0
      5242840
      0
      0
      0
      1033309
      
  • 分段写入,缓冲区满了则继续向下执行,等到缓冲区有空间时触发可写事件,再执行

    • package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.Buffer;
      import java.nio.ByteBuffer;
      import java.nio.channels.SelectableChannel;
      import java.nio.channels.SelectionKey;
      import java.nio.channels.Selector;
      import java.nio.channels.ServerSocketChannel;
      import java.nio.channels.SocketChannel;
      import java.nio.charset.StandardCharsets;
      import java.util.Iterator;/*** @Author: sunyang* @Date: 2021/8/18* @Description:*/
      @Slf4j(topic = "c.Demo")
      public class SelectorWriteServer {public static void main(String[] args) throws IOException {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);SelectionKey scSelectionKey = socketChannel.register(selector, 0);StringBuilder builder = new StringBuilder();for (int i = 0; i < 300000000; i++) {builder.append("a");}ByteBuffer buffer = StandardCharsets.UTF_8.encode(builder.toString());// 有的网卡缓冲区比较小的情况会出现不能一次性将数据发送过去的现象,会分多次发送,// 所以为了性能,让缓冲区满了的时候去执行别的操作,等缓冲区有空间时再来执行。
      //                    while (buffer.hasRemaining()) {int write = socketChannel.write(buffer);System.out.println(write);
      //                    }// 第一次没读完的,等待缓冲区有空间再触发可写事件。再进行读取if (buffer.hasRemaining()) {// 注册可写事件scSelectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);// 把未写完的buffer挂在key的附件上。scSelectionKey.attach(buffer);}} else if (key.isWritable()) {ByteBuffer byteBuffer = (ByteBuffer) key.attachment();SocketChannel socketChannel = (SocketChannel) key.channel();int write = socketChannel.write(byteBuffer);System.out.println(write);// 清理操作if (!byteBuffer.hasRemaining()) {// 清除掉附件,让垃圾回收回收掉不用的Bufferkey.attach(null);// 写完之后清除掉关注的可写事件key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);}}}}}
      }
      
    • 3932130
      4325343
      6946763
      7208905
      4718556
      3538917
      7995331
      11272106
      10616751
      10485680
      13238171
      15597449
      11534248
      12058532
      10485680
      7995331
      12320674
      10485680
      9830325
      18481011
      9830325
      7602118
      18743153
      5636053
      5111769
      5242840
      2490349
      5242840
      5111769
      2490349
      2621420
      2621420
      5242840
      5111769
      2490349
      5242840
      5111769
      2490349
      2621420
      2621420
      2621420
      633836

3.7 完整版代码

服务器端

package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;import static com.sunyang.netty.study.ByteBufferUtil.debugAll;
import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
@Slf4j(topic = "c.Demo")
public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("serverSocketChannel register accept key: {}", sscSelectionKey);while (true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理// select 在事件发生后,但未处理时,他不会阻塞// 所以事件发生后,要么处理,要么取消,不能置之不理
//            log.debug("select 前 keys大小:{}", selector.keys().size());
//            log.debug("select 前 selectedKeys 大小: {}", selector.selectedKeys().size());selector.select();
//            log.debug("select 后 keys大小:{}", selector.keys().size());
//            log.debug("select 后 selectedKeys 大小: {}", selector.selectedKeys().size());// 有事件发生,处理事件// 获取注册到selector中所有的keyIterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // accept, read
//            log.debug("遍历 keys大小:{}", selector.keys().size());
//            log.debug("遍历 selectedKeys 大小: {}", selector.selectedKeys().size());// 迭代所有的key// 拿到所有可用事件集合// 增强for循环不能再遍历时删除。while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理完key 要手动删除 开始删,结束删都可以,因为这个key已经拿到了。iterator.remove();log.debug("key: {}", key);// 判断key的类型if (key.isAcceptable()) { // accept事件// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channelServerSocketChannel channel = (ServerSocketChannel) selectableChannel;// 然后接受创建连接。SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("建立连接的socketChannel---->{}", socketChannel);// 注册到selector  并设置感兴趣的事件为read(可读事件)// 在注册时添加附件 可以解决消息边界问题,但不完美,如果数据很大会一直扩容,数据少时不会缩减,不能动态调整,Netty可以// 附件 设置一个SocketChannel专属的Buffer,可以解决局部buffer变量再数据大时,第一次读取数据丢失的情况,然后,通过扩容拷贝的方式将数据组合起来。ByteBuffer buffer = ByteBuffer.allocate(16);SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, buffer);
//                    SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, null); // 测试没有专属buffer时使用log.debug("socketChannel register read key: {}", scSelectionKey);
//                    log.debug("remove() 前 keys 大小: {}", selector.keys().size());
//                    log.debug("remove() 前 selectedKeys 大小: {}", selector.selectedKeys().size());
//                    iterator.remove();
//                    log.debug("remove() 后 keys 大小: {}", selector.keys().size());
//                    log.debug("remove() 后 selectedKeys 大小: {}", selector.selectedKeys().size());} else if (key.isReadable()) { // read事件try {//                        ByteBuffer buffer = ByteBuffer.allocate(16); // 准备一个缓冲区,用于存放channel读取的数据 , 将局部变量转为附件,作为channel的专属bufferByteBuffer buffer = (ByteBuffer) key.attachment();// 获取 发生读事件的socketChannel,因为可能会有很多客户端(socketChannel)发生读事件,要确定是哪个SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;// 如果是正常断开,read的方法返回值是-1,如果有数据就继续处理// 非阻塞模式下没有数据为0,但是因为有selector,没有数据也不会触发读事件,所以页进不到这里int read = socketChannel.read(buffer);if (read == -1) {key.cancel();} else {// 切换读模式
//                            buffer.flip();
//                            debugRead(buffer);// 替换成之前写好的split(buffer);// 判断 如果position 和 limit 相等,说明没有找到\n 消息分隔符,这时说明一条消息没有结束,buffer不够大了,需要扩容。// 这时就需要对原buffer进行扩容拷贝if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);// 切换成读模式buffer.flip();newBuffer.put(buffer);// 将新buffer 与key 绑定。key.attach(newBuffer);}}} catch (IOException e) {// 如果不抛出异常,客户端断开连接后,服务器也会报错退出,// 因为客户端在断开连接后,会发出一个读事件,又因为读事件内容为空,所以会报错,// 但是捕获后异常后我们还要对产生的这个key的读事件做一个处理,如果不处理,就会陷入非阻塞状态,死循环。// 因为抛出异常后,等于没对这个读事件做处理,所以会再次产生一个读事件,即使之前已经删除了e.printStackTrace();// 正确处理完可以直接删除,如果没有处理,就删除,他还会在产生一个新的事件// 所以,需要将key取消,()
//                        log.debug("cancel() 前 keys 大小: {}", selector.keys().size());
//                        log.debug("cancel() 前 selectedKeys 大小: {}", selector.selectedKeys().size());key.cancel();
//                        log.debug("cancel() 后 keys 大小: {}", selector.keys().size());
//                        log.debug("cancel() 后 selectedKeys 大小: {}", selector.selectedKeys().size());}}// 如果不想处理,可以取消,不然会陷入非阻塞,一直循环
//                key.cancel();}}}private static void split(ByteBuffer source) {// 切换成读模式source.flip();for (int i = 0; i < source.limit(); i++) {// 找到一条完整消息 以后会有更高效的方法,这里要一个字节一个字节去遍历一条消息的结束。浪费时间和资源if (source.get(i) == '\n') {int length = i + 1 - source.position();// 把这条完整消息存入新的ByteBufferByteBuffer target = ByteBuffer.allocate(length);// 从source读,向target写for (int j = 0; j < length; j++) {target.put(source.get());}debugAll(target);}}// 切换成写模式 但是不能用clear 因为clear会从头写,那么未读取完的部分就会被丢弃,所以得用compacct()source.compact();}
}

客户端

package com.sunyang.netty.study;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式channel.write(StandardCharsets.UTF_8.encode("0123456789123456hello\nworld\n"));
//        System.in.read();channel.close();
//        System.out.println("waiting....");}
}

3.8 个人问题总结

可读可写同时进行时,附件怎么设置。

并不是selector实现了非阻塞,而是selector为了让非阻塞变成更好的无事件阻塞有事件非阻塞

单线程版,在处理事件时还是串行处理,再处理一个事件的时候,其他事件阻塞

可读低水位 可写低水位

缓冲区

  • ByteBuffer缓冲区
  • 操作系统缓冲区-
    • 也就是网卡的缓冲区,如果是网络IO,向外面发送数据和接收数据,都要经过网卡缓冲区,然后复制到我们自己定义的Bytebuffer缓冲区,或者从Bytebuffer缓冲区复制到网卡缓冲区。

key.remove() 和 key.cancel() 的区别, (个人理解,不对请指正)

  • key.remove() 是将selectedKeys集合(也就是发生事件的key集合)中的key删除掉,但是keys集合(也就是注册到selector中的key集合)中的key还在,并没有被删除,

  • 因为他还得监听这个SocketChannel是否还有事件发生,remove()只是将这个key的事件从selectedKeys集合中删除了。

  • 但是cancel是将key从keys集合中删除了,也就是说这时,客户端已经断开了连接(或者发生了异常),这时就需要将此客户端注册到seletcor的keys集合中的key删除掉,代表不再监控这个SocketChannel,因为他已经断开了。但是这个删除是延迟删除的,并不是调用了这个方法就直接删除掉了,而是再次有事件发生时,调用了select()方法时,才会将其key从keys中删除,并且selectedKeys中的事件key也会被删除。

  • package com.sunyang.netty.study;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;import static com.sunyang.netty.study.ByteBufferUtil.debugRead;/*** @program: netty-study* @description: Selector非阻塞单线程服务端* @author: SunYang* @create: 2021-08-17 20:00**/
    @Slf4j(topic = "c.Demo")
    public class SelectorServer {@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws IOException {// 1. 创建服务端ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 2. 绑定监听端口serverSocketChannel.bind(new InetSocketAddress(8080));// 使用selector必须是非阻塞,否则报错。serverSocketChannel.configureBlocking(false);// 3. 定义selector  管理多个channelSelector selector = Selector.open();// 注册通道,返回键值, 建立selector与channel的联系(注册)// selectionKey 就是事件发生后,通过它可以知道是什么事件,和哪个channel发生的事件// 类似于管理员,这个selectionKey管理的是serverSocketChannel// 当需要读取数据时,还需要在注册一个selectionKey用来管理SocketChannel// 0 表示不关注任何事件SelectionKey sscSelectionKey = serverSocketChannel.register(selector, 0, null);// 注册感兴趣的事件 这里sscSelectionKey只关注accept事件 多个客户端连接所返回的key都为同一个key 都是这个key,因为是同一个serverSocketChannel的同一个事件accept所以key相同。// 四种事件类型// accept  会在有连接请求时触发// connect 客户端和服务端连接建立后触发的事件,客户端 channel.connect(new InetSocketAddress("localhost", 8080));// read 可读事件 表示有数据了,可读// write 可写事件sscSelectionKey.interestOps(SelectionKey.OP_ACCEPT);log.debug("serverSocketChannel register accept key: {}", sscSelectionKey);while (true) {// 无事件发生时 会在此阻塞,有事件发生时,会获取事件的key然后进行相应的处理// select 在事件发生后,但未处理时,他不会阻塞// 所以事件发生后,要么处理,要么取消,不能置之不理log.debug("keys大小:{}", selector.keys().size());log.debug("selectedKeys 大小: {}", selector.selectedKeys().size());selector.select();log.debug("keys大小:{}", selector.keys().size());log.debug("selectedKeys 大小: {}", selector.selectedKeys().size());// 有事件发生,处理事件// 获取注册到selector中所有的keyIterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // accept, readlog.debug("keys大小:{}", selector.keys().size());log.debug("selectedKeys 大小: {}", selector.selectedKeys().size());// 迭代所有的key// 拿到所有可用事件集合// 增强for循环不能再遍历时删除。while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理完key 要手动删除 开始删,结束删都可以,因为这个key已经拿到了。
    //                iterator.remove();log.debug("key: {}", key);// 判断key的类型if (key.isAcceptable()) { // accept事件// 根据key 获取到 发生事件的ServerSocketChannel,和发生的事件SelectableChannel selectableChannel = key.channel();// 拿到事件发生的channelServerSocketChannel channel = (ServerSocketChannel) selectableChannel;// 然后接受创建连接。SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("建立连接的socketChannel---->{}", socketChannel);// 注册到selector  并设置感兴趣的事件为read(可读事件)SelectionKey scSelectionKey = socketChannel.register(selector, SelectionKey.OP_READ, null);log.debug("socketChannel register read key: {}", scSelectionKey);iterator.remove();} else if (key.isReadable()) { // read事件try {ByteBuffer buffer = ByteBuffer.allocate(16); // 准备一个缓冲区,用于存放channel读取的数据// 获取 发生读事件的socketChannel,因为可能会有很多客户端(socketChannel)发生读事件,要确定是哪个SelectableChannel channel = key.channel();SocketChannel socketChannel = (SocketChannel) channel;socketChannel.read(buffer);// 切换读模式buffer.flip();debugRead(buffer);} catch (IOException e) {// 如果不抛出异常,客户端断开连接后,服务器也会报错退出,// 因为客户端在断开连接后,会发出一个读事件,又因为读事件内容为空,所以会报错,// 但是捕获后异常后我们还要对产生的这个key的读事件做一个处理,如果不处理,就会陷入非阻塞状态,死循环。// 因为抛出异常后,等于没对这个读事件做处理,所以会再次产生一个读事件,即使之前已经删除了e.printStackTrace();// 正确处理完可以直接删除,如果没有处理,就删除,他还会在产生一个新的事件// 所以,需要将key取消,()log.debug("selectedKeys 大小: {}", selector.selectedKeys().size());key.cancel();log.debug("keys大小:{}", selector.keys().size());log.debug("selectedKeys 大小: {}", selector.selectedKeys().size());}}// 如果不想处理,可以取消,不然会陷入非阻塞,一直循环
    //                key.cancel();}}}
    }
    
  • 客户端

    • package com.sunyang.netty.study;import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.channels.SocketChannel;
      import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
      public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式
      //        channel.write(StandardCharsets.UTF_8.encode("hello"));System.out.println("waiting....");}
      }
      
  • 10:25:47.968 [main] c.Demo - serverSocketChannel register accept key: sun.nio.ch.SelectionKeyImpl@2357d90a
    10:25:47.973 [main] c.Demo - select 前 keys大小:1
    10:25:47.973 [main] c.Demo - select 前 selectedKeys 大小: 0----------------------发生accept事件,一个客户端请求连接----------------------------------10:25:51.555 [main] c.Demo - select 后 keys大小:1
    10:25:51.555 [main] c.Demo - select 后 selectedKeys 大小: 1
    10:25:51.555 [main] c.Demo - 遍历 keys大小:1
    10:25:51.555 [main] c.Demo - 遍历 selectedKeys 大小: 1
    10:25:51.556 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2357d90a
    10:25:51.556 [main] c.Demo - 建立连接的socketChannel---->java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:58827]
    10:25:51.556 [main] c.Demo - socketChannel register read key: sun.nio.ch.SelectionKeyImpl@35083305
    10:25:51.556 [main] c.Demo - remove() 前 keys 大小: 2
    10:25:51.556 [main] c.Demo - remove() 前 selectedKeys 大小: 1
    10:25:51.556 [main] c.Demo - remove() 后 keys 大小: 2
    10:25:51.556 [main] c.Demo - remove() 后 selectedKeys 大小: 0  // 处理完remove-----------------------accept事件处理完成,等待新事件--------------------------------------
    10:25:51.556 [main] c.Demo - select 前 keys大小:2
    10:25:51.556 [main] c.Demo - select 前 selectedKeys 大小: 0-----------------------客户端一断开连接,会触发一个read事件---------------------------------
    10:25:54.752 [main] c.Demo - select 后 keys大小:2
    10:25:54.753 [main] c.Demo - select 后 selectedKeys 大小: 1
    10:25:54.753 [main] c.Demo - 遍历 keys大小:2
    10:25:54.753 [main] c.Demo - 遍历 selectedKeys 大小: 1
    10:25:54.753 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@35083305
    java.io.IOException: 远程主机强迫关闭了一个现有的连接。at sun.nio.ch.SocketDispatcher.read0(Native Method)at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)at sun.nio.ch.IOUtil.read(IOUtil.java:197)at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:378)at com.sunyang.netty.study.SelectorServer.main(SelectorServer.java:97)-----------------------出现异常,准备进入try块调用cancel() 方法----------------------------
    10:25:54.755 [main] c.Demo - cancel() 前 keys 大小: 2
    10:25:54.756 [main] c.Demo - cancel() 前 selectedKeys 大小: 1-----------------------canael()方法后,keys和selectedKeys没有变化-----------------------
    10:25:54.756 [main] c.Demo - cancel() 后 keys 大小: 2
    10:25:54.756 [main] c.Demo - cancel() 后 selectedKeys 大小: 1
    10:25:54.756 [main] c.Demo - select 前 keys大小:2
    10:25:54.756 [main] c.Demo - select 前 selectedKeys 大小: 1----- 第二个客户端触发accept事件,但是还没注册到selector,这时才删除keys中第一个客户端的key-------
    10:26:25.419 [main] c.Demo - select 后 keys大小:1 // 只剩下一个ServerSocketChannel的key
    10:26:25.419 [main] c.Demo - select 后 selectedKeys 大小: 1 // 这个时acept事件
    10:26:25.419 [main] c.Demo - 遍历 keys大小:1
    10:26:25.419 [main] c.Demo - 遍历 selectedKeys 大小: 1
    10:26:25.419 [main] c.Demo - key: sun.nio.ch.SelectionKeyImpl@2357d90a
    10:26:25.419 [main] c.Demo - 建立连接的socketChannel---->java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:50452]
    10:26:25.420 [main] c.Demo - socketChannel register read key: sun.nio.ch.SelectionKeyImpl@1b083826
    10:26:25.420 [main] c.Demo - remove() 前 keys 大小: 2
    10:26:25.420 [main] c.Demo - remove() 前 selectedKeys 大小: 1
    10:26:25.420 [main] c.Demo - remove() 后 keys 大小: 2
    10:26:25.420 [main] c.Demo - remove() 后 selectedKeys 大小: 0
    10:26:25.420 [main] c.Demo - select 前 keys大小:2
    10:26:25.420 [main] c.Demo - select 前 selectedKeys 大小: 0
    
  • 源码

    • selector.select();
      
    • protected int doSelect(long var1) throws IOException {if (this.channelArray == null) {throw new ClosedSelectorException();} else {this.timeout = var1;this.processDeregisterQueue(); // 删除keyif (this.interruptTriggered) {this.resetWakeupSocket();return 0;} else {this.adjustThreadsCount();this.finishLock.reset();this.startLock.startThreads();try {this.begin();try {this.subSelector.poll();} catch (IOException var7) {this.finishLock.setException(var7);}if (this.threads.size() > 0) {this.finishLock.waitForHelperThreads();}} finally {this.end();}this.finishLock.checkForException();this.processDeregisterQueue();int var3 = this.updateSelectedKeys();this.resetWakeupSocket();return var3;}}
      }
      
    • void processDeregisterQueue() throws IOException {Set var1 = this.cancelledKeys();synchronized(var1) {if (!var1.isEmpty()) {Iterator var3 = var1.iterator();while(var3.hasNext()) {SelectionKeyImpl var4 = (SelectionKeyImpl)var3.next();try {this.implDereg(var4);} catch (SocketException var11) {throw new IOException("Error deregistering key", var11);} finally {var3.remove();}}}}
      }
      

4. 多线程优化

思路:分两组选择器
  • 单线程配一个选择器,专门处理accept事件
  • 创建CPU核心数的线程,每个线程配一个选择器,轮流处理read事件

服务端代码
package com.sunyang.netty.study;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;import static com.sunyang.netty.study.ByteBufferUtil.debugAll;/*** @Author: sunyang* @Date: 2021/8/19* @Description: 多线程版selector服务器端*/
@Slf4j(topic = "c.Demo")
public class SelectorThreadsServer {public static void main(String[] args) throws IOException {AtomicInteger index = new AtomicInteger();Thread.currentThread().setName("Boss");ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8080));serverSocketChannel.configureBlocking(false);Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);log.debug("Boss-serverSocketChannel 注册, 等待连接.....");WorkerEventLoop[] workers = initEventLoops();while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);log.debug("connected....{}", socketChannel.getRemoteAddress());log.debug("before register ....{}", socketChannel.getRemoteAddress());// 负载均衡 轮询workers[index.getAndIncrement() % workers.length].register(socketChannel);log.debug("after register ....{}", socketChannel.getRemoteAddress());// 因为是 多线程, 所以这里要再一个新的线程中注册,而每个线程拥有一个自己selector// 所以创建一个内部类,用来创建线程和selector
//                    socketChannel.register(selector, SelectionKey.OP_READ);}}}}public static WorkerEventLoop[] initEventLoops() {// 如果是IO密集型可以利用阿姆达尔定律 如果是CPU密集型可以用内核数+1 防止缺页// Runtime.getRuntime().availableProcessors() 再Docker容器中可能不准确,因为他算是的你物理机的真实核心数,// 但如果你的Docker核心数只分配了一个,那么就会出问题。
//        WorkerEventLoop[] workerEventLoops = new WorkerEventLoop[Runtime.getRuntime().availableProcessors()];WorkerEventLoop[] workerEventLoops = new WorkerEventLoop[8];for (int i = 0; i < workerEventLoops.length; i++) {workerEventLoops[i] = new WorkerEventLoop("worker-" + i);}return workerEventLoops;}static class WorkerEventLoop implements Runnable {private String threadName;private Thread thread;private Selector selector;private volatile boolean start = false;// 线程安全的队列,用于两个线程间传递数据并且想让代码不是立刻执行,在某个位置执行,就可以用这个。作为两个线程间数据的通道。private ConcurrentLinkedQueue<Runnable> tasks = new ConcurrentLinkedQueue<>();public WorkerEventLoop(String threadName) {this.threadName = threadName;}public void register(SocketChannel socketChannel) throws IOException {if (!start) {selector = Selector.open();thread = new Thread(this,threadName);thread.start();start = true;}// 向任务队列中添加任务,但未执行,可以在想执行任务的地方调用任务的run方法tasks.add(() -> {try {socketChannel.register(selector, SelectionKey.OP_READ);} catch (ClosedChannelException e) {e.printStackTrace();}});// 唤醒 selector 类似于 park unparkselector.wakeup();}@Overridepublic void run() {while (true) {try {selector.select();Runnable task = tasks.poll();if (task != null) {task.run();}Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isReadable()) {SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(32);try {int read = socketChannel.read(buffer);if (read == -1) {key.cancel();socketChannel.close();} else {buffer.flip();log.debug("{} message:", socketChannel.getRemoteAddress());debugAll(buffer);}} catch (IOException e) {e.printStackTrace();key.cancel();socketChannel.close();}}}} catch (IOException e) {e.printStackTrace();}}}}}
客户端代码
package com.sunyang.netty.study;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;/*** @Author: sunyang* @Date: 2021/8/17* @Description:*/
public class BIOClientDemo {public static void main(String[] args) throws IOException {SocketChannel channel = SocketChannel.open();channel.connect(new InetSocketAddress("localhost", 8080));// 这一步用debug模式实现  来直观感受阻塞模式channel.write(StandardCharsets.UTF_8.encode("hello sunyang"));System.in.read();channel.close();
//        System.out.println("waiting....");}
}
输出
10:46:11.939 [Boss] c.Demo - Boss-serverSocketChannel 注册, 等待连接.....
10:46:20.669 [Boss] c.Demo - connected..../127.0.0.1:60942
10:46:20.672 [Boss] c.Demo - before register ..../127.0.0.1:60942
10:46:20.744 [Boss] c.Demo - after register ..../127.0.0.1:60942
10:46:20.745 [worker-0] c.Demo - /127.0.0.1:60942 message:
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [13]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 20 73 75 6e 79 61 6e 67 00 00 00 |hello sunyang...|
|00000010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
+--------+-------------------------------------------------+----------------+
10:46:25.948 [Boss] c.Demo - connected..../127.0.0.1:60950
10:46:25.948 [Boss] c.Demo - before register ..../127.0.0.1:60950
10:46:25.950 [Boss] c.Demo - after register ..../127.0.0.1:60950
10:46:25.951 [worker-1] c.Demo - /127.0.0.1:60950 message:
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [13]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 20 73 75 6e 79 61 6e 67 00 00 00 |hello sunyang...|
|00000010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
+--------+-------------------------------------------------+----------------+
10:46:30.846 [Boss] c.Demo - connected..../127.0.0.1:60958
10:46:30.846 [Boss] c.Demo - before register ..../127.0.0.1:60958
10:46:30.848 [Boss] c.Demo - after register ..../127.0.0.1:60958
10:46:30.849 [worker-2] c.Demo - /127.0.0.1:60958 message:
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [13]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 20 73 75 6e 79 61 6e 67 00 00 00 |hello sunyang...|
|00000010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
+--------+-------------------------------------------------+----------------+
10:46:49.702 [worker-0] c.Demo - /127.0.0.1:60942 message:
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69 20 73 75 6e 79 61 6e 67 00 00 00 00 00 00 |hi sunyang......|
|00000010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
+--------+-------------------------------------------------+----------------+
10:47:22.123 [worker-1] c.Demo - /127.0.0.1:60950 message:
+--------+------------- ------- all ------------------------+----------------+
position: [0], limit: [7]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69 20 79 61 6e 67 00 00 00 00 00 00 00 00 00 |hi yang.........|
|00000010| 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
+--------+-------------------------------------------------+----------------+

5. 网络编程小结

5.1 非阻塞 vs 阻塞 VS 多路复用

阻塞

  • 阻塞模式下,相关方法都会导致线程暂停

    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

非阻塞

  • 非阻塞模式下,相关方法都会不会让线程暂停

    • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
    • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
    • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
  • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu,CPU空转
  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

多路复用

selector可以配合单线程完成对多个Channel的可读可写等事件的监控,就称之为多路复用。

  • 多路复用仅针对网络IO,普通文件IO没法利用多路复用
  • 如果不用Selector的非阻塞模式,线程大部分时间都在做无用功,CPU一直在空转,而selector能够保证
    • 有可连接事件时才去连接
    • 有可读取事件时才去读取
    • 有可写事件才去写入
      • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

6. IO模型

6.1 Stream VS Channel

  • stream 不会自动缓冲数据(高层面API,不会关心系统提供的一些缓冲功能),channel会利用系统提供的发送缓冲区和接收缓冲区(更为底层)
  • stream仅支持阻塞API,channel同时支持阻塞API,非阻塞API,仅有网络channel可配合selector实现多路复用
  • 二者均为全双工,即读写可同时进行。

6.2 模型分类

阻塞IO,非阻塞IO,多路复用,信号驱动,异步IO

同步阻塞,同步非阻塞,多路复用,异步阻塞(好像没有这种情况,除非自己代码等待另一个线程返回结果),异步非阻塞

当调用一次channel.read或stream.read后会触发系统调用, 然后会切换至操作系统内核态由操作系统来完成真正的数据读取,而读取又分为两个阶段,分别为:

  • 等待数据阶段
  • 复制数据阶段

6.3 阻塞IO

用户线程处于阻塞状态。

6.4 非阻塞IO

发生了轮询,CPU会空转,且涉及到了多次用户态到内核态的切换,会浪费资源,只不过是他在等待数据的时候可以处理其他事情,而不会在此阻塞等待。

6.4.1 非阻塞和阻塞

6.5 多路复用

发生了两次系统调用。select 和 read

6.5.1 阻塞和多路复用

阻塞IO只涉及到一次系统调用,多路复用涉及到两次系统调用。

多路复用省去了等待数据和等待连接的阶段,只有有数据可读或者有连接可连接时,才会触发事件,然后,我就可以直接去读取数据或者建立连接,而不用等待数据和等待连接。

而且多路复用同时可以监控多个事件的发生,处理多个事件,但是阻塞一时间就只能干一件事,我要读取数据,但是数据没准备好,这时来了一个新的客户端请求连接,但是,我还是不能去处理另一个客户端的连接事件,我必须把客户端的数据读取完了,我才能去处理第二个客户端的连接。

或者,我在没有新连接到达的情况下,在等待新的连接,这时客户端一又发来了新的数据,但是我这时在等待新的连接到来,在没有连接到来之前,我就不能去读取数据。

这件事即使还没准备好数据,还干不了这件事,但是即使下一件事已经准别好了,我依然不能去干,我只能等待。但是多路复用就是,你只要准备好了,我就能去干。

6.5.2 非阻塞和多路复用

非阻塞IO在等待数据时是通过不断查询的方式去查看数据是否准备好,要频繁发生系统调用并且发生用户态到内核态的切换,还会导致CPU空转,但是多路复用是准备好了数据通知你,你再去读取数据,也就是有事件的时候非阻塞,无事件时阻塞等待事件到来。

6.6 异步IO

同步异步区别
  • 同步:线程自己去发起调用,然后再由自己去获取结果,不管是通过自己不断询问,还是通过事件触发。
  • 异步:线程自己去发起调用,然后由其他线程送来结果(至少两个线程)

6.8 个人问题总结

记录

读到内存之后 再从物理内存拷贝到 JVM堆内存缓冲区。

多路复用的优势在于连接数较多的情况下才有优势,因为如果连接数较少,那么阻塞模式更好,因为多路复用需要两次系统调用,而阻塞模式只需要一次。

epool的位置=select()

模型区别个人理解
  • 阻塞 等待数据和复制数据都是阻塞的,

  • 而非阻塞实现了等待数据是非阻塞的,复制数据依然是阻塞的,但是在等待数据的过程中因为非阻塞,但是他一直在轮询,CPU空转

  • 然后多路复用实现了等待数据是非阻塞的,因为,只有数据准备好了,才会触发读事件,但是这个过程中不会一直去轮询。复制数据依然是阻塞的。

  • 而AIO异步IO解决了等待数据和复制数据都是非阻塞的,因为异步IO只需要告诉内核,我要读取数据。然后数据准备好并复制好以后,由一个新的线程送来结果。

非阻塞和多路复用/信号驱动中的等待数据都是非阻塞的,只有复制数据是阻塞的,而复制数据的非阻塞是由AIO解决的。

7. 文件传输层零拷贝

非Netty,Netty和这个还有所不同。

注意:0拷贝不是说数据传输时没有拷贝的现象出现,而是指用户态和内核态没有拷贝操作,不会将数据拷贝到JVM内存中。只会在操作系统层面进行数据拷贝。

传统IO问题

传统的IO将一个文件通过Socket写出

File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");byte[] buf = new byte[(int)f.length()];
file.read(buf);Socket socket = ...;
socket.getOutputStream().write(buf);

内部流程图为

  1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu

    DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO

  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA

  3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝

  4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

  • 用户态与内核态的切换发生了 3 次,这个操作比较重量级
  • 数据拷贝了共 4 次

NIO优化

MMAP优化

通过DirectByteBuffer

  • ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存(JVM的堆内存)
  • ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存

大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将直接内存(也称堆外内存)映射到 jvm 内存中来直接访问使用

  • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
  • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
    • DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
    • 通过专门线程访问引用队列,根据虚引用释放堆外内存
  • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

transferTo/From优化

进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据,只有FileChannel有,SocketChannel和ServerSocketChannel没有。因为socketchannel是网络IO,不是操作系统层面控制的,所以就不存在这种情况。

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
  3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到

  • 只发生了一次用户态与内核态的切换
  • 数据拷贝了 3 次

进一步优化(linux 2.4)

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
  3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,减少 cpu 缓存伪共享
  • 零拷贝适合小文件传输
    • 文件如果比较大,就要将数据都读取到缓冲区。

进化史

8. AIO

先来看 AsynchronousFileChannel

@Slf4j
public class AioDemo1 {public static void main(String[] args) throws IOException {try{AsynchronousFileChannel s = AsynchronousFileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);ByteBuffer buffer = ByteBuffer.allocate(2);log.debug("begin...");s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer attachment) {log.debug("read completed...{}", result);buffer.flip();debug(buffer);}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {log.debug("read failed...");}});} catch (IOException e) {e.printStackTrace();}log.debug("do other things...");System.in.read();}
}
13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - begin...
13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - do other things...
13:44:56 [DEBUG] [Thread-5] c.i.aio.AioDemo1 - read completed...2+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 0d                                           |a.              |
+--------+-------------------------------------------------+----------------+

可以看到

  • 响应文件读取成功的是另一个线程 Thread-5
  • 主线程并没有 IO 操作阻塞

守护线程

默认文件 AIO 使用的线程都是守护线程,所以最后要执行 System.in.read() 以避免守护线程意外结束

网络 AIO

public class AioServer {public static void main(String[] args) throws IOException {AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();ssc.bind(new InetSocketAddress(8080));ssc.accept(null, new AcceptHandler(ssc));System.in.read();}private static void closeChannel(AsynchronousSocketChannel sc) {try {System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());sc.close();} catch (IOException e) {e.printStackTrace();}}private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {private final AsynchronousSocketChannel sc;public ReadHandler(AsynchronousSocketChannel sc) {this.sc = sc;}@Overridepublic void completed(Integer result, ByteBuffer attachment) {try {if (result == -1) {closeChannel(sc);return;}System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());attachment.flip();System.out.println(Charset.defaultCharset().decode(attachment));attachment.clear();// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件sc.read(attachment, attachment, this);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {closeChannel(sc);exc.printStackTrace();}}private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {private final AsynchronousSocketChannel sc;private WriteHandler(AsynchronousSocketChannel sc) {this.sc = sc;}@Overridepublic void completed(Integer result, ByteBuffer attachment) {// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容if (attachment.hasRemaining()) {sc.write(attachment);}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {exc.printStackTrace();closeChannel(sc);}}private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {private final AsynchronousServerSocketChannel ssc;public AcceptHandler(AsynchronousServerSocketChannel ssc) {this.ssc = ssc;}@Overridepublic void completed(AsynchronousSocketChannel sc, Object attachment) {try {System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());} catch (IOException e) {e.printStackTrace();}ByteBuffer buffer = ByteBuffer.allocate(16);// 读事件由 ReadHandler 处理sc.read(buffer, buffer, new ReadHandler(sc));// 写事件由 WriteHandler 处理sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件ssc.accept(null, this);}@Overridepublic void failed(Throwable exc, Object attachment) {exc.printStackTrace();}}
}
closeChannel(sc);return;}System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());attachment.flip();System.out.println(Charset.defaultCharset().decode(attachment));attachment.clear();// 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件sc.read(attachment, attachment, this);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {closeChannel(sc);exc.printStackTrace();}
}private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {private final AsynchronousSocketChannel sc;private WriteHandler(AsynchronousSocketChannel sc) {this.sc = sc;}@Overridepublic void completed(Integer result, ByteBuffer attachment) {// 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容if (attachment.hasRemaining()) {sc.write(attachment);}}@Overridepublic void failed(Throwable exc, ByteBuffer attachment) {exc.printStackTrace();closeChannel(sc);}
}private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {private final AsynchronousServerSocketChannel ssc;public AcceptHandler(AsynchronousServerSocketChannel ssc) {this.ssc = ssc;}@Overridepublic void completed(AsynchronousSocketChannel sc, Object attachment) {try {System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());} catch (IOException e) {e.printStackTrace();}ByteBuffer buffer = ByteBuffer.allocate(16);// 读事件由 ReadHandler 处理sc.read(buffer, buffer, new ReadHandler(sc));// 写事件由 WriteHandler 处理sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));// 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件ssc.accept(null, this);}@Overridepublic void failed(Throwable exc, Object attachment) {exc.printStackTrace();}
}

}


Netty学习笔记二网络编程相关推荐

  1. 黑马程序员_java自学学习笔记(八)----网络编程

    黑马程序员_java自学学习笔记(八)----网络编程 android培训. java培训.期待与您交流! 网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无 ...

  2. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  3. Netty学习笔记(二)Netty服务端流程启动分析

    先贴下在NIO和Netty里启动服务端的代码 public class NioServer { /*** 指定端口号启动服务* */public boolean startServer(int por ...

  4. JavaSE学习笔记之网络编程

    网络基础 网络模型 网络模型一般是指OSI七层参考模型和TCP/IP四层参考模型.这两个模型在网络中应用最为广泛. OSI模型,即开放式通信系统互联参考模型(Open System Interconn ...

  5. Qt学习笔记之网络编程

    Qt网络模块提供了允许您编写TCP / IP客户端和服务器的类.它提供了代表低级网络概念的低级类(例如QTcpSocket,QTcpServer和QUdpSocket),以及高级类(例如QNetwor ...

  6. TCP/IP网络编程 学习笔记_1 --网络编程入门

    前言:这个系列网络编程教程实例代码是在Xcode上运行的,MacOSX,是一个基于UNIX核心的系统,所以基于Linux的网络编程代码一般可以直接在Xcode上运行,如果要移植到Windows其实就只 ...

  7. Java基础学习笔记之网络编程

    Java基础之网络编程 1.网络编程概述 什么是网络编程 指的是在多个设备(计算机)执行,其中的设备使用一个网络中的所有连接到对方编写程序 网络编程的目的 与其他计算机进行通信 网络编程的问题 1.如 ...

  8. C 语言学习笔记(二):编程基础

    目录 一.冯诺依曼模型 二.程序语言发展历史 三.进制 3.1 二进制 3.2 八进制 3.3 十六进制 3.4 进制转换:二进制.八进制.十六进制.十进制之间的转换 3.4.1 将二进制.八进制.十 ...

  9. 【Java学习笔记】 网络编程04 优化字符串拼接:JSON

    学习时间 0731 优化拼接字符串 String : 是复合类型 ,相当于char的数组 是final类,也就是不支持继承 public final class String {private fin ...

最新文章

  1. Linux VIM IDE
  2. 洛谷1373 小a和uim之大逃离
  3. eplan图纸怎么发给别人_EPLAN标签导出材料清单(附模板+图文教程)
  4. Dubbo的Provider配置
  5. 历史版本_新版本爆料第弹丨英雄练习新去处,荣耀历史秀出来!
  6. JSON_dump和load
  7. 基于MM2的跨IDC kafka热备多活方案
  8. Python中OS模块
  9. RSS文件的基本格式
  10. DNF单机从服务器获取信息,dnf单机云服务器
  11. html文字自动轮播代码怎么写,图片轮播HTML代码
  12. 阵列天线方向图乘积定理的Python实现
  13. CSS3干货23:常用字体样式设置
  14. 网站服务器商标属于哪类,网络平台商标注册属于什么类别?-商标分类表-猪八戒知识产权...
  15. Java Swing窗体JFrame之设置窗体图标
  16. 运维文档管理规范标准
  17. 2019 NIPS | Variational graph recurrent neural network
  18. thinkphp6,压缩图片内存,并上传到oss,质量影响不大。
  19. 塔防游戏c语言源代码,用Unity开发一款塔防游戏(一):攻击方设计
  20. Elastic Job入门示例

热门文章

  1. php-pear的使用
  2. 00020.07 集合Map的接口和它的实现类们(包含HashMap、Hashtable、TreeMap、LinkedHashMapProperties以及实现Comparable接口模板)
  3. 电子礼品卡腐败必须重视
  4. .bat批处理(一):@echo off
  5. HUAWEI华为MateBook X Pro 2020 i7 16GB+512GB (MACHC-WAE9LP)原装出厂系统恢复原厂系统
  6. python函数怎么打_python ln函数怎么打
  7. AVI转换成MP4格式的教程方法集合
  8. Computer:将视频的avi格式转换为MP4格式多种方法集合
  9. 继CRI CNI CSI后,CNCF对三方设备的管理也将推行标准化接口了
  10. 证书服务器CA的搭建和管理