Server:
package com;
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.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 使用NIO完成聊天室服务端
*/
public class NIOServer {
public void start(){
try {
//存放对应所有客户端的SocketChannel,用于广播消息
List<SocketChannel> allChannel = new ArrayList<>();
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//ServerSocketChannel默认是阻塞通道,可改为非阻塞通道
serverSocketChannel.configureBlocking(false);//设置为非阻塞
//为ServerSocketChannel绑定端口,客户端通过该端口进行链接
serverSocketChannel.bind(new InetSocketAddress(8088));
/*
创建多路选择器
这个是NIO实现非阻塞的关键API,用于监控多路设备的事件,并可以做出响应。
使得单个线程仅需要轮询多路选择器就可以对多个设备做出事件处理。
*/
Selector selector = Selector.open();
/*
将ServerSocketChannel注册到多路选择器上,让其监控是否有客户端链接的事件
解放了原来需要让主线程调用serverSocket.accept()这里阻塞的情况。
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//让主线程一直关注选择器是否有事件需要处理了
while(true){
//当选择器发现注册在它上面的某些通道有事件时,该方法会立即返回,否则会阻塞
selector.select();
//开始处理事件
//通过选择器获取目前所有有事件的通道
Set<SelectionKey> keySet = selector.selectedKeys();
//遍历并处理每一个事件
for (SelectionKey key : keySet){
//判断该事件是否为可接受客户端链接的
//(ServerSocketChannel注册的OP_ACCEPT所响应的事件)
if(key.isAcceptable()){
//获取该事件对应通道
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
//“接电话”。
SocketChannel socketChannel = ssc.accept();
//非阻塞的ServerSocketChannel的accept方法可能返回null
if(socketChannel==null){//null就忽略操作
continue;
}
//将SocketChannel设置为非阻塞式
socketChannel.configureBlocking(false);
//将SocketChannel注册到多路选择器上
//关心的事件为:有数据可读取(该客户端发消息过来)
socketChannel.register(selector,SelectionKey.OP_READ);
//将该SocketChannel存入allChannel集合便于广播消息
allChannel.add(socketChannel);
//通过SocketChannel获取远端计算机地址信息并输出
System.out.println(
socketChannel.socket().getInetAddress().getHostAddress()+
"上线了,当前在线人数:"+allChannel.size()
);
//如果该事件是表示有消息可读(该事件一定是SocketChannel响应的)
}else if(key.isReadable()){
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);//读取客户端发送过来的所有数据存入buffer
buffer.flip();//position:0 limit:前面读进来的所有字节
if(buffer.limit()==0){//如果本次一个字节都没有读取到就忽略
buffer.clear();
continue;
}
//获取缓冲区冲本次读取到的所有字节:0--limit
//创建一个与buffer中目前可用字节一样长的一个字节数组
byte[] data = new byte[buffer.limit()];
//要求buffer将所有可用字节存入我们传入的字节数组data中
buffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);
message = sc.socket().getInetAddress().getHostAddress()
+"说:"+message;
//清除buffer后,position=0 limit=容量
buffer.clear();
//put后,position=转换的字节数组长度 limit=容量
buffer.put(message.getBytes(StandardCharsets.UTF_8));
//广播消息给所有客户端
for(SocketChannel channel : allChannel){
buffer.flip();//position=0 limit=转换的字节数组长度
channel.write(buffer);
//write完毕后 position=转换的字节数组长度 limit=转换的字节数组长度
}
System.out.print(message);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
NIOServer server = new NIOServer();
server.start();
}
}
Client:
package com;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* client 客户端
*
* 聊天室客户端
*/
public class Client {
/*
java.net.Socket 套接字
封装了TCP协议的通讯细节,使用它可以和远端计算机建立TCP链接
并基于两条流的读写操作完成与远端计算机的数据交换。
*/
private Socket socket;
/**
* 构造方法,用于初始化客户端
*/
public Client(){
try {
/*
实例化Socket时通常需要传入两个参数:
1:远端计算机的地址信息(IP) 192.168.1.1
2:远端计算机开启的服务端口
我们通过地址信息找到网络上的服务端计算机,通过端口链接到该机器上运行的服务端
应用程序。
实例化的过程就是与服务端链接的过程,若链接失败会抛出异常!
*/
System.out.println("正在连接服务端...");
socket = new Socket("localhost",8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
//启动一条线程来处理读取服务端发送过来消息的操作
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.setDaemon(true);//如果不再发消息了也就不用再读客户端消息了,所以可以设置为守护线程
t.start();
try {
/*
通过Socket的方法getOutputStream()可以获取一个字节输出流
使用这个输出流写出的字节会发送给远端计算机.
*/
OutputStream out = socket.getOutputStream();
//转换输出流(高级流),1:负责衔接字符与字节流 2:将写出的字符转换为字节
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
//缓冲字符输出流(高级流),负责块写文本数据提高写出效率
BufferedWriter bw = new BufferedWriter(osw);
//PrintWriter(高级流),1:按行写出字符串 2:自动行刷新
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
System.out.println("请开始输入内容,单独输入exit退出。");
while(true) {
String line = scanner.nextLine();
if("exit".equals(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
//处理与服务端断开链接的操作
try {
//调用socket的close可以与远端计算机断开链接,并且自动关闭通过它获取的流
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
/**
* 该线程任务负责读取服务端发送过来的消息
*/
private class ServerHandler implements Runnable{
public void run(){
try {
//通过socket获取输入流,用于读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
//循环读取服务端发送过来的每一条消息并输出
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}