# 前情提要:
- 在上一篇文章BIO在聊天室項目中的演化中提到页畦,告知對方消息已經(jīng)發(fā)送完畢的方式有4種
- 關(guān)閉Socket連接
- 關(guān)閉輸出流内地,
socket.shutdownOutput();
- 使用標志符號接剩,借助字符流,
Reader.readLine()
舟铜,該方法會在讀取到\r
,\n
或者\r\n
時返回所讀取到的內(nèi)容菊霜。 - 通過指定本次發(fā)送的數(shù)據(jù)的字節(jié)大小。告知對方從輸入流中讀取指定大小的字節(jié)脊另。
本文使用第四種方案來實現(xiàn)聊天室
- 思路為:
- 客戶端在發(fā)送消息之前导狡,先計算出本次發(fā)送的數(shù)據(jù)量的字節(jié)大小,比如為
N
個字節(jié)偎痛。那么在向服務(wù)器發(fā)送數(shù)據(jù)的前旱捧,先約定好流中的前1個字節(jié)(或者前X
個字節(jié),根據(jù)自己項目的實際情況來決定)為本次發(fā)送的數(shù)據(jù)量的大小踩麦。 - 客戶端發(fā)送消息廊佩,先將計算出的字節(jié)大小N寫入輸出流,再將實際的內(nèi)容寫入輸出流靖榕。
- 服務(wù)端在獲取到輸入流之后标锄,根據(jù)約定,先讀取前
X
個字節(jié)茁计,根據(jù)這個字節(jié)的值可以知道料皇,本次發(fā)送的數(shù)據(jù)量的大小,那么在讀取數(shù)據(jù)時星压,只需要讀取后續(xù)的N
個字節(jié)即可践剂。
- 客戶端在發(fā)送消息之前导狡,先計算出本次發(fā)送的數(shù)據(jù)量的字節(jié)大小,比如為
- 溫馨提示: 注意看代碼注釋喲~
# 代碼實現(xiàn)
- 客戶端
/**
* @author futao
* @date 2020/7/6
*/
public class BioChatClient {
private static final Logger logger = LoggerFactory.getLogger(BioChatClient.class);
private static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor();
/**
* 啟動客戶端
*/
public void start() {
try { //嘗試連接到聊天服務(wù)器
Socket socket = new Socket("localhost", Constants.SERVER_PORT);
logger.debug("========== 成功連接到聊天服務(wù)器 ==========");
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
//從輸入流中讀取數(shù)據(jù)
SINGLE_THREAD_EXECUTOR.execute(() -> {
try {
while (true) {
String message = IOUtils.messageReceiver(inputStream);
logger.info("接收到服務(wù)端消息:[{}]", message);
}
} catch (IOException e) {
logger.error("發(fā)生異常", e);
}
});
while (true) {
//獲取用戶輸入的數(shù)據(jù)
String message = new Scanner(System.in).nextLine();
if (StringUtils.isBlank(message)) {
break;
}
//將內(nèi)容轉(zhuǎn)換為字節(jié)數(shù)組
byte[] contentBytes = message.getBytes(Constants.CHARSET);
//內(nèi)容字節(jié)數(shù)組的大小
int length = contentBytes.length;
//第一個字節(jié)寫入本次傳輸?shù)臄?shù)據(jù)量的大小
outputStream.write(length);
//寫入真正需要傳輸?shù)膬?nèi)容
outputStream.write(contentBytes);
//刷新緩沖區(qū)
outputStream.flush();
if (Constants.KEY_WORD_QUIT.equals(message)) {
//客戶端退出
SINGLE_THREAD_EXECUTOR.shutdownNow();
inputStream.close();
outputStream.close();
socket.close();
break;
}
}
} catch (IOException e) {
logger.error("發(fā)生異常", e);
}
}
public static void main(String[] args) {
new BioChatClient().start();
}
}
- 從輸入流中讀取指定大小的數(shù)據(jù)
/**
* 從輸入流中讀取指定大小的字節(jié)數(shù)據(jù)并轉(zhuǎn)換成字符串
*
* @param inputStream 輸入流
* @return 讀取到的字符串
* @throws IOException
*/
public static String messageReceiver(InputStream inputStream) throws IOException {
//本次傳輸?shù)臄?shù)據(jù)量的大小
int curMessageLength = inputStream.read();
byte[] contentBytes = new byte[curMessageLength];
//讀取指定長度的字節(jié)
inputStream.read(contentBytes);
return new String(contentBytes);
}
- 服務(wù)端
/**
* @author futao
* @date 2020/7/6
*/
public class BioChatServer {
private static final Logger logger = LoggerFactory.getLogger(BioChatServer.class);
/**
* 可同時接入的客戶端數(shù)量
*/
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
/**
* 當前接入的客戶端
*/
private static final Set<Socket> CLIENT_SOCKET_SET = new HashSet<Socket>() {
@Override
public synchronized boolean add(Socket o) {
return super.add(o);
}
@Override
public synchronized boolean remove(Object o) {
return super.remove(o);
}
};
/**
* 啟動服務(wù)端
*/
public void start() {
try {
//啟動服務(wù)器,監(jiān)聽端口
ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT);
logger.debug("========== 基于BIO的聊天室在[{}]端口啟動成功 ==========", Constants.SERVER_PORT);
while (true) {
//監(jiān)聽客戶端接入事件
Socket socket = serverSocket.accept();
THREAD_POOL.execute(() -> {
CLIENT_SOCKET_SET.add(socket);
int port = socket.getPort();
logger.debug("客戶端[{}]成功接入聊天服務(wù)器", port);
try {
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
while (true) {
//獲取到客戶端發(fā)送的消息
String message = IOUtils.messageReceiver(inputStream);
logger.info("接收到客戶端[{}]發(fā)送的消息:[{}]", port, message);
//客戶端是否退出
boolean isQuit = IOUtils.isQuit(message, socket, CLIENT_SOCKET_SET);
if (isQuit) {
socket.close();
break;
} else {
//消息轉(zhuǎn)發(fā)
IOUtils.forwardMessage(port, message, CLIENT_SOCKET_SET);
}
}
} catch (IOException e) {
logger.error("發(fā)生異常", e);
}
});
}
} catch (IOException e) {
logger.error("發(fā)生異常", e);
}
}
public static void main(String[] args) {
new BioChatServer().start();
}
}
- 客戶端下線與消息轉(zhuǎn)發(fā)
/**
* 判斷客戶端是否下線娜膘,并且將需要下線的客戶端下線
*
* @param message 消息
* @param socket 客戶端Socket
* @param clientSocketSet 當前接入的客戶端Socket集合
* @return 是否退出
* @throws IOException
*/
public static boolean isQuit(String message, Socket socket, Set<Socket> clientSocketSet) throws IOException {
boolean isQuit = StringUtils.isBlank(message) || Constants.KEY_WORD_QUIT.equals(message);
if (isQuit) {
clientSocketSet.remove(socket);
int port = socket.getPort();
socket.close();
logger.debug("客戶端[{}]下線", port);
}
return isQuit;
}
/**
* 轉(zhuǎn)發(fā)消息
*
* @param curSocketPort 當前發(fā)送消息的客戶端Socket的端口
* @param message 需要轉(zhuǎn)發(fā)的消息
* @param clientSocketSet 當前接入的客戶端Socket集合
*/
public static void forwardMessage(int curSocketPort, String message, Set<Socket> clientSocketSet) {
if (StringUtils.isBlank(message)) {
return;
}
for (Socket socket : clientSocketSet) {
if (socket.isClosed() || socket.getPort() == curSocketPort) {
continue;
}
if (socket.getPort() != curSocketPort) {
try {
OutputStream outputStream = socket.getOutputStream();
byte[] messageBytes = message.getBytes(Constants.CHARSET);
outputStream.write(messageBytes.length);
//將字符串編碼之后寫入客戶端
outputStream.write(messageBytes);
//刷新緩沖區(qū)
outputStream.flush();
} catch (IOException e) {
logger.error("消息轉(zhuǎn)發(fā)失敗", e);
}
}
}
}
# 測試一下~
- 服務(wù)端啟動逊脯,客戶端接入
image.png
- 客戶端接入
image.png
- 客戶端發(fā)送消息
image.png
- 服務(wù)端打印并轉(zhuǎn)發(fā)消息
image.png
- 聊天室內(nèi)的其他小伙伴收到服務(wù)器轉(zhuǎn)發(fā)的消息
image.png
- 小馬哥客戶端下線
image.png
- 服務(wù)器收到小馬哥的下線通知
image.png
# 總結(jié)
- 非常優(yōu)雅~??
# 注意
- 本文約定的是第一個字節(jié)為消息大小的標記,一個字節(jié)可以表示的最大值為255竣贪,所以一次最多傳輸255個字節(jié)军洼,如果超過這個值巩螃,會造成業(yè)務(wù)錯誤,需要注意匕争。
- 所以使用幾個字節(jié)來作為標識需要從業(yè)務(wù)的角度來考慮
- 一個字節(jié)8位避乏,可表示的最大值為 255 = 255B
- 二個字節(jié)16位,可表示的最大值為 65535 = 64KB
- 三個字節(jié)24位甘桑,可表示的最大值為 16777215 = 16MB
- 四個字節(jié)32位拍皮,可表示的最大值為 4294967295 = 4GB
- 以此類推....
# 系列文章
歡迎在評論區(qū)留下你看文章時的思考,及時說出跑杭,有助于加深記憶和理解铆帽,還能和像你一樣也喜歡這個話題的讀者相遇~
image.png