需求:
实现一个服务器,可多用户登录,用户可知道其他在线用户并与之一对一聊天,也可结束当前聊天和别的用户聊天。
实现:
1:使用ServerSocketChannel多路复用来做服务器,客户端连接注册时,用<用户ID,socketChannel>的键值对集合保存客户端的连接socketChannel对象。
2:客户端可通过查询服务端的键值对集合来获取所有在线用户信息。
3:客户端A选定用户B进行一对一聊天,发送聊天信息(聊天信息中包含聊天对象B的用户ID)。
4:服务端通过解析客户端A发送的报文,即可知道A发送给B的信息,通过键值对集合获取B的socketChannel对象,调用写方法,即可把信息发送至B客户端。
5:客户端A想结束与B的聊天,与客户端C聊天,也是与步骤4相同操作
代码:
多路复用简单例子:
参考我另一篇博客 :https://blog.csdn.net/DGH2430284817/article/details/119064573?
本博客需求实现代码:
下面代码实现的功能都是简单实现,实际项目中各个功能都是要根据实际情况修改的
服务端:
package com.dgh;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
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.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* NIO是面向缓冲区的
* User: lihaoquan
*/
public class Server {
private Selector selector;
/**
* 值得注意的是 Buffer 及其子类都不是线程安全的。
*/
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);//设置缓冲区大小
/**
* 保存登陆客户端socketChannel对象集合。
*/
private Map<String , SocketChannel> clientSocketList = new HashMap<>();
private SocketChannel socketChannel ;
/**
* 启动方法
* @throws IOException
*/
public void start() throws Exception {
//创建服务端的channel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//非阻塞方式
serverSocketChannel.configureBlocking(false);
//绑定IP
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
//创建选择器
selector = Selector.open();
//注册监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (!Thread.currentThread().isInterrupted()) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(!key.isValid()) {
continue;
}
if(key.isAcceptable()) {
accept(key);
}else if(key.isReadable()) {
read(key);
}
//去除本次keyIterator.next()的对象,但不会对下次遍历有影响
keyIterator.remove();
}
}
}
/**
* 读入
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
SocketChannel socketChannel = (SocketChannel) key.channel();
this.socketChannel = socketChannel;
Socket socket = socketChannel.socket();
this.readBuffer.clear();
int numRead;
try {
/**
* 获取客户点的read操作读入数据块数量
*/
numRead = socketChannel.read(readBuffer);
}catch (Exception e) {
key.cancel();
socketChannel.close();
return;
}
if(numRead > 0) {
//接收客户端信息
String recMsg = new String(readBuffer.array() ,0 , numRead );
System.out.println("收到客户端 "+socket.getInetAddress()+"信息:" +recMsg);
//返回客户端信息
String sendMsg = "";
if (recMsg.startsWith("1:")){ //模拟简易登陆功能,不校验。可在实际项目中自行写逻辑
String[] recMsgSplit = recMsg.split("1:");
//将登陆的用户的socketChannel添加到结合中
clientSocketList.put(recMsgSplit[1],socketChannel);
sendMsg = "用户:" + recMsgSplit[1]+" 登陆成功!";
}else if(recMsg.startsWith("2:")){
sendMsg = "所有在线用户:" + clientSocketList.keySet();
}else if(recMsg.startsWith("3:")){
String[] recMsgSplit = recMsg.split("3:");
sendMsg = recMsgSplit[1].trim();
if (sendMsg.contains("-")){
String[] recMsgSplit2 = recMsgSplit[1].trim().split("-");
if (clientSocketList.containsKey(recMsgSplit2[0])){
this.socketChannel = clientSocketList.get(recMsgSplit2[0]);
sendMsg = recMsgSplit2[1].trim();
}else {
sendMsg = "用户 " +recMsgSplit2[0] +" 不存在!";
}
}else {
sendMsg = "聊天报文解析失败";
}
}else {
sendMsg = "报文解析失败";
}
ByteBuffer writeBuffer = ByteBuffer.wrap(sendMsg.getBytes());
this.socketChannel.write(writeBuffer);
}else {
System.out.println("客户端断开连接:" +socket.getInetAddress());
}
}
/**
* 接收
* @param key
* @throws IOException
*/
public void accept(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel
= (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector,SelectionKey.OP_READ);
System.out.println("成功连接客户端:"+ serverSocketChannel.getLocalAddress());
}
public static void main(String[] args) throws Exception{
new Server().start();
}
}
客户端:
package com.dgh;
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.Set;
/**
* User: lihaoquan
*/
public class Client {
//服务器返回信息
public static String receiveMsg ="";
/**
* 启动开关
* @throws IOException
*/
public void start() throws Exception {
//通过静态工厂生产客户端的channnel
SocketChannel socketChannel = SocketChannel.open();
//设置客户端请求为非阻塞方式
socketChannel.configureBlocking(false);
//绑定IP
socketChannel.connect(new InetSocketAddress(8080));
//创建选择器
Selector selector = Selector.open();
//注册监听事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//键盘输入
Scanner scanner = new Scanner(System.in);
//单独开个线程让客户端输入信息到服务端
String finalReceiveMsg = receiveMsg;
new Thread(()->{
boolean isON = false ; //判断用户是否已登陆
while (true) {
try {
System.out.println("请选择功能(输入对应数字点击回车)");
System.out.println("1:登陆");
System.out.println("2:聊天");
String message = scanner.nextLine();
if ("1".equals(message)){//登陆操作
if (!isON){
System.out.println("请输入登录的用户ID");
isON = true;
message = scanner.nextLine();
message = "1:" + message;
}else {
System.out.println("操作失败,您已登陆!");
continue;
}
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(writeBuffer);
}else if ("2".equals(message)){
message = "2:" ;//获取所有在线用户信息
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(writeBuffer);
//选择聊天对象
System.out.println("请输入想要聊天的用户ID,点击回车!");
String chatUser = scanner.nextLine();
if ( receiveMsg.contains(chatUser)){//判断输入的用户ID是否存在
System.out.println("===========开始与 " + message +" 的聊天(输入exit退出聊天)==================");
while (true){
message = scanner.nextLine();
if ("exit".equals(message)){
//退出聊天
System.out.println("===========退出与 " + chatUser +" 的聊天====================");
break;
}
message = "3:" +chatUser +"-" +message ;
writeBuffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(writeBuffer);
}
}else{
System.out.println("输入错误,用户不存在,请重新输入!"+ receiveMsg + "-");
continue;
}
}else {
System.out.println("输入错误,请重新输入!");
continue;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if(key.isConnectable()) {
socketChannel.finishConnect();
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("客户端已经连上服务器端 ");
break;
}else if(key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(48);
byteBuffer.clear();
long byteRead = clientChannel.read(byteBuffer);
if (byteRead == -1){
clientChannel.close();
}else{
byteBuffer.flip();
receiveMsg = new String(byteBuffer.array(),0,byteBuffer.limit());
//接收来自服务器的消息
System.out.println( receiveMsg);
}
}
}
}
}
public static void main(String[] args) throws Exception{
new Client().start();
}
}
启动 服务端和两个客户端:
客户端先后用各自ID登录,然后聊天,具体操作结果如下:
客户端1:
客户端2:
服务端:
效果:
上述代码实现了多人登录和聊天功能,并且是可以和自己聊天的,而且连接是单向的,不需要对方同意也可发送信息,因为所有客户端在注册时对应的socket对象已经保存在服务端,并且都是保持长链接。