我实现的这个C/S模式的聊天室项目,主要是为了方便大家学习Java NIO的。由于NIO的优势在于单机能处理的并发连接数高,因此特别适合用于聊天程序的服务端。
为什么使用Java Swing来做图形界面呢?我们大家都知道,Java Swing现在已经过时了,并且Java的优势不在于图形界面,但我的需求并不需要漂亮美观的界面,并且Java语言实现C端界面的首选就是Java Swing了,只不过我只用了相对简单的图形组件及组件交互,但这也足够了。
阅读本文需要一点NIO的前置知识,大家可以看一下与本文同专栏的的这篇文章: Java NIO三大组件Buffer、Channel、Selector保姆级教程 附聊天室核心代码
项目代码已上传至CSDN,链接地址为:https://download.csdn.net/download/xl_1803/85162646
本文只粘贴了项目的核心代码,如希望了解项目全貌,请点击上述链接下载项目,零积分便可下载。
如遇到项目启动问题或bug,可在评论区留言。
效果演示话不多说,先来演示一下我做的聊天程序吧!虽然界面比较简洁,并且只实现了核心功能,但相较而言这些都不是重点!重点在于服务端与客户端之间的NIO通信流程,稍后我会为大家分析讲解代码。
CS模式聊天程序演示
项目结构├─nio-chat-client 客户端项目
│ │ pom.xml
│ └─src
│ └─main
│ ├─java
│ │ └─com
│ │ └─bobo
│ │ │ ChatClientBootstrap.java 客户端启动类
│ │ ├─entity
│ │ │ FriendMsg.java 存放好友信息
│ │ ├─handler 放各种处理器
│ │ │ EventHandler.java 图形组件事件处理器
│ │ │ IOHandler.java 客户端IO处理器
│ │ │ UIHandler.java 图形组件处理器
│ │ │
│ │ └─ui
│ │ MainUI.java 继承自JFrame,程序主界面
│ └─resources
│ trumpet.jpeg 小喇叭图标
│
├─nio-chat-common 放服务端与客户端公共代码
│ │ pom.xml
│ └─src
│ └─main
│ └─java
│ └─com
│ └─bobo
│ ├─constant
│ │ CommonConstant.java 公共常量
│ ├─entity
│ │ ChatMsg.java 服务端与客户端通信的实体类
│ └─io 下面两个类分别用于读数据与写数据,用分隔符解决TCP粘拆包问题
│ ChatBufferReader.java
│ ChatMsgWrapper.java
│
└─nio-chat-server 服务端项目
│ pom.xml
└─src
└─main
└─java
└─com
└─bobo
│ ChatServerBootstrap.java 服务端启动类
└─handler
ServiceHandler.java 服务端处理器,NIO核心代码都在这个类里
主界面结构
说明:
- localhost:6666为默认的服务端地址,可以修改成你自己的
即main方法所在处。
public class ChatClientBootstrap {
public static void main(String[] args) {
// new一个客户端主界面实例
MainUI mainUI = new MainUI();
// 注册事件
EventHandler.doRegister(mainUI);
}
}
NIO通信处理IOHandler
用于处理客户端发送消息至服务端,以及接收从服务端发来的消息等。客户端这边也是用了NIO模式,将客户端对应的SocketChannel注册到Selector上,并在单独的一个线程里无线循环处理客户端的读事件,并通过UIHandler类的各种方法将处理结果渲染到UI界面,实现界面与数据的联动。
public class IOHandler {
private static IOHandler ioHandler = new IOHandler();
private IOHandler(){}
public static IOHandler getHandler(){
return ioHandler;
}
// 存储好友的数据
private Map<String,FriendMsg> friendsData = new LinkedHashMap<>();
// 当前客户端的ID
private String selfId;
// 当前客户端的SocketChannel
private SocketChannel socketChannel;
// 读缓冲,以\n为分隔符读取消息,用于解决TCP粘包拆包问题
private ChatBufferReader chatBufferReader = new ChatBufferReader();
// 给消息的最后添加一个分隔符\n
private ChatMsgWrapper chatMsgWrapper = new ChatMsgWrapper();
public void doConnect(MainUI mainUI) throws IOException {
Selector selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 获取服务端ip:port
try {
String[] arr = mainUI.getServer().getText().split(":");
if(arr.length != 2){
throw new RuntimeException("服务端地址格式错误");
}
socketChannel.connect(new InetSocketAddress(arr[0], Integer.valueOf(arr[1])));
} catch (Exception e) {
e.printStackTrace();
UIHandler.alert(mainUI,e.getMessage(),"提示",JOptionPane.ERROR_MESSAGE);
return;
}
socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ, ByteBuffer.allocate(CommonConstant.BUFFER_SIZE));
for (;;){
int num = selector.select();
if(num <= 0){
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isConnectable()){
while(!socketChannel.finishConnect()){
UIHandler.setText(mainUI.getBottom(),"客户端连接中...");
}
selfId = socketChannel.getLocalAddress().toString().substring(1);
// 连接成功后,渲染界面
UIHandler.setText(mainUI.getBottom(),"已成功连接服务器");
UIHandler.alert(mainUI,"成功连接服务器","提示",JOptionPane.INFORMATION_MESSAGE);
UIHandler.append(mainUI.getGroupArea(),"成功连接服务器,您现在可以发送群聊消息");
UIHandler.setText(mainUI.getId(),selfId);
UIHandler.setVisible(mainUI.getConnect(),false);
UIHandler.setEditable(mainUI.getServer(),false);
}else if (key.isReadable()){
// 缓冲读,以\n为分隔符读取消息,可解决TCP粘包拆包问题
String msg = chatBufferReader.readMsg(key);
ChatMsg chatMsg = JSONObject.parseObject(msg, ChatMsg.class);
if(CommonConstant.MSG_TYPE_SYNC_TO_NEW_CLIENT == chatMsg.getType()){
// 新客户端(自己)上线,获取服务端发过来的所有好友信息
Map<String,String> map = JSONObject.parseObject(chatMsg.getMsg(), Map.class);
map.forEach((subject,name)->{
// 初始化FriendMsg
friendsData.put(subject,new FriendMsg(subject,name,0,new ArrayList()));
});
// 重新加载好友列表
UIHandler.reloadFriends(mainUI);
}else if(CommonConstant.MSG_TYPE_NOTICE_HAS_NEW_CLIENT == chatMsg.getType()){
// 有新的好友上线
UIHandler.setText(mainUI.getBottom(),String.format("有新的好友[%s]已上线",chatMsg.getMsg()));
// 初始化FriendMsg
friendsData.put(chatMsg.getMsg(),new FriendMsg(chatMsg.getMsg(),"",0,new ArrayList()));
// 重新加载好友列表
UIHandler.reloadFriends(mainUI);
}else if(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME == chatMsg.getType()){
// 好友修改了昵称
UIHandler.setText(mainUI.getBottom(),
String.format("好友[%s]修改了昵称[%s]",chatMsg.getSubject(),chatMsg.getMsg()));
// 修改FriendMsg
friendsData.get(chatMsg.getSubject()).setName(chatMsg.getMsg());
// 重新加载好友列表
UIHandler.reloadFriends(mainUI);
// 如果正在与该好友聊天,则更新私聊标题
UIHandler.updatePrivateTitle(mainUI,chatMsg.getSubject(),false);
}else if(CommonConstant.MSG_TYPE_RECV_GROUP == chatMsg.getType()){
// 接收群聊消息
UIHandler.append(mainUI.getGroupArea(),UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
}else if(CommonConstant.MSG_TYPE_RECV_PRIVATE == chatMsg.getType()){
// 接收私聊消息
FriendMsg friendMsg = friendsData.get(chatMsg.getSubject());
// 将消息记录到friendMsg的msgList中
friendMsg.getMsgList().add(UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
if(mainUI.getPrivateTitle().getText().contains(chatMsg.getSubject())){
// 当前正在与该好友聊天,则还要将消息追加到私聊窗口
UIHandler.append(mainUI.getPrivateArea(),UIHandler.buildSessionMsg(chatMsg.getSubject(),chatMsg.getMsg()));
}else{
// 当前没有在于该好友聊天,则未读消息个数加1
friendMsg.setUnreadCount(friendMsg.getUnreadCount()+1);
// 底部通知栏添加通知
UIHandler.setText(mainUI.getBottom(),UIHandler.buildSessionMsg(chatMsg.getSubject(),"发来了一条新消息"));
// 重新加载好友列表
UIHandler.reloadFriends(mainUI);
}
}else if(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE == chatMsg.getType()){
// 好友下线,底部通知栏添加通知
UIHandler.setText(mainUI.getBottom(),UIHandler.buildSessionMsg(chatMsg.getSubject(),"下线"));
// 如果正在与该好友聊天,则更新私聊标题
UIHandler.updatePrivateTitle(mainUI,chatMsg.getSubject(),true);
// 移除
friendsData.remove(chatMsg.getSubject());
// 重新加载好友列表
UIHandler.reloadFriends(mainUI);
}
}
// 最后移除此次发生处理的selectionKey,防止事件重复处理
iterator.remove();
}
}
}
/**
* 写数据,chatMsgWrapper实现了自定义消息格式(\n)
*/
public void writeMsg(int type,String subject,String text){
try {
socketChannel.write(chatMsgWrapper.wrap(new ChatMsg(type,subject,text)));
} catch (IOException e) {
e.printStackTrace();
}
}
public String getSelfId() {
return selfId;
}
public Map<String, FriendMsg> getFriendsData() {
return friendsData;
}
}
服务端核心代码分析
入口ChatServerBootstrap
即main方法所在处,可随意修改绑定的地址与端口号,但也要对应修改客户端连接时指定的服务端地址。
public class ChatServerBootstrap {
public static void main(String[] args) throws IOException {
new ServiceHandler().handle("localhost",6666);
}
}
NIO通信处理ServiceHandler
public class ServiceHandler {
/**
* 存储客户端昵称
*/
private Map<String,String> clientNameMap = new HashMap();
// 读缓冲,以\n为分隔符读取消息,用于解决TCP粘包拆包问题
private ChatBufferReader chatBufferReader = new ChatBufferReader();
// 给消息的最后添加一个分隔符\n
private ChatMsgWrapper chatMsgWrapper = new ChatMsgWrapper();
public void handle(String ip,int port) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(ip,port));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("聊天服务器已就绪...");
for(;;){
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()){
SocketChannel newSocketChannel = serverSocketChannel.accept();
newSocketChannel.configureBlocking(false);
// 获取新客户端id
String id = newSocketChannel.getRemoteAddress().toString().substring(1);
System.out.println(String.format("客户端上线[%s]",id));
// 先通知其他所有人,有新的客户端上线
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != newSocketChannel){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_HAS_NEW_CLIENT,null,id)));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
}
}
// 向新客户端同步在线好友
ByteBuffer att = ByteBuffer.allocate(CommonConstant.BUFFER_SIZE);
att.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_SYNC_TO_NEW_CLIENT,null,JSONObject.toJSONString(clientNameMap))));
newSocketChannel.register(selector,SelectionKey.OP_READ | SelectionKey.OP_WRITE,att);
// 初始化新客户端的昵称
clientNameMap.put(id,"");
}else if(selectionKey.isReadable()){
SocketChannel channel = (SocketChannel)selectionKey.channel();
String msg = null;
try {
msg = chatBufferReader.readMsg(selectionKey);
} catch (IOException e) {
// 通知所有客户端下线
clientNameMap.remove(e.getMessage());
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE,e.getMessage(),e.getMessage())));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
}
}
iterator.remove();
continue;
}
ChatMsg chatMsg = JSONObject.parseObject(msg, ChatMsg.class);
String id = channel.getRemoteAddress().toString().substring(1);
if(CommonConstant.MSG_TYPE_MODIFY_NAME == chatMsg.getType()){
// 通知其他人有好友修改了昵称
clientNameMap.put(id,chatMsg.getMsg());
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME,id,chatMsg.getMsg())));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
}
}
}else if(CommonConstant.MSG_TYPE_SEND_GROUP == chatMsg.getType()){
// 转发群聊消息
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_GROUP,id,chatMsg.getMsg())));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
}
}
}else if(CommonConstant.MSG_TYPE_SEND_PRIVATE == chatMsg.getType()){
// 转发私聊消息
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != channel
&& ((SocketChannel) selectableChannel).getRemoteAddress().toString().substring(1).equals(chatMsg.getSubject())){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_PRIVATE,id,chatMsg.getMsg())));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
break;
}
}
}
}else if(selectionKey.isWritable()){
// 写事件
SocketChannel channel = (SocketChannel)selectionKey.channel();
ByteBuffer att = (ByteBuffer) selectionKey.attachment();
att.flip();
channel.write(att);
selectionKey.interestOps(SelectionKey.OP_READ);
}
// 最后移除此次发生处理的selectionKey,防止事件重复处理
iterator.remove();
}
}
}
}
专题
如何解决TCP粘拆包问题?
解决此类问题,有许多的方法可以使用,如消息定长(Long类型消息固定为8个字节、Int类型消息固定为4个字节、随机大小如200字节不够则空格)、分隔符(\n、\r\n、其它自定义分隔符)等。
本项目基于分隔符\n的方案,实现消息的读取与写入。
消息的写入在ChatMsgWrapper.java类中,如下代码所示。
public class ChatMsgWrapper {
public ByteBuffer wrap(ChatMsg chatMsg){
String jsonString = JSONObject.toJSONString(chatMsg);
return ByteBuffer.wrap((jsonString+"\n").getBytes(StandardCharsets.UTF_8));
}
}
由此可见,通过wrap方法,会在每个原生消息chatMsg的后面追加上一个换行符。
那怎么读取呢?别着急,消息的读取在ChatBufferReader.java类中,如下代码所示。
public class ChatBufferReader {
/**
* 缓冲区
*/
private Map<SocketChannel,String> msgBuffer = new HashMap();
public String readMsg(SelectionKey selectionKey) throws IOException {
SocketChannel channel = (SocketChannel)selectionKey.channel();
ByteBuffer buffer = (ByteBuffer)selectionKey.attachment();
StringBuilder sb = new StringBuilder();
try {
String msg = "";
// 循环读,读到有\n截止
while (!msg.contains("\n")){
buffer.clear();
channel.read(buffer);
msg = new String(buffer.array(), 0, buffer.position(), StandardCharsets.UTF_8);
sb.append(msg);
}
} catch (IOException e) {
String host = channel.getRemoteAddress().toString().substring(1);
System.out.println(String.format("远程机器下线[%s]",host));
selectionKey.cancel();
channel.close();
throw new IOException(host);
}
// 此次读到的消息
String message = sb.toString();
// 将缓冲区中上次读到的消息放到前面来
if (msgBuffer.containsKey(channel) && null != msgBuffer.get(channel)){
message = msgBuffer.get(channel)+message;
}
// 这里取第一个\n前面的数据作为本次读取的消息,后面的按原样放到缓冲区
// 放到缓冲区
msgBuffer.put(channel,message.length()<=message.indexOf("\n")+2?"":message.substring(message.indexOf("\n")+2));
// 将本次读取的消息返回出去
return message.substring(0,message.indexOf("\n"));
}
}
通过while (!msg.contains("\n"))循环,我们可能会读好几次,直到读到的数据有换行符为止,为什么呢?因为换行符代表一个消息的结束,如果没有换行符就代表还没有读到消息的末尾,那就肯定还要继续读啊。
读完了之后,怎么处理呢?由于TCP粘包机制,因此读到的数据可能会长这样:abcdefg\nhijk,\n代表了一个消息的结束,那么abcdefg就是一条完整的消息,但hijk是属于下一条消息的,只不过被粘在一起了,我们需要做的是将hijk放到缓冲区缓存起来,然后将abcdefg返回出去(代表本次读到的消息)。
缓存起来怎么办呢?难道就不管了吗?当然不是!当我们尝试读下一条消息时,读到的数据可能是这样的:lmn\nopq,通过上面的论述,我们已经知道了opq是属于下一条消息的,要放缓冲区里,但lmn就是一条完整的消息吗?当然不是,lmn是消息的下半部分,那上半部分在哪?在缓冲区呀!
这就是缓冲区的作用,缓冲区可以将消息的上半部分先缓存起来,等到消息的下半部分也读到了,再组合在一块儿,就是一条完整的消息了。
服务端和客户端通信的消息类型有很多种,如何区分?这个好办,用一个实体类就能搞定。
ChatMsg.java类定义如下。
public class ChatMsg {
private int type;
private String subject;
private String msg;
public ChatMsg() {
}
public ChatMsg(int type, String subject, String msg) {
this.type = type;
this.subject = subject;
this.msg = msg;
}
// 省略getter/setter方法
}
ChatMsg就是封装的服务端与客户端之间通信的消息对象,并且通过type属性区分消息类型,消息类型定义在CommonConstant.java类中。
public interface CommonConstant {
/**
* 0 向新客户端同步在线好友
* 1 通知有新客户端上线
* 2 修改昵称
* 3 通知其它人修改昵称
* 4 发送群聊消息
* 5 转发群聊消息
* 6 发送私聊消息
* 7 转发私聊消息
* 8 通知有客户端下线
*/
int MSG_TYPE_SYNC_TO_NEW_CLIENT = 0; // server->client
int MSG_TYPE_NOTICE_HAS_NEW_CLIENT = 1; // server->client
int MSG_TYPE_MODIFY_NAME = 2; // client->server
int MSG_TYPE_NOTICE_OTHER_CLIENT_MODIFY_NAME = 3; // server->client
int MSG_TYPE_SEND_GROUP = 4; // client->server
int MSG_TYPE_RECV_GROUP = 5; // server->client
int MSG_TYPE_SEND_PRIVATE = 6; // client->server
int MSG_TYPE_RECV_PRIVATE = 7; // server->client
int MSG_TYPE_NOTICE_OTHER_CLIENT_OFFLINE = 8; // server->client
}
到时候,无论是客户端读到了服务端发来的消息,还是服务端读到了客户端发来的消息,都可以先将消息反序列化为ChatMsg对象,然后拿到type属性做if判断,确定本次要处理的是何种业务。
写事件有什么用?我直接写不行嘛,为啥还需要等到写事件发生才能写呢?在ServiceHandler.java类中,会处理客户端发来的消息,比如客户端A发来群聊的消息,那么服务端需要将该消息写给除客户端A之外的所有客户端,即消息的转发。
那么在这里,我们服务端不是直接写数据的,而是将数据先放到缓冲区里,然后触发一个写事件,在写事件发生时才将数据写数据。
群聊业务处理代码如下所示。
// 转发群聊消息
for (SelectionKey otherKey : selector.keys()) {
SelectableChannel selectableChannel = otherKey.channel();
if(selectableChannel instanceof SocketChannel && selectableChannel != channel){
ByteBuffer otherAtt = (ByteBuffer) otherKey.attachment();
otherAtt.clear();
otherAtt.put(chatMsgWrapper.wrap(new ChatMsg(CommonConstant.MSG_TYPE_RECV_GROUP,id,chatMsg.getMsg())));
// 触发write事件
otherKey.interestOps(otherKey.interestOps() | SelectionKey.OP_WRITE);
}
}
处理写事件代码如下所示。
if(selectionKey.isWritable()){
// 写事件
SocketChannel channel = (SocketChannel)selectionKey.channel();
ByteBuffer att = (ByteBuffer) selectionKey.attachment();
att.flip();
channel.write(att);
selectionKey.interestOps(SelectionKey.OP_READ);
}
那么不直接写,而是等写事件发生才写,这么做的好处是什么呢?
写事件的作用:没有写事件也可以,直接将用户buffer数据拷贝到socket发送缓冲区,但是高并发情况下(用户buffer频繁往socket buffer拷贝数据)以及网络环境很差的情况下(socket 发送缓冲区将数据发出去的速度很慢),socket发送缓冲区很快就满了,这样最终会导致CPU利用率100%。
因此可以用写事件优化,当socket发送缓冲区没有满时,即有空闲会触发可写事件,此时才去写数据,而当满了的时候,就不会触发可写事件,这样能让CPU歇一歇。
另外,在写事件处理结束之后,一定要调用一下selectionKey.interestOps(SelectionKey.OP_READ)取消写事件,如果不调用则selectionKey.isWritable()一直会返回true,而实际并没有可写的数据,这样会使CPU空转。
私聊记录如何保存?IOHandler.java中有一个属性,如下所示。
private Map<String,FriendMsg> friendsData = new LinkedHashMap<>();
该属性用于保存客户端的好友信息。Map的key为好友的id(ip:port),而FriendMsg如下所示。
/**
* 存储与好友聊天信息
*/
public class FriendMsg {
/**
* 好友的id(ip:port)
*/
private String subject;
/**
* 好友的昵称
*/
private String name;
/**
* 未读消息个数
*/
private int unreadCount;
/**
* 存储我与该好友的聊天记录
*/
private List<String> msgList;
public FriendMsg(){}
public FriendMsg(String subject, String name, int unreadCount, List<String> msgList) {
this.subject = subject;
this.name = name;
this.unreadCount = unreadCount;
this.msgList = msgList;
}
// 省略getter/setter方法
}
本客户端与对应好友的私聊记录就保存在msgList属性中,需要注意的是私聊记录是保存在内存中的,一旦好友下线或本客户端下线(进程终止),私聊记录都会丢失。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)