Netty的介绍与使用--第二天(NIO中Selector的介绍与多人聊天室实现)
1.Selector(选择器)
1.1 Selector基本介绍
(1)java的NIO,用非阻塞的IO方式。可以使用一个线程,处理多个的客户端连接,就会使用到Selector(选择器);
(2)Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求;
(3)只有在连接(通道)真正有读写事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程;
(4)避免了多线程之间的上下文切换导致的开销。
1.2 Selector类相关方法
1.2.1 Selector类是一个抽象类,常用方法和说明如下:
Public abstract class Selector implement Closeable{
Public static selector open();//得到一个选择器对象
Public int select(long timeout);//监控所注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数来设置超时时间
Public Set selecteKeys();//从内部集合中得到所有的SelectionkKey
}
1.2.2 Selector相关方法说明
- Selector.select()//阻塞
- Selector.select(1000)//阻塞1000毫秒,在1000毫秒后返回
- Selector.wakeup()//唤醒selector
- Selector.selectNow;//不阻塞
1.2.3 SelectionKey说明
SelectionKey表示Selector和网络通道的注册关系,共有四种情况:
- int OP_ACCPET:有新的网络可以连接accept,值为16
- Int OP_CONNECT:代表连接已经建立,值为8
- Int OP_WRITE:代表写操作,值为4
- Int OP_READ:代表读操作 值为1
SelectionKey相关的方法
/**
* Returns the selector for which this key was created. This method will
* continue to return the selector even after the key is cancelled.
*
* @return This key's selector
*
*/
//得到关联的selector
public abstract Selector selector();
/**
* Returns the channel for which this key was created. This method will
* continue to return the channel even after the key is cancelled.
*
* @return This key's channel
*/
//得到与之相关的channel
public abstract SelectableChannel channel();
//得到与之关联的共享数据
private volatile Object attachment = null;
private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
SelectionKey.class, Object.class, "attachment"
);
/**
* Sets this key's interest set to the given value.
*
* <p> This method may be invoked at any time. Whether or not it blocks,
* and for how long, is implementation-dependent. </p>
*
* @param ops The new interest set
*
* @return This selection key
*
* @throws IllegalArgumentException
* If a bit in the set does not correspond to an operation that
* is supported by this key's channel, that is, if
* {@code (ops & ~channel().validOps()) != 0}
*
* @throws CancelledKeyException
* If this key has been cancelled
*/
//设置或改变监听事件
public abstract SelectionKey interestOps(int ops);
1.3 Selector的使用
1,但客户端连接时,会通过ServerSocketChannel得到SocketChannel
2,selector进行监听 select方法,返回有事件发生的通道的个数)
3,将SocketChannel注册到selectot上,register(Selector sel,int ops)
4,注册后返回一个selectionKey,会和该selector关联(集合)
5,进一步得到各个selectionKey(有事件发生)
6,再通过selectionKey反向获取SocketChannel,方法 channel()
可以通过得到的channel完成业务处理
2.1 Selector代码示例
2.1.1 实现服务器与客户端连接
服务端代码:
package com.Channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @author linjiazeng
* @version 1.0
* @date 2020/10/27 14:07
**/
public class NIOServer {
public static void main(String[] args)throws Exception {
//创建ServerSocketChannel->ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selector对象
Selector selector = Selector.open();
//绑定一个端口6666,在服务端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//把serverSocketChannel注册到selector 该关注的事件为OP_ACCEPT:打开连接 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
//这里我们等待一秒,如果没有事件发生,则返回
if (selector.select(1000)==0){//没有事件发生
System.out.println("服务器等待了1秒,无连接。。。");
continue;
}
//如果返回大于0,就获取相关的selectionKey集合
//1.如果返回的大于0,表示已经获取到关注的事件
//2.selector.selectedkeys()返回关注事件的集合
//通过selectionKeys反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历 Set<SelectionKey>,使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
//获取selectionKey
SelectionKey key = keyIterator.next();
//根据key对应的通道发生的事件进行相应的处理
if (key.isAcceptable()){
//该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);//设置为非阻塞状态
//将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){//发生OP_READ
//通过key 反向获取到对应channel
SelectableChannel channel =(SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
((SocketChannel) channel).read(buffer);
System.out.println("from 客户端"+new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
客户端代码:
package com.Channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @author linjiazeng
* @version 1.0
* @date 2020/10/27 14:34
**/
public class NIOClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//提供服务端的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他的工作。。。");
}
}
//如果连接成功,就发送数据
String str = "hello,林老三";
ByteBuffer wrap = ByteBuffer.wrap(str.getBytes());
socketChannel.write(wrap);
System.in.read();
}
}
运行后效果:
2.1.2 多人聊天室实现
服务端代码:
package com.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @author linjiazeng
* @version 1.0
* @date 2020/10/27 14:37
**/
public class GroupChatServer {
//定于属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer(){
try {
//得到选择器
selector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel = listenChannel.bind(new InetSocketAddress(PORT));
listenChannel.configureBlocking(false);
listenChannel.register(selector,SelectionKey.OP_ACCEPT);
//TODO 进行监听的行为
}catch (IOException e){
e.printStackTrace();
}
}
//监听
public void listen(){
try {
while (true){
int count = selector.select();
if (count>0){//有事件处理
//遍历得到的SelecttionKey集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//取出selecttionKey
SelectionKey key = iterator.next();
//监听到accept
if (key.isAcceptable()){
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该sc注册到selector
sc.register(selector,SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress()+" 上线");
}
if (key.isReadable()){//通道发送read事件,即通道是可读的状态
//TODO 处理写方法
readData(key);
}
//当前的key删除,防止重复出现
iterator.remove();
}
}else{
System.out.println("等待---");
}
}
}catch (IOException e){
e.printStackTrace();
}
}
private void readData(SelectionKey key){
//取到关联的channel
SocketChannel channel = null;
try{
//得到channel
channel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if (count>0){
//把缓冲区的数据转成字符串
String msg = new String(buffer.array());
//输出该信息
System.out.println("from 客户端"+msg);
//TODO 向其他的客户端发送信息
sendInfoToOtherClients(msg,channel);
}
}catch (IOException e){
try {
System.out.println(channel.getRemoteAddress()+"离线了。。。");
//取消注册
key.cancel();
//关闭通道
channel.close();
}catch (IOException e1){
}
e.printStackTrace();
}
}
private void sendInfoToOtherClients(String msg,SocketChannel self)throws IOException{
System.out.println("服务器转发信息中。。。。");
//遍历所有注册selector上的Socketchannel ,并排除self
for (SelectionKey key:selector.keys()){
//通过key取出对应的SocketChannel
SelectableChannel targetChannel = key.channel();
if (targetChannel instanceof SocketChannel &&targetChannel !=self){
//转型
SocketChannel dest = (SocketChannel) targetChannel;
//将mg存储到buffer中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
、
客户端代码:
package com.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
/**
* @author linjiazeng
* @version 1.0
* @date 2020/10/15 23:11
**/
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1";//服务器的ip
private final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public GroupChatClient()throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
//设置为非阻塞
socketChannel.configureBlocking(false);
//将channel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username+"is ok...");
}
//先服务器发送消息
public void sendInfo(String info){
info = username+"说:"+info;
try{
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (IOException e){
e.printStackTrace();
}
}
//读取从服务器端回复的信息
public void readInfo(){
try{
int readChannel = selector.select();
if (readChannel>0){//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isReadable()){
//得到相关的通道
SocketChannel sc =(SocketChannel) key.channel();
//得到一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
}else {
}
}
catch (IOException e1){
e1.printStackTrace();
}
}
public static void main(String[] args) throws IOException{
//启动客户端
GroupChatClient groupChatClient = new GroupChatClient();
//启动一个线程
new Thread(){
public void run(){
while (true){
groupChatClient.readInfo();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
groupChatClient.sendInfo(s);
}
}
}
客户端1:
客户端2:
服务端:
好了,就这样!!!明天开始正式进入Netty主题,因为Netty是对NIO进行进一步包装的,我们在学习Netty前需要先学习了解一些NIO和IO的一些知识,这样能够在以后的学习或工作中更好的理解Netty的工作原理。
打工人,打工神,打工都是人上人!!!!
哎,果然资本家都是吃人血馒头的!!