回顾 socket·NIO socket 实现 控制台 一问一答式聊天
本文将实现多客户端自由群聊,加服务端广播的功能
服务端广播需要输入,而 NIO socket 依赖循环遍历不通的状态,会造成阻塞,一种思路是对不通状态的监听使用多线程,一种是将写这个动作使用多线程。
从实践看,NIO 是可以避免使用多线程的。所以我们采取第二种方法,使用 Queue 保存控制台写入的信息。
为了降低 Cpu 负载,我们在代码中增加了 Thread.sleep 的逻辑
selector.key() 是全集注册的 channel,但是你不能手动 remove,除非 “ A key is removed only after
it has been cancelled and its channel has been deregistered”。
selector.selectedKeysa() 允许你编辑加 remove,看业务场景
/**
* Returns this selector's key set.
*
* The key set is not directly modifiable. A key is removed only after
* it has been cancelled and its channel has been deregistered. Any
* attempt to modify the key set will cause an {@link
* UnsupportedOperationException} to be thrown.
*
*
The key set is not thread-safe.
*
* @return This selector's key set
*
* @throws ClosedSelectorException
* If this selector is closed
*/
public abstract Set<SelectionKey> keys();
/**
* Returns this selector's selected-key set.
*
* Keys may be removed from, but not directly added to, the
* selected-key set. Any attempt to add an object to the key set will
* cause an {@link UnsupportedOperationException} to be thrown.
*
*
The selected-key set is not thread-safe.
*
* @return This selector's selected-key set
*
* @throws ClosedSelectorException
* If this selector is closed
*/
public abstract Set<SelectionKey> selectedKeys();
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Title: Socket Server
* @Description: NIO Socket
* @Author: WuJie
*/
public class NioSocketServer {
private final static String CHARSET = "UTF8";
private static Selector selector = null;
private final static String CLIENTINFO = "clientInfo";
/**降低一下CPU的负载~*/
private final static long SLEEP_MILLIS = 100;
/**临时存储输入的消息*/
volatile static Queue<String> writeInfoQueue = new LinkedBlockingQueue();
public static void main(String[] args) {
NioSocketServer nioSocketServer = new NioSocketServer();
//启动服务器,等待客户端访问
nioSocketServer.startServer();
//处理控制台的输入信息
ScannerThread myThread = new ScannerThread();
myThread.start();
if (selector != null) {
//处理客户端的请求
nioSocketServer.handle(selector);
} else {
System.out.println("error 2,服务端启动失败");
}
}
private static class ScannerThread extends Thread{
@Override
public void run() {
while(true){
Scanner scanner=new Scanner(System.in);
String response=scanner.nextLine();
writeInfoQueue.add(response);
}
}
}
/**
* 启动服务器
*/
public void startServer() {
try {
//初始化 nio Selector
selector = SelectorProvider.provider().openSelector();
//阻塞 selector 1000 毫秒,非必须
selector.select(100);
//Channel 多线程安全、支持异步关闭
//初始化服务端 SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置服务端端口
serverSocketChannel.bind(new InetSocketAddress(10000));
//非阻塞模式,否则就不是NIO了
serverSocketChannel.configureBlocking(false);
//将服务端 Channel 注册到 Selector,感兴趣事件为 准备接受访问
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端已启动,服务器端字符编码格式为" + CHARSET + ",等待客户端的访问:");
} catch (IOException e) {
System.out.println("error 1,获取 Selector 失败" + e);
}
}
/**
* 处理客户端的请求与交互
*
* @param selector
*/
public void handle(Selector selector) {
try {
for (; ; ) {
try {
Thread.sleep(SLEEP_MILLIS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取已注册 channel 数量
int keyNumbers = selector.select();
//如果已注册 channel 数量>0,则开始处理
if (keyNumbers > 0) {
//获取 selector 中已注册 channel 的 SelectionKey 集合
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator iterable = selectionKeySet.iterator();
while (iterable.hasNext()) {
//注意语法规则,必须 next 才能 remove
SelectionKey selectionKey = (SelectionKey) iterable.next();
iterable.remove();
if (selectionKey.isAcceptable()) {
//接受客户端请求
acceptable(selectionKey);
} else if (selectionKey.isReadable() && selectionKey.isValid()) {
//读取客户端传输的数据
readable(selectionKey, selector.keys());
} else if(!writeInfoQueue.isEmpty()){
String info = writeInfoQueue.poll();
if(StringUtils.isEmpty(info)){
continue;
}
Iterator<SelectionKey> iterator = selector.keys().iterator();
while(iterator.hasNext()){
SelectionKey target = iterator.next();
//iterable.remove();
if(target.isWritable()&&target.isValid()) {
writable(target,"[服务端广播]:" + info);
}
}
}
}
} else {
continue;
}
}
} catch (IOException e) {
System.out.println("error 3,获取 Selector 已注册 Channel 数量失败" + e);
}
}
/**
* 处理客户端请求
* 将客户单 channel 注册到 selector
*
* @param selectionKey
*/
private static void acceptable(SelectionKey selectionKey) {
try {
//获取客户端请求
SocketChannel clientChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
clientChannel.configureBlocking(false);
//可以将该map连同 channel 一起注册到 selector,map可以携带附属信息
Map<String, String> map = new HashMap();
map.put(CLIENTINFO, clientChannel.getRemoteAddress().toString());
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, map);
} catch (ClosedChannelException e1) {
System.out.println("error 4,将客户端请求 channel 注册到 selector 失败" + e1);
} catch (IOException e2) {
System.out.println("error 5,获取客户端请求失败" + e2);
}
}
/**
* 处理可读的channel
* 读取客户端发送的信息
*
* @param selectionKey
*/
private static void readable(SelectionKey selectionKey, Set<SelectionKey> set) {
try {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
//准备 1k 的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将channel 内容写入缓存 byteBuffer
int len = clientChannel.read(byteBuffer);
//System.out.println("byteBuffer.remaining()="+byteBuffer.remaining()+";len="+len);
//如果没有信息传入,说明客户端可能中断连接,则关闭这个客户端channel
if (len == -1) {
try {
clientChannel.close();
selector.wakeup();//唤起其他channel
System.out.println("line 133,客户端已关闭:" + clientChannel.socket().getRemoteSocketAddress());
System.out.println("可读--------------结束");
//continue;
return;
} catch (Exception e) {
System.out.println("error 6,关闭客户端 channel 失败" + e);
System.out.println("可读--------------结束");
//continue;
return;
}
}
//调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
byteBuffer.flip();
//初始化一个 缓存区可读长度的数组
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, byteBuffer.remaining());
byteBuffer.clear();
String info = new String(bytes, CHARSET);
Map<String,String> attachment=(HashMap) selectionKey.attachment();
System.out.println("客户端["+ attachment.get(CLIENTINFO)+"]" + info);
Iterator<SelectionKey> iterator = set.iterator();
while(iterator.hasNext()){
SelectionKey target = iterator.next();
if(target.isWritable()&&target.isValid()) {
Map<String,String> targetAttachment=(HashMap) target.attachment();
//iterator.remove();
if(targetAttachment.get(CLIENTINFO).equals(attachment.get(CLIENTINFO))){
continue;
}
writable(target,"客户端["+ attachment.get(CLIENTINFO)+"]" + info);
}
}
} catch (IOException e) {
System.out.println("error 7,出错了");
}
}
/**
* 处理可写的channel
* 向客户端发送信息,该信息从控制台输入
*
* @param selectionKey
*/
private static void writable(SelectionKey selectionKey, String info) {
try {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
int len = clientChannel.write(ByteBuffer.wrap(info.getBytes(CHARSET)));
if (len == -1) {
try {
clientChannel.close();
selector.wakeup();
System.out.println("line 182,客户端已关闭:" + clientChannel.socket().getRemoteSocketAddress());
System.out.println("可写--------------结束");
//continue;
return;
} catch (Exception e) {
System.out.println("error 8" + e);
System.out.println("可写--------------结束");
//continue;
return;
}
}
selector.wakeup();
} catch (UnsupportedEncodingException e) {
System.out.println("error 9,字符格式不被支持" + e);
} catch (IOException e2) {
System.out.println("error 10" + e2);
}
}
}
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Title:Socket Client
* @Description: NIO Socket
* @Author: WuJie
*/
public class NioSocketClient {
private final static String CHARSET="UTF8";
/**降低一下CPU的负载~*/
private final static long SLEEP_MILLIS = 100;
private static Selector selector=null;
/**临时存储输入的消息*/
volatile static Queue<String> writeInfoQueue = new LinkedBlockingQueue();
public static void main(String [] args){
NioSocketClient nioSocketClient=new NioSocketClient();
nioSocketClient.connectedServer();
//处理控制台的输入信息
ScannerThread myThread = new ScannerThread();
myThread.start();
if(selector!=null){
nioSocketClient.handle(selector);
}else{
System.out.println("error 2,服务端启动失败");
}
}
private static class ScannerThread extends Thread{
@Override
public void run() {
while(true){
Scanner scanner=new Scanner(System.in);
String response=scanner.nextLine();
writeInfoQueue.add(response);
}
}
}
/**
* 处理客户端与服务器端的交互
*/
public void connectedServer(){
try{
//初始化 nio Selector
selector= SelectorProvider.provider().openSelector();
//阻塞 selector 1000 毫秒,非必须
selector.select(1000);
//Channel 多线程安全、支持异步关闭
//初始化 客户端 SocketChannel
SocketChannel clientChannel=SocketChannel.open();
//非阻塞模式,否则就不是NIO了
clientChannel.configureBlocking(false);
//连接服务端
clientChannel.connect(new InetSocketAddress("127.0.0.1",10000));
//将 clientChannel 注册到selector,感兴趣事件为连接
clientChannel.register(selector,SelectionKey.OP_CONNECT);
System.out.println("客户端已启动,客户端字符编码格式为"+CHARSET+",等待取得和服务器端的连接:");
}catch(IOException e){
System.out.println("error 1,获取 Selector 失败"+e);
}
}
/**
* 处理和服务器端的交互
* @param selector
*/
public void handle(Selector selector){
try{
for(;;){
try {
Thread.sleep(SLEEP_MILLIS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取已注册 channel 数量
int keyNumbers=selector.select();
//如果已注册 channel 数量>0,则开始处理
if(keyNumbers>0){
//获取 selector 中已注册 channel 的 SelectionKey 集合
Set<SelectionKey> selectionKeySet=selector.selectedKeys();
Iterator iterable=selectionKeySet.iterator();
while(iterable.hasNext()){
//注意基础语法,必须 next 后才能 remove
SelectionKey selectionKey=(SelectionKey)iterable.next();
iterable.remove();
if(selectionKey.isConnectable()){
//处理 连接 类型的channel
connectable(selectionKey);
}else if(selectionKey.isReadable()&&selectionKey.isValid()){
//读取服务端传输的数据
readable(selectionKey);
}else if(selectionKey.isWritable()&&selectionKey.isValid()) {
//返回给服务端信息
writable(selectionKey);
}
}
}else{
continue;
}
}
}catch(IOException e){
System.out.println("error 3,获取 Selector 已注册 Channel 数量失败"+e);
}
}
/**
* 处理可连接的channel
* 取得和服务端的连接,向服务端发送信息,该信息从控制台输入
* @param selectionKey
*/
private static void connectable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel)selectionKey.channel();
if(clientChannel.isConnectionPending()){
while(!clientChannel.finishConnect()){
System.out.println("当 channel 的 socket 连接上后,返回true");
};
}
//客户端已连接上服务端,将感兴趣事件修改为 可读|可写
selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
selector.wakeup();
}catch(UnsupportedEncodingException e){
System.out.println("error 5,字符格式不被支持"+e);
}catch(IOException e2){
System.out.println("error 6"+e2);
}
}
/**
* 处理可读的channel
* 读取客户端发送的信息
* @param selectionKey
*/
private static void readable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel) selectionKey.channel();
//准备 1k 的缓冲区
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
//将channel 内容写入缓存 byteBuffer
int len=clientChannel.read(byteBuffer);
//System.out.println("byteBuffer.remaining()="+byteBuffer.remaining()+";len="+len);
if(len==-1){
try{
clientChannel.close();
selector.wakeup();//唤起其他channel
System.out.println("line 160,服务端已关闭:"+clientChannel.socket().getRemoteSocketAddress());
System.out.println("可读--------------结束");
//continue;
return;
}catch(Exception e){
System.out.println("error 7,关闭客户端 channel 失败"+e);
System.out.println("可读--------------结束");
//continue;
return;
}
}
//调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
byteBuffer.flip();
//初始化一个 缓存区可读长度的数组
byte[] bytes=new byte[byteBuffer.remaining()];
byteBuffer.get(bytes,0,byteBuffer.remaining());
byteBuffer.clear();
System.out.println("服务端:"+new String(bytes,CHARSET));
}catch(IOException e){
System.out.println("error 8,出错了");
}
}
/**
* 处理可写的channel
* 向服务端发送信息,该信息从控制台输入
* @param selectionKey
*/
private static void writable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel) selectionKey.channel();
if(writeInfoQueue.isEmpty()){
return;
}
String info= writeInfoQueue.poll();
if(StringUtils.isEmpty(info)){
return;
}
int len=clientChannel.write(ByteBuffer.wrap(info.getBytes(CHARSET)));
if(len==-1){
try{
clientChannel.close();
selector.wakeup();
System.out.println("line 210,服务端已关闭:"+clientChannel.socket().getRemoteSocketAddress());
System.out.println("可写--------------结束");
//continue;
return ;
}catch(Exception e){
System.out.println("error 9"+e);
System.out.println("可写--------------结束");
//continue;
return ;
}
}
selector.wakeup();//唤醒其他key
}catch(UnsupportedEncodingException e){
System.out.println("error 9,字符格式不被支持"+e);
}catch(IOException e2){
System.out.println("error 10"+e2);
}
}
}
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Queue;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @Title:Socket Client
* @Description: NIO Socket
* @Author: WuJie
*/
public class NioSocketClient2 {
private final static String CHARSET="UTF8";
/**降低一下CPU的负载~*/
private final static long SLEEP_MILLIS = 100;
private static Selector selector=null;
/**临时存储输入的消息*/
volatile static Queue<String> writeInfoQueue = new LinkedBlockingQueue();
public static void main(String [] args){
NioSocketClient2 nioSocketClient=new NioSocketClient2();
nioSocketClient.connectedServer();
//处理控制台的输入信息
ScannerThread myThread = new ScannerThread();
myThread.start();
if(selector!=null){
nioSocketClient.handle(selector);
}else{
System.out.println("error 2,服务端启动失败");
}
}
private static class ScannerThread extends Thread{
@Override
public void run() {
while(true){
Scanner scanner=new Scanner(System.in);
String response=scanner.nextLine();
writeInfoQueue.add(response);
}
}
}
/**
* 处理客户端与服务器端的交互
*/
public void connectedServer(){
try{
//初始化 nio Selector
selector= SelectorProvider.provider().openSelector();
//阻塞 selector 1000 毫秒,非必须
selector.select(1000);
//Channel 多线程安全、支持异步关闭
//初始化 客户端 SocketChannel
SocketChannel clientChannel=SocketChannel.open();
//非阻塞模式,否则就不是NIO了
clientChannel.configureBlocking(false);
//连接服务端
clientChannel.connect(new InetSocketAddress("127.0.0.1",10000));
//将 clientChannel 注册到selector,感兴趣事件为连接
clientChannel.register(selector,SelectionKey.OP_CONNECT);
System.out.println("客户端已启动,客户端字符编码格式为"+CHARSET+",等待取得和服务器端的连接:");
}catch(IOException e){
System.out.println("error 1,获取 Selector 失败"+e);
}
}
/**
* 处理和服务器端的交互
* @param selector
*/
public void handle(Selector selector){
try{
for(;;){
try {
Thread.sleep(SLEEP_MILLIS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取已注册 channel 数量
int keyNumbers=selector.select();
//如果已注册 channel 数量>0,则开始处理
if(keyNumbers>0){
//获取 selector 中已注册 channel 的 SelectionKey 集合
Set<SelectionKey> selectionKeySet=selector.selectedKeys();
Iterator iterable=selectionKeySet.iterator();
while(iterable.hasNext()){
//注意基础语法,必须 next 后才能 remove
SelectionKey selectionKey=(SelectionKey)iterable.next();
iterable.remove();
if(selectionKey.isConnectable()){
//处理 连接 类型的channel
connectable(selectionKey);
}else if(selectionKey.isReadable()&&selectionKey.isValid()){
//读取服务端传输的数据
readable(selectionKey);
}else if(selectionKey.isWritable()&&selectionKey.isValid()) {
//返回给服务端信息
writable(selectionKey);
}
}
}else{
continue;
}
}
}catch(IOException e){
System.out.println("error 3,获取 Selector 已注册 Channel 数量失败"+e);
}
}
/**
* 处理可连接的channel
* 取得和服务端的连接,向服务端发送信息,该信息从控制台输入
* @param selectionKey
*/
private static void connectable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel)selectionKey.channel();
if(clientChannel.isConnectionPending()){
while(!clientChannel.finishConnect()){
System.out.println("当 channel 的 socket 连接上后,返回true");
};
}
//客户端已连接上服务端,将感兴趣事件修改为 可读|可写
selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
selector.wakeup();
}catch(UnsupportedEncodingException e){
System.out.println("error 5,字符格式不被支持"+e);
}catch(IOException e2){
System.out.println("error 6"+e2);
}
}
/**
* 处理可读的channel
* 读取客户端发送的信息
* @param selectionKey
*/
private static void readable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel) selectionKey.channel();
//准备 1k 的缓冲区
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
//将channel 内容写入缓存 byteBuffer
int len=clientChannel.read(byteBuffer);
//System.out.println("byteBuffer.remaining()="+byteBuffer.remaining()+";len="+len);
if(len==-1){
try{
clientChannel.close();
selector.wakeup();//唤起其他channel
System.out.println("line 160,服务端已关闭:"+clientChannel.socket().getRemoteSocketAddress());
System.out.println("可读--------------结束");
//continue;
return;
}catch(Exception e){
System.out.println("error 7,关闭客户端 channel 失败"+e);
System.out.println("可读--------------结束");
//continue;
return;
}
}
//调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
byteBuffer.flip();
//初始化一个 缓存区可读长度的数组
byte[] bytes=new byte[byteBuffer.remaining()];
byteBuffer.get(bytes,0,byteBuffer.remaining());
byteBuffer.clear();
System.out.println("服务端:"+new String(bytes,CHARSET));
}catch(IOException e){
System.out.println("error 8,出错了");
}
}
/**
* 处理可写的channel
* 向服务端发送信息,该信息从控制台输入
* @param selectionKey
*/
private static void writable(SelectionKey selectionKey){
try{
SocketChannel clientChannel=(SocketChannel) selectionKey.channel();
if(writeInfoQueue.isEmpty()){
return;
}
String info= writeInfoQueue.poll();
if(StringUtils.isEmpty(info)){
return;
}
int len=clientChannel.write(ByteBuffer.wrap(info.getBytes(CHARSET)));
if(len==-1){
try{
clientChannel.close();
selector.wakeup();
System.out.println("line 210,服务端已关闭:"+clientChannel.socket().getRemoteSocketAddress());
System.out.println("可写--------------结束");
//continue;
return ;
}catch(Exception e){
System.out.println("error 9"+e);
System.out.println("可写--------------结束");
//continue;
return ;
}
}
selector.wakeup();//唤醒其他key
}catch(UnsupportedEncodingException e){
System.out.println("error 9,字符格式不被支持"+e);
}catch(IOException e2){
System.out.println("error 10"+e2);
}
}
}
服务端已启动,服务器端字符编码格式为UTF8,等待客户端的访问:
广播测试
客户端[/127.0.0.1:50179]客户端1-1
客户端[/127.0.0.1:50179]客户端1-2
客户端[/127.0.0.1:50186]客户单2-1
客户端[/127.0.0.1:50186]客户端2-2
广播测试2
客户端已启动,客户端字符编码格式为UTF8,等待取得和服务器端的连接:
服务端:[服务端广播]:广播测试
客户端1-1
客户端1-2
服务端:客户端[/127.0.0.1:50186]客户单2-1
服务端:客户端[/127.0.0.1:50186]客户端2-2
服务端:[服务端广播]:广播测试2
客户端已启动,客户端字符编码格式为UTF8,等待取得和服务器端的连接:
服务端:[服务端广播]:广播测试
服务端:客户端[/127.0.0.1:50179]客户端1-1
服务端:客户端[/127.0.0.1:50179]客户端1-2
客户单2-1
客户端2-2
服务端:[服务端广播]:广播测试2