特别說明
我實作的這個C/S模式的聊天室項目,主要是為了友善大家學習Java NIO的。由于NIO的優勢在于單機能處理的并發連接配接數高,是以特别适合用于聊天程式的服務端。
為什麼使用Java Swing來做圖形界面呢?我們大家都知道,Java Swing現在已經過時了,并且Java的優勢不在于圖形界面,但我的需求并不需要漂亮美觀的界面,并且Java語言實作C端界面的首選就是Java Swing了,隻不過我隻用了相對簡單的圖形元件及元件互動,但這也足夠了。
閱讀本文需要一點NIO的前置知識,大家可以看一下與本文同專欄的的這篇文章: Java NIO三大元件Buffer、Channel、Selector保姆級教程 附聊天室核心代碼
本文隻粘貼了項目的核心代碼,如希望了解項目全貌,請點選上述連結下載下傳項目,零積分便可下載下傳。
如遇到項目啟動問題或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為預設的服務端位址,可以修改成你自己的
用戶端核心代碼分析
入口ChatClientBootstrap
即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<>();
/**
* 存儲與好友聊天資訊
*/
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方法
}