一.概述
背景:
由于分布式系统大行其道,并且分布式系统的根基在于网络编程,因为各个分布式系统之间要进行相互通信。而Netty是Java领域中关于网络编程领域的高性能框架。netty的底层框架是NIO,所以本文会介绍NIO的知识。
二.NIO基础
1.三大组件
如上图所示,组成NIO的有三大组件,Channel,Buffer和Selector。
Channel:是数据读写的双向通道,可以将数据读入到buffer中,也可以将buffer中的数据写入到Channel中。常见的Channel有:FileChannel(文件传输),DatagramChannel(UDP传输),SocketChannel(TCP传输,客户端和服务器端都能用),ServerSocketChannel(TCP传输,专用于服务端)。
Buffer:就是用来进行缓存的组件。常见的buffer有ByteBuffer(MappedByteBuffer, DirectByteBuffer, HeapByteBuffer)使用字节来缓冲数据。
Selector:(1)在原本的多线程服务器的设计中,系统为每一个socket都开启了一个线程,但是这样做的话内存占用高、线程上下文切换成本高、只适合连接数少的场景,相当于在餐厅里给每个客人都配备了一个服务员。(2)后来人们提出了线程池的技术。缺点就是在阻塞模式下,线程仅能处理一个Socket连接,仅适合短链接的场景。相当于一个服务员只有在当前客人吃饭结束后才能去服务别的客人。(3) 新版selector的设计就是使用一个线程来管理多个channel,获取这些channel上发生的事情,这些channel发生在非阻塞的模式下,不会让线程吊死在一个channel上。适合连接数特别多,但是流量低的场景。当selector管理的某个channel发生读写就绪事件,selector就能监听到,然后返回这个事件交给thread来进行处理。
2.ByteBuffer基本使用
快速入门
package com.guguo.test.netty;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class TestByteBuffer {
public static void main(String[] args) throws IOException {
//获得通道
FileChannel channel =
new FileInputStream("document\\data.txt")
.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10);//得到缓冲区,并命令缓冲区大小为10
while(channel.read(buffer)!=-1){//从channel中读数据,向buffer中写
buffer.flip();//将buffer切换成读状态
while(buffer.hasRemaining()){
byte b = buffer.get();
System.out.println((char)b);
}
buffer.clear();将buffer切换成写状态
}
}
}
内部结构
ByteBuffer内部有以下比较重要的属性:capacity,position,limit。如果说Buffer是一个数组,那么capacity表示这个数组的总大小,position表示当前读写的指针位置,limit表示读写的限制。写模式下,capacity=limit,只要不超过capacity想写多少写多少,position表示当前正在写入的位置。
写模式切换到读模式下,position归0,limit移动到之前position的位置。
读模式切换到写模式时,(1)如果使用clear()方法,是清空数组,从0开始写(2)compact方法是把未读完的部分向前压缩,然后切换至写模式,从上次未读完的部分末尾开始追加写。
常见方法
ByteBuffer.allocate(20);//使用java堆内存,读写效率低,会受到垃圾回收机制影响。ByteBuffer.allocateDirect(20);//使用java的直接内存,读写效率高,不会收到GC影响,但是分配内存的效率较低,可能会造成内存泄漏。
//向buffer写数据的两种方式
(1)int read=channel.read(buffer);
(2)buffer.put((byte)127); buffer.put(new byte[]{'a','b','c'});
//从buffer读数据的两种方式
(1)int write=channel.write(buffer);
(2)byte b=buf.get(); //get()方法会让读指针向后走,如果想重复获取数据,可以使用rewind方法将position变成0。
//字符串和BateBuffer之间的转换方法
(1)ByteBuffer buffer=ByteBuffer.allocate(20);
buffer.put("hello".getBytes());
(2)ByteBuffer buffer=StandardCharsets.UTF-8.encode("hello");
(3)ByteBuffer buffer=ByteBuffer.wrap("hello".getBytes())
(4)String str=StandardCharsets.UTF-8.decode(buffer).toString();//此时得是读状态
//特殊方法
(1)mark(); //做一个标记,记录position的位置
(2)reset(); //是将position重置到mark的位置
(3)get(i); //按照索引去读,不会导致读指针后移。
黏包半包
在数据传输的过程中,一般会把多条信息合并到一起进行传输,这种行为称为黏包。但是不是所有时候都在正好黏整数个包,如果内存不够,有可能会把一条信息拆成两个包来传输。比如:
hello\n world\n 你好\n -----> hello\nworld\n你 好\n
3.FileChannel基本使用
获取:
不能直接打开FileChannel,必须通过FileInputStream,FileOutputStream或者RandomAccessFile来获取FileChannel,他们都有getChannel方法。
1.通过FileInputStream获取的FileChannel只能读。2.通过FileOutputStream获取的FileChannel只能写。3.通过RandomAccessFile获取的FileChannel能否读写根据构造RandomAccessFile时的读写模式决定。
读取:
会从channel读取数据填充ByteBuffer,返回值表示读到了多少字节,-1表示到达了文件的末尾。
int readBytes=channel.read(buffer);
写入:
这里使用循环是因为write方法并不能保证一次九江buffer中的内容全部写进channel。
while(buffer.hasRemaining()){
channel.write(buffer);
)
关闭:
channel必须关闭,不过调用了FileInputStream,FileOutputStream或者RandomAccessFile中的close方法会间接调用channel的close方法。
常见方法:
size方法,获取文件的大小。
position()方法,获取当前读到的位置。
force(true)方法,可以将文件内容强制立刻写进磁盘,不会再进行数据缓存。
4.selector选择器
NIO阻塞模式
1.默认accept()方法是阻塞方法,方法执行到这里,如果没有客户端来进行连接申请,那么程序就停止在这里了。
2.read(buffer)方法也是阻塞方法,如果客户端连接了,但是没发送数据,程序就会一直停在这里。
3.如果你停在了accept()方法这里,那么即使客户端A连接过,他发送的数据也过不来
package com.guguo.test.netty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
//服务端代码
public class selectorDemo {
public static void main(String[] args) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(16);
//1.创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定监听接口
ssc.bind(new InetSocketAddress(8080));//客户端向这个端口发送消息,服务端就能收到
//3.连接集合,用来存储申请连接的客户端
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
while(true){
//4.该方法是接受客户端发来的请求,建立连接。返回值sc用来和客户端通信
SocketChannel sc = ssc.accept();
socketChannels.add(sc);
for (SocketChannel socketChannel:socketChannels) {
//5.将socketChannel中的数据读取到buffer中
socketChannel.read(buffer);
buffer.flip();//换成读模式
String string = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(string);
buffer.clear();//换成写模式
}
}
}
}
package com.guguo.test.netty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
//客户端代码
public class Socket {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
socketChannel.write(ByteBuffer.wrap("hello".getBytes()));
}
}
NIO非阻塞模式
sc.configureBlocking(false);将SocketChannel设置成非阻塞模式,此时read方法将不再是阻塞方法。如果没读到数据,返回值是0。 ssc.configureBlocking(false);//将ServerSocketChannel设置成非阻塞模式,此时accept方法不再是阻塞方法,如果没有客户端发起链接申请,那么返回值是null。
package com.guguo.test.netty;
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.nio.charset.StandardCharsets;
import java.util.ArrayList;
@Slf4j
public class selectorDemo2 {
public static void main(String[] args) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(16);
//1.创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//将ServerSocketChannel设置成非阻塞模式,此时accept方法不再是阻塞方法,如果没有客户端发起链接申请,那么返回值是null
//2.绑定监听接口
ssc.bind(new InetSocketAddress(8080));//客户端向这个端口发送消息,服务端就能收到
//3.连接集合,用来存储申请连接的客户端
ArrayList<SocketChannel> socketChannels = new ArrayList<>();
while(true){
//4.该方法是接受客户端发来的请求,建立连接。返回值sc用来和客户端通信
SocketChannel sc = ssc.accept();
if(sc!=null){
sc.configureBlocking(false);将SocketChannel设置成非阻塞模式,此时read方法将不再是阻塞方法。如果没读到数据,返回值是0
socketChannels.add(sc);
log.debug("链接完毕。。。"+sc);
}
//5.将socketChannel中的数据读取到buffer中
for (SocketChannel socketChannel:socketChannels) {
int read=socketChannel.read(buffer);
if(read>0){
buffer.flip();//换成读模式
String string = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(string);
buffer.clear();//换成写模式
}
}
}
}
}
package com.guguo.test.netty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
public class Socket {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
// socketChannel.write(ByteBuffer.wrap("hello".getBytes()));
}
}
selector强势加入
共有7个问题:
1.事件:
一共有四种事件:accept(在服务端,有连接请求时触发),connect(在客户端,连接建立后触发),read(可读事件),write(可写事件)。
2.事件处理问题:
在selector调用select()方法,得到某个事件。但是如果不对这个事件进行处理,下面的while循环就会一直循环下去。因为selector会觉得,你还没处理完,你要接着处理,然后又把这个事件加进selectedKeys()里面去。所以要么进行处理,要么使用key.cancel()方法把事件取消。
3.key值不移除导致空指针问题:
selector.selectedKeys();这个方法会得到目前有事件发生的key的集合,但是他不会主动在事件处理完之后删除key。在下次循环中,如果拿到该key,但是该key上又没有事件发生,就会返回一个null。所以在处理完事件之后记得将key移除。
4.客户端断开问题:
在客户端强制断开的时候,客户端会给服务端发送一个读事件,但是又没有真正的数据,此时就会产生一个IO异常,并且导致服务端停止运行。我们要及时捕捉到异常,然后把key取消。同样客户端正常断开时候,我们也要及时处理,处理的时机是当read方法的返回值是0时。
package com.guguo.test.netty;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
@Slf4j
public class selectorDemo3 {
public static void main(String[] args) throws IOException {
//1.创建选择器,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
//2.建立selector和channel的联系
SelectionKey registerKey = ssc.register(selector, 0, null);
//3.给这个registerKey指明它关注的事件
registerKey.interestOps(SelectionKey.OP_ACCEPT);//关注了连接请求事件
while(true){
//4.如果没有事情发生,那么这里阻塞。如果有事件发生,方法执行下去。
selector.select();
//5.处理事件,得到不同事件触发后才能返回的key。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();//处理事件之后,记得把key移除
//5.1如果发生了accept事件
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
}
//如果发生了read事件
if(key.isReadable()){
try{
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer);
if(read==-1){//如果客户端正常关闭,这个key要及时取消掉
key.cancel();
}
if(read>0){
buffer.flip();//换成读模式
String string = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(string);
buffer.clear();//换成写模式
}
}catch(IOException e){
e.printStackTrace();
key.cancel();//在客户端断开后,及时把key取消掉
}
}
}
}
}
}
5.处理消息边界问题:
产生原因:比如需要传递的字符占三个字节,但是接收的时候用两个字节来接收,这样这个字就会被分成两部分,就会产生乱码。
三种情况:第一种是ByteBuffer的长度没有传过来的信息长,这个时候需要把ByteBuffer的长度翻倍,来把整个的消息都存进去。第二种,ByteBuffer的长度相对较长,但是能存下1条信息,不一定能完整地存下第2条信息。第三种,ByteBuffer的长度过长,这个时候就会造成黏包的问题。
解决方法:(1)以最长的信息规定一个固定的传送长度。如果规定了长度是10,那么即使传过来两个长度是5的信息,也要分两次来传输。缺点很明显,就会导致资源的浪费嘛。(2)客户端传过来的不同消息用\n进行分割,服务端检测到\n之后,就会创建一个新的buffer来接受新一条消息。缺点就是效率不高,因为要对信息中的每个字符进行判断,看看有没有\n。(3)在每条消息的前几个字节规定好该条消息是多长,服务端就知道该条消息多长了,就读取固定长度的信息。这种被叫做LTV格式,Http2.0就是这种格式。
6.ByteBuffer的大小分配问题:
(1)每个socketChannel都要一个专门属于自己的ByteBuffer来进行数据的传输,这个ByteBuffer以附件的形式注册到key中。
(2)ByteBuffer不能太大,不然链接太多的时候,就会占用大量的内存,因此就要设计大小靠边的ByteBuffer。一种思路是首先分配一个比较小的buffer,比如4k,如果不够再给你个8k的空间。优点是消息连续容易处理,缺点是数据拷贝消耗性能。下面的方法就是这种思想的实现。另一种思路是使用多个数组组成的buffer,一个数组不够,就把多的数据写到新的数组中,和前面的区别是这种方式消息存储不连续,优点是避免了拷贝要引起的性能损耗。
//使用第二种方法解决消息边界问题:(此时buffer缓冲区的长度足够长)
private static void split(ByteBuffer buffer){
buffer.flip();//切换成读模式
for(int i=0;i<buffer.limit();i++){
if(buffer.get(i)=='\n'){
int length=i+1-buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
for(int j=0;j<length;j++){
target.put(buffer.get());
}
target.flip();
String string = StandardCharsets.UTF_8.decode(target).toString();
System.out.print(string);
}
}
buffer.compact();//把读完的数据清理走,剩下的数据堆到前面去
}
package com.guguo.test.netty;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
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;
import java.util.Set;
/*
* 处理消息队列问题,遇到需要扩容的问题
* */
@Slf4j
public class selectorDemo4 {
private static void split(ByteBuffer buffer){
buffer.flip();//切换成读模式
for(int i=0;i<buffer.limit();i++){
if(buffer.get(i)=='\n'){
int length=i+1-buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
for(int j=0;j<length;j++){
target.put(buffer.get());
}
target.flip();
String string = StandardCharsets.UTF_8.decode(target).toString();
System.out.print(string);
}
}
buffer.compact();//把读完的数据清理走,剩下的数据堆到前面去
}
public static void main(String[] args) throws IOException {
//1.创建选择器,管理多个channel
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
//2.建立selector和channel的联系
SelectionKey registerKey = ssc.register(selector, 0, null);
//3.给这个registerKey指明它关注的事件
registerKey.interestOps(SelectionKey.OP_ACCEPT);//关注了连接请求事件
while(true){
//4.如果没有事情发生,那么这里阻塞。如果有事件发生,方法执行下去。
selector.select();
//5.处理事件,得到不同事件触发后才能返回的key。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();//处理事件之后,记得把key移除
//5.1如果发生了accept事件
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
SelectionKey sckey = sc.register(selector, 0, buffer);//同时在这个key上,注册一个只能该channel用的附件
sckey.interestOps(SelectionKey.OP_READ);
}
//如果发生了read事件
if(key.isReadable()){
try{
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();//获得这个附件
int read = channel.read(buffer);
if(read==-1){
key.cancel();
}
if(read>0){
split(buffer);
//每写一个字符,position都会+1,如果position==limit,说明写满了还没有找到\n,就需要进行扩容
if(buffer.position()==buffer.limit()){
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);//进行扩容
buffer.flip();
newBuffer.put(buffer);
key.attach(newBuffer);
}
}
}catch(IOException e){
e.printStackTrace();
key.cancel();
}
}
}
}
}
}
7.可写事件:
当写入的缓冲区满的时候,调用SocketChannel的write(buffer)方法写不进去东西的。所以我们可以在写缓冲区满的时候不要去反复去写了。可以去看看别的SocketChannel中有没有发生可读事件,去处理读事件,处理完之后再尝试去写。
package com.guguo.test.netty;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/*
* 处理可写问题
* */
@Slf4j
public class writeServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector,SelectionKey.OP_ACCEPT,null);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
//1.我们往客户端写点东西
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 300000000; i++) {
stringBuilder.append("a");
}
ByteBuffer buffer=StandardCharsets.UTF_8.encode(stringBuilder.toString());
//2.进行一次写操作
int write = sc.write(buffer);
System.out.println(write);
//3.判断buffer是否有剩余内容没有写完
if(buffer.hasRemaining()){
//4.关注可写事件
scKey.interestOps(scKey.interestOps()+SelectionKey.OP_WRITE);//在原来关注的事件上增加事件
//5.把未写完的数据挂到附件上
scKey.attach(buffer);
}
}else if(key.isWritable()){
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc1 = (SocketChannel) key.channel();
int write1 = sc1.write(buffer);
System.out.println(write1);
//6.清理操作
if(!buffer.hasRemaining()){
key.attach(null);//如果内容都写完了,那么就需要清除buffer,并且取消关注可写事件
key.interestOps(key.interestOps()- SelectionKey.OP_WRITE);
}
}
}
}
}
}
package com.guguo.test.netty;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class writeClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
//接收数据
int count=0;
while(true){
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
count+=socketChannel.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
多线程优化
之前的都是单线程,我们现在使用多线程对刚才的事件进行优化。前面的代码只有一个选择器,现在线程多了之后我们可以搞多个选择器。分两组选择器:单线程配一个选择器,专门处理accept事件;然后创建cpu核心数的线程,每个线程配一个选择器,轮流处理read事件。
根据上文,我们创建选择器boss,专门处理accept事件;然后创建一个worker类,该类会额外启动一个线程,专门用来处理读写事件。
问题1:但是我们写完多线程代码之后会出现问题:注册方法中会开启一个线程,然后这个线程中selector.select();这个方法在没有事件触发时候会阻塞,影响下面sc.register(...);导致方法无法执行。解决方法是使用任务队列把sc.register(...);方法放到worker类的线程run方法中去。修改前和修改后的代码如下:
package com.guguo.test.netty;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
@Slf4j
public class MultiThreadSever {
static class Worker{
private Thread thread;
private Selector selector;
private String name;
private boolean start=false;//此时表示还没有进行初始化
public Worker(String name) {
this.name = name;
}
public void register() throws IOException {
if(!start){
//给选择器赋初值
selector=Selector.open();
//开启一个线程,并且规定这个线程要干什么事
thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isReadable()){//读过来数据,然后打印
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
log.info("读到了数据"+channel.getRemoteAddress());
channel.read(buffer);
buffer.flip();
String str = Charsets.UTF_8.decode(buffer).toString();
System.out.println(str);
buffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
thread.setName(name);
thread.start();
start=true;
}
}
}
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");//为当前进程命名
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
Selector boss = Selector.open();
SelectionKey sscKey = ssc.register(boss, 0, null);
sscKey.interestOps(SelectionKey.OP_ACCEPT);
//1.创建固定数量的worker对象,然后进行初始化
Worker worker = new Worker("worker-0");
while(true){
boss.select();
Iterator<SelectionKey> keyIterator =boss.selectedKeys().iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
log.info("建立了连接"+sc.getRemoteAddress());
//关联selector
log.info("注册前"+sc.getRemoteAddress());
worker.register();//这里注册方法中会开启一个多线程,然后这个多线程中selector.select();这个方法会阻塞,影响下面sc.register(...);导致该方法无法执行
sc.register(worker.selector,SelectionKey.OP_READ,null);
log.info("注册后"+sc.getRemoteAddress());
}
}
}
}
}
package com.guguo.test.netty;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
@Slf4j
public class MultiThreadSever {
static class Worker{
private Thread thread;
private Selector selector;
private String name;
private boolean start=false;//此时表示还没有进行初始化
private ConcurrentLinkedQueue<Runnable> queue=new ConcurrentLinkedQueue<>();
public Worker(String name) {
this.name = name;
}
public void register(SocketChannel sc) throws IOException {
if(!start){
//给选择器赋初值
selector=Selector.open();
//开启一个线程,并且规定这个线程要干什么事
thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
selector.select();
//在这个位置,从队列中把任务整出来,然后执行
Runnable task = queue.poll();
if (task!=null){
task.run();
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isReadable()){//读过来数据,然后打印
ByteBuffer buffer = ByteBuffer.allocate(16);
SocketChannel channel = (SocketChannel) key.channel();
log.info("读到了数据"+channel.getRemoteAddress());
channel.read(buffer);
buffer.flip();
String str = Charsets.UTF_8.decode(buffer).toString();
System.out.println(str);
buffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
thread.setName(name);
thread.start();
start=true;
}
//向队列里添加了一个任务,但是此时这个任务只是添加进去了,但是此时并没有执行
queue.add(()->{
try {
sc.register(selector,SelectionKey.OP_READ,null);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
});
selector.wakeup();
}
}
public static void main(String[] args) throws IOException {
Thread.currentThread().setName("boss");//为当前进程命名
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
Selector boss = Selector.open();
SelectionKey sscKey = ssc.register(boss, 0, null);
sscKey.interestOps(SelectionKey.OP_ACCEPT);
//1.创建固定数量的worker对象,然后进行初始化
Worker worker = new Worker("worker-0");
while(true){
boss.select();
Iterator<SelectionKey> keyIterator =boss.selectedKeys().iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();
if(key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
log.info("建立了连接"+sc.getRemoteAddress());
//关联selector
log.info("注册前"+sc.getRemoteAddress());
worker.register(sc);//这里注册方法中会开启一个多线程,然后这个多线程中selector.select();这个方法会阻塞,影响下面sc.register(...);导致该方法无法执行
log.info("注册后"+sc.getRemoteAddress());
}
}
}
}
}