Java NIO 实现群聊系统
服务端代码
package nio.server;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import nio.MessageInfo;
import nio.MessageTypeEnum;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.function.Function;
public class NIOGroupChatServer {
private NIOGroupChatServer() {
try {
selector = Selector.open();
server = ServerSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
private static Selector selector;
private static ServerSocketChannel server;
private static Map<MessageTypeEnum, Function<String, String>> map = new HashMap<>();
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 把Function接口函数存到map的值里,去除掉一大堆if else判断,易于维护
*/ {
map.put(MessageTypeEnum.MESSAGE_INFO, c -> {
return messageHandle(c);
});
map.put(MessageTypeEnum.ONLINE, c -> {
return onlineMsgHandle(c);
});
map.put(MessageTypeEnum.OFFLINE, c -> {
return offlineMsgHandle(c);
});
}
public static void main(String[] args) {
NIOGroupChatServer.getInstance().bind(9310).run();
}
public NIOGroupChatServer bind(int port) {
try {
server.socket().bind(new InetSocketAddress(port));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
return this;
}
public void run() {
while (true) {
try {
int select = selector.select();
if (select > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) {
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
String content = "用户[" + socketChannel.hashCode() + "]";
MessageInfo messageInfo = new MessageInfo(content, null, MessageTypeEnum.ONLINE);
// 通知其他用户,当前用户已上线
forWardMsg(messageInfo, socketChannel);
}
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
// 这里这么做是因为buffer.array() 的数组如果实际大小不足1024 其他空位会以0补齐
// 定义一个新的数组,大小为buffer.limit(),节省了传输数据的空间
byte[] newArray = new byte[buffer.limit()];
System.arraycopy(buffer.array(),0,newArray,0,buffer.limit());
String msg = new String(newArray,"UTF-8");
String username = String.valueOf(channel.hashCode());
MessageInfo messageInfo = new MessageInfo(msg, username, MessageTypeEnum.MESSAGE_INFO);
// 普通消息转发
forWardMsg(messageInfo, channel);
}
} catch (IOException e) {
key.cancel();
SocketChannel channel = (SocketChannel) key.channel();
String msg = "用户[" + channel.hashCode() + "]";
// 通知其他用户,当前用户已下线
MessageInfo messageInfo = new MessageInfo(msg, null, MessageTypeEnum.OFFLINE);
forWardMsg(messageInfo, channel);
channel.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 消息转发
*
* @param messageInfo 消息封装类
* @param excludeChannel 要排除的消息转发的channel
*/
private void forWardMsg(MessageInfo messageInfo, SocketChannel excludeChannel) {
String content = map.get(messageInfo.getType()).apply(messageInfo.getContent());
messageInfo.setContent(content);
ObjectMapper mapper = new ObjectMapper();
try {
String msg = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(messageInfo);
System.out.println(msg.length());
selector.keys().stream().map(SelectionKey::channel)
.filter(c -> c instanceof SocketChannel && c != excludeChannel)
.forEach(c -> {
try {
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
((SocketChannel) c).write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
/**
* 普通消息处理
*
* @param msg 普通消息
* @return
*/
private String messageHandle(String msg) {
String dateStr = formatter.format(LocalDateTime.now());
StringBuilder builder = new StringBuilder(dateStr);
builder.append("\n\t").append(msg);
return builder.toString();
}
/**
* 上线消息处理
*
* @param clientName 客户端名称
* @return
*/
private String onlineMsgHandle(String clientName) {
String dateStr = formatter.format(LocalDateTime.now());
StringBuilder builder = new StringBuilder(dateStr);
builder.append(" ").append(clientName).append("已上线");
return builder.toString();
}
/**
* 下线消息处理
*
* @param clientName 客户端名称
* @return
*/
private String offlineMsgHandle(String clientName) {
String dateStr = formatter.format(LocalDateTime.now());
StringBuilder builder = new StringBuilder(dateStr);
builder.append(" ").append(clientName).append("已下线");
return builder.toString();
}
public static NIOGroupChatServer getInstance() {
return ServerInstance.instance;
}
static class ServerInstance {
private static final NIOGroupChatServer instance = new NIOGroupChatServer();
}
}
客户端代码
package nio.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import nio.MessageInfo;
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.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicInteger;
public class NIOGroupChatClient implements Runnable {
private static Selector selector;
private static SocketChannel client;
private static final String HOST = "127.0.0.1";
private AtomicInteger count = new AtomicInteger();
public NIOGroupChatClient(int port) {
try {
selector = Selector.open();
client = SocketChannel.open();
boolean isConnect = client.connect(new InetSocketAddress(HOST, port));
while (!isConnect && !client.finishConnect() && count.incrementAndGet() < 4){
System.out.printf("连接服务器失败,第%d次重新连接...\n",count.get());
isConnect = client.connect(new InetSocketAddress(HOST, port));
}
if (count.get() > 0 && !isConnect) throw new RuntimeException("连接服务器失败");
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public void inputMessage() {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.next();
sendInfo(s);
}
}
public void sendInfo(String msg) {
try {
client.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
int select = selector.select();
if (select > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
// todo 解决拆包粘包问题
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
String msg = new String(buffer.array(),"UTF-8");
messageHandle(msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void messageHandle(String msg) {
ObjectMapper mapper = new ObjectMapper();
try {
MessageInfo messageInfo = mapper.readValue(msg, MessageInfo.class);
switch (messageInfo.getType()) {
case ONLINE:
case OFFLINE:
System.out.println(messageInfo.getContent());
break;
case MESSAGE_INFO:
System.out.println(messageInfo.getUsername() + " " + messageInfo.getContent());
break;
default:
break;
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
NIOGroupChatClient client = new NIOGroupChatClient(9310);
new Thread(client).start();
client.inputMessage();
}
}
消息实体类
package nio;
public class MessageInfo {
private String content;
private String username;
private MessageTypeEnum type;
public MessageInfo() {
}
public MessageInfo(String content, String username, MessageTypeEnum type) {
this.content = content;
this.username = username;
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public MessageTypeEnum getType() {
return type;
}
public void setType(MessageTypeEnum type) {
this.type = type;
}
@Override
public String toString() {
return "MessageInfo{" +
"content='" + content + '\'' +
", username='" + username + '\'' +
", type=" + type +
'}';
}
}
消息类型枚举
package nio;
public enum MessageTypeEnum {
ONLINE,
OFFLINE,
MESSAGE_INFO
}