1.Buffer
常用ByteBuffer
- 其中的三个属性position,limit,capacity
- capacity 指的是容量,如capacity为1024的IntBuffer可以存放1024个int类型的值
- position默认在位置0,代表下一个 要读或写的位置,每次读写一个数据,后移一位
- limit 写模式下,代表最大写入多少个数据,此时和capacity相同,读模式下,代表最多读多少个数据(数据个数)
- 一些常用方法
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
public static ByteBuffer wrap(byte[] array) {
...
}
- 一些api
写模式切换到读模式的flip
public final Buffer flip() {
limit = position; // 将 limit 设置为实际写入的数据数量
position = 0; // 重置 position 为 0
mark = -1; // mark 之后再说
return this;
}
读完之后可能重新用buffer来写,使用clear
重新设置
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
当读到第5个数据时先mark一下,读到10时reset一下就可以实现重新从第5个数据读mark reset
.就是将position的位置记录一下,等会再修改回来.
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
2.channel
- 和buffer的读写
将数据从buffer写到channel里面channel.write(buffer)
- channel的实现类有
FileChannel SocketChannel ServerSocketChannel
- 使用channel实现文件拷贝
static void channel() throws IOException {
File file = new File("F:\\in.txt");
File file1 = new File("F:\\out.txt");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream outputStream = new FileOutputStream(file1);
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
int len = -1;
while ((len = inChannel.read(byteBuffer)) != -1) {
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
}
fileInputStream.close();
outputStream.close();
}
SocketChannel和ServerSocketChannel
// 打开一个通道
SocketChannel socketChannel = SocketChannel.open();
// 发起连接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
SocketChannel socketChannel = serverSocketChannel.accept();
}
3.Selector
可以监听多个Channel
可以监听channel的读事件,写事件,建立连接的事件
selectionKey = channel.register(selector,read)
将通道注册到selector上,并监听可读的请求.
之后可以通过Set<SelectionKey> selectedKeys = selector.selectedKeys();
获取SelectionKey的集合,通过遍历处理事件(如果是连接请求,创建一个新的SocketChannel并注册到selector上,如果是可读的事件,调用channel.read(buffer)
接收数据)
举例
public class SelectorServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
// 将其注册到 Selector 中,监听 OP_ACCEPT 事件
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
// 遍历
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
// 有已经接受的新的到服务端的连接
SocketChannel socketChannel = server.accept();
// 有新的连接并不代表这个通道就有数据,
// 这里将这个新的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 有数据可读
// 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int num = socketChannel.read(readBuffer);
if (num > 0) {
// 处理进来的数据...
System.out.println("收到数据:" + new String(readBuffer.array()).trim());
ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes());
socketChannel.write(buffer);
} else if (num == -1) {
// -1 代表连接已经关闭
socketChannel.close();
}
}
}
}
}
}
4.NIO的Selector和linux的select poll epoll
Selector封装了后面三种方式,对于linux来讲,可能用的其中一个
5.select
一个进程管理多个socket的连接
- 进程A调用select进入阻塞,即进入每个socket的等待队列.由于CPU并行执行其他进程,因此进入阻塞的进程不会占用CPU
- select调用时传入需要管理的socket集合,将对应的文件描述符由用户进程传入内核,并进行检查有没有对应的事件到达,如果有则立即返回.
- 当网络数据到达时,引发中断,cpu调用中断处理程序,将数据通过DMA 硬件等方式由网卡传递到socket的读缓冲区
- 进程A由于等待的事件到达被唤醒,从socket的等待队列移动到进程运行的队列
- 即select函数返回值大于0,表示有多少个等待的事件到达,这时需要遍历所有socket,看看是哪个socket的事件发生了
- 处理事件
epoll
epoll_create创建对象
epoll_ctl将管理的socket加入到epoll对象当中
epoll_wait等待数据到达 - 首先调用epoll_create创建eventpoll对象
- 调用epoll_ctl会将eventpoll对象加入到需要管理的socket的等待队列当中
- 当数据到达网卡,触发中断信号,中断程序将数据由网卡通过DMA 硬件等方式拷贝到内存,还会操作socket的等待队列中的eventpoll对象,其维护了一个就绪列表,存放有事件发送的socket
- 调用epol_wait时,如果eventpoll对象里面的就绪列表中已经有引用的socket,就会返回,如果没有会阻塞进程.
- eventpoll对象维护监视的socket集合是通过红黑树来维护,而就绪列表是通过双向链表来维护.