文章目录
前面的聊天室已经可以实现多个客户端登录和群聊,服务端负责保存在线用户并把收到的消息广播出去。但这个版本还有一个比较明显的问题:所有消息默认都会发给其他在线用户,不能指定某个人单独聊天。如果直接在消息里写:
text
@小明 你好
然后让服务端自己判断有没有 @,虽然也能做,但这种方式不太稳定。因为普通聊天内容里本身也可能出现 @,以后再增加表情、文件等消息后,服务端会越来越难判断一串数据到底代表什么。所以这次开始给聊天室重新设计一套消息协议。
之前使用的协议比较简单:
text
消息长度 + 消息内容
现在需要先告诉服务端:
text
这是一条什么消息
然后服务端才能决定后面应该怎么读取。先规定:
text
1 → 私聊
2 → 群聊
消息类型只需要占一个字节。
一、自定义消息协议
对于私聊来说,服务端不仅要知道消息内容,还要知道消息发给谁。所以私聊消息按照下面的顺序发送:
text
消息类型
好友名称长度
好友名称
消息长度
消息内容
例如发送:
text
@小明 hello
客户端先从字符串中提取:
text
好友名称:小明
消息内容:hello
然后按协议写入 Socket。代码中首先判断第一个字符是不是 @:
java
char c1 = message.charAt(0);
if (c1 == '@') {
// 私聊
}
接着找到用户名和消息之间的空格:
java
int index = message.indexOf(" ");
截取好友名称:
java
String friendName = message.substring(1, index);
再拿到真正的聊天内容:
java
String msg = message.substring(index + 1);
例如:
text
@小明 今天上课吗
最后拆出来就是:
text
friendName = 小明
msg = 今天上课吗
然后按照已经约定好的格式进行发送:
java
os.write(1);
int fn = friendName.getBytes().length;
os.write(fn);
os.write(friendName.getBytes());
int msglen = msg.getBytes().length;
os.write(msglen);
os.write(msg.getBytes());
也就是说,一条私聊消息在网络中的结构大致是:
text
┌──────┬──────────┬────────┬──────────┬────────┐
│ 类型 │ 用户名长度 │ 用户名 │ 消息长度 │ 消息正文 │
└──────┴──────────┴────────┴──────────┴────────┘
群聊现在还没有群组 ID,所以暂时简单一些:
java
os.write(2);
byte[] msgs = message.getBytes();
int len = msgs.length;
os.write(len);
os.write(msgs);
结构就是:
text
消息类型 + 消息长度 + 消息正文
这次真正比较重要的不是记住这些数字,而是理解一个思路:发送端怎么写,接收端就必须按照完全相同的顺序去读。
二、服务端解析私聊和群聊
客户端把消息结构改了以后,服务端的 receive() 也必须跟着改。
原来服务端一上来就读取消息长度,现在第一步先读取消息类型:
java
int type = is.read();
然后根据:
text
1 或 2
决定后面应该怎么读取。如果:
java
type == 1
说明收到的是私聊。服务端继续读取好友名称长度:
java
int idlen = is.read();
再根据长度读取好友名称:
java
byte[] id = new byte[idlen];
is.read(id);
String friendName = new String(id);
然后继续读取消息长度和消息正文:
java
int msglen = is.read();
byte[] msgs = new byte[msglen];
is.read(msgs);
String msg = new String(msgs);
到这里,服务端实际上已经知道:
text
这是一条私聊消息
目标用户是谁
真正的消息是什么
如果收到的是群聊:
java
type == 2
就按照原来的消息长度和正文格式读取。当前代码为了方便后面的处理,把解析结果暂时拼成字符串返回:
java
"1#" + friendName + "#" + msg
或者:
java
"2# #"+msg
然后在消息处理线程里再次:
java
String[] datas = msg.split("#");
取出:
java
String type = datas[0];
String friendName = datas[1];
String msgs = datas[2];
三、私聊消息的定向转发
群聊比较简单,还是遍历所有在线用户发送,私聊就不能广播了。服务端已经有:
java
ArrayList<ClientUser> SocketList
其中每一个 ClientUser 都保存了:
text
account
socket
所以收到私聊以后,只需要遍历在线用户:
java
for (int i = 0;
i < SocketList.size();
i++) {
ClientUser cu1 = SocketList.get(i);
if (cu1.account.equals(friendName)) {
msgs = cu.account + "说:" + msgs;
send(msgs,cu1.socket.getOutputStream());
break;
}
}
例如:
text
A → @B 你好
服务端解析之后:
text
发送者:A
目标用户:B
内容:你好
然后只找到 B 对应的 Socket:
text
A
\
Server → B
C 不会收到这条消息。而普通消息仍然使用原来的广播。