Java NIO 网络编程学习小结(2)
附上笔者学习的慕课课程链接:
解锁网络编程之NIO的前世今生-慕课网 (imooc.com)
上述课程中项目代码如下(笔者进行了注释和一些改动):
本地实现聊天室程序
服务器端:
package TalkingHouse;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author 作者:Ali
* 描述: 客户端的线程类,专门用于接收服务器端相应信息
*/
public class NioServer {
//start:启动方法
public void start() throws Exception{
//创建selector
Selector selector=Selector.open();
//通过ServerSocketChannel创建channel通道
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//为channel通道绑定监听端口
serverSocketChannel.bind(new InetSocketAddress(8000));
//设置channel为非阻塞模式
serverSocketChannel.configureBlocking(false);
//将channel注册到selector上,监听连接事件,接受连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功!");
//循环等待新接入的连接
while(true){
//获取可用的channel数量
int readyChannel=selector.select();
//TODO 为什么要加这句话 --> 防止selector的空轮询
if(readyChannel==0) continue;
//获取可用的channel集合
Set<SelectionKey> selectionKeys=selector.selectedKeys();
Iterator iterator=selectionKeys.iterator();
while(iterator.hasNext()){
//取出selectionKey的实例
SelectionKey selectionKey=(SelectionKey) iterator.next();
/**
* 移除set中的当前selectionKey
* 原因:selector 监听到一个channel就绪之后,将其加入单独的set集合,通过selectedKeys方法进行取集合;
* 在下一次检测到channel就绪事件之后,还会把之前的channel放入集合中,所以对selectionKey进行处理
* 的时候,就要先将其移出集合,避免set集合中的元素堆积过多。
*/
iterator.remove();
//根据就绪状态,调用对应的方法处理业务
//1.是接入事件
if(selectionKey.isAcceptable()) {
acceptHandler(serverSocketChannel, selector);
}
//2.是可读事件
if (selectionKey.isReadable()){
readHandler(selectionKey,selector);
}
}
}
}
/**
* 一个Channel只能被注册到Selector上一次,如果将Channel注册多次到Selector上,
* 其实相当于是在更新。所以,如果对Channel感兴趣的事件没有变化,是不需要重新注册的。
* 以下两事件处理方法中把再次注册的代码去掉也是不影响运行的
* */
//接入事件处理器
private void acceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) throws Exception{
//服务器接受到客户端的连接之后,再生成一个Socket或SocketChannel与此客户端通信
SocketChannel socketChannel=serverSocketChannel.accept();
socketChannel.configureBlocking(false);//设置非阻塞channel
socketChannel.register(selector,SelectionKey.OP_READ);
//回复客户端提示消息
socketChannel.write(Charset.forName("UTF-8").encode("提示:网络仅为虚拟世界,请注意个人言行以及安全。"));
}
//可读事件处理器
private void readHandler(SelectionKey selectionKey,Selector selector) throws Exception{
//从selectionKey中获取到已经就绪的channel
SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
//创建buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//循环读取客户端请求信息
String request="";
while (socketChannel.read(byteBuffer) >0 ){
//切换buffer为读模式
byteBuffer.flip();
//读取内容
request+=Charset.forName("UTF-8").decode(byteBuffer);
}
//将channel再次注册到selector上,监听他的可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//将客户端发送的消息进行 广播到其他客户端
if(request.length()>0){
//TODO 广播信息到其他客户端
broadCast(selector,socketChannel,request);
}
}
/**
* 广播给其他客户端:
* 1.获取到已接入的客户端channel
* 2.循环向所有channel广播信息
*/
private void broadCast(Selector selector,SocketChannel sourceChannel,String request){
Set<SelectionKey> selectionKeys=selector.keys();
selectionKeys.forEach(
selectionKey -> {
Channel targetChannel=selectionKey.channel();
//除去发送消息的客户端
if (targetChannel instanceof SocketChannel && targetChannel != sourceChannel){
try {
//发送信息到targetChannel客户端
((SocketChannel)targetChannel).write(Charset.forName("UTF-8").encode(request));
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
}
public static void main(String[] args) throws Exception {
new NioServer().start();
}
}
客户端:
package TalkingHouse;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;
/**
* @author 作者:Ali
* 描述: 客户端的线程类,专门用于接收服务器端相应信息
*/
public class NioClient {
/**
* 启动客户端:新开线程用于接收服务器端的响应数据
* 新开线程的代码应当置于向服务器端发送请求之前
* 否则相应信息无法打印
*/
public void start(String name) throws Exception{
//连接服务器端
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
/**
* 接收服务器端响应
* 新开一个线程、专门负责接收服务器端的响应数据
* 主线程返回 继续处理下一个任务 新线程去处理服务器端的响应
* 做到了非阻塞服务
* selector、socketChannel设置非阻塞、注册、新开线程
*/
Selector selector=Selector.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
new Thread(new NioClientHandler(selector)).start();
//向服务器端发出请求数据
Scanner scanner=new Scanner(System.in);
while (scanner.hasNextLine()){
String request=scanner.nextLine();
if (request!=null && request.length()>0){
socketChannel.write(Charset.forName("UTF-8").encode(name+" : "+request));
}
}
}
}
客户端新开线程处理服务器端响应:
package TalkingHouse;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author 作者:Ali
* 描述: 客户端的线程类,专门用于接收服务器端相应信息
*/
public class NioClientHandler implements Runnable {
private Selector selector;
public NioClientHandler(Selector selector){
this.selector=selector;
}
@Override
public void run() {
try {
while (true) {
int readyChannel = selector.select();
if (readyChannel == 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
readHandler(selectionKey, selector);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//可读事件处理器
private void readHandler(SelectionKey selectionKey,Selector selector) throws Exception{
//从selectionKey中获取到已经就绪的channel
SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
//创建buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//循环读取服务器端响应信息
String response="";
while (socketChannel.read(byteBuffer) >0 ){
//切换buffer为读模式
byteBuffer.flip();
//读取内容
response+= Charset.forName("UTF-8").decode(byteBuffer);
}
//将channel再次注册到selector上,监听他的可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//将客户端发送的消息进行 广播到其他客户端
if(response.length()>0){
System.out.println(response);
}
}
}
创建多个客户端:
public class AClient {
public static void main(String[] args) throws Exception {
new NioClient().start("Aclient");
}
}
public class BClient {
public static void main(String[] args) throws Exception {
new NioClient().start("Bclient");
}
}
public class CClient {
public static void main(String[] args) throws Exception {
new NioClient().start("Cclient");
}
}
执行效果图如下:
首先启动服务器端,再启动多个客户端:
服务器将每个客户端输入的数据广播到其他所有客户端。
并且因为可以为不同客户端定义不同name属性,所以可以数据发出的客户端名称。
(本文用于学习分享)