推薦:Java網絡程式設計彙總
Java網絡程式設計-Socket程式設計初涉二(基于BIO模型的簡易多人聊天室)
要求
我們要實作一個基于BIO模型的簡易多人聊天室,不能像上一個版本一樣(Java網絡程式設計-Socket程式設計初涉一(簡易用戶端-伺服器)),當伺服器與某個用戶端連接配接成功後,它們在進行資料互動過程中,其他用戶端的連接配接請求,伺服器是不會響應的,我們這裡要使用BIO模型來改善這一點。
所謂多人聊天室,就是某個使用者發送的消息,其他使用者也是可以看見的。
什麼是BIO模型?
BIO模型與僞異步I/O模型
多人聊天室的時序圖
時序圖如下圖所示,先不糾結時序圖是否标準,等下解釋代碼時,可以與時序圖結合起來進行了解。
代碼
伺服器代碼
先給出全部代碼,然後分子產品進行解釋。
ChatServer類
是多人聊天室的伺服器部分。
package bio.chatroom.server;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class ChatServer {
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 把用戶端的port當作用戶端的id
private Map<Integer , Writer> connectedClients;
public ChatServer(){
connectedClients = new HashMap<>();
}
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
connectedClients.put(port , writer);
System.out.println("用戶端["+port+"]已連接配接到伺服器");
}
}
public synchronized void removeClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
if(connectedClients.containsKey(port)){
connectedClients.get(port).close();
connectedClients.remove(port);
System.out.println("用戶端["+port+"]已斷開連接配接");
}
}
}
public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
// 發送消息的端口
int sendMessagePort = socket.getPort();
for(Integer port : connectedClients.keySet()){
if(!port.equals(sendMessagePort)){
Writer writer = connectedClients.get(port);
writer.write(fwdMsg);
writer.flush();
}
}
}
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public synchronized void close(){
if(serverSocket != null){
try {
serverSocket.close();
System.out.println("關閉了ServerSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
// 建立ServerSocket,綁定和監聽端口
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("啟動伺服器,監聽端口"+DEFAULT_PORT+"...");
while(true){
// 等待用戶端連接配接
Socket socket = serverSocket.accept();
// 建立ChatHandler線程
new Thread(new ChatHandler(this , socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
}
子產品一
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 把用戶端的port當作用戶端的id
private Map<Integer , Writer> connectedClients;
public ChatServer(){
connectedClients = new HashMap<>();
}
DEFAULT_PORT
是伺服器建立的
ServerSocket
需要綁定、監聽的端口,
QUIT
是用于判斷使用者是否準備退出,
connectedClients
相當于存儲線上使用者的容器(實際上并沒有存儲使用者),而是以使用者的
port
為
key
,以伺服器向該
port
的使用者發送消息的
Writer
為
value
,組成的
Map
,這樣友善伺服器轉發某個使用者的消息給其他使用者。
構造器
ChatServer()
要初始化
connectedClients
,因為伺服器啟動後,就可能馬上需要存儲上線的使用者。
子產品二
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
connectedClients.put(port , writer);
System.out.println("用戶端["+port+"]已連接配接到伺服器");
}
}
public synchronized void removeClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
if(connectedClients.containsKey(port)){
connectedClients.get(port).close();
connectedClients.remove(port);
System.out.println("用戶端["+port+"]已斷開連接配接");
}
}
}
public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
// 發送消息的端口
int sendMessagePort = socket.getPort();
for(Integer port : connectedClients.keySet()){
if(!port.equals(sendMessagePort)){
Writer writer = connectedClients.get(port);
writer.write(fwdMsg);
writer.flush();
}
}
}
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public synchronized void close(){
if(serverSocket != null){
try {
serverSocket.close();
System.out.println("關閉了ServerSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
這些方法的作用也正如方法名一樣。為了一定的線程安全,有些需要涉及增删改查的方法直接用
synchronized
修飾(我們現在不用過分糾結高性能、線程安全等問題)。
-
:存儲新上線的使用者。addClient()
-
:移除退出的使用者。removeClient()
-
:把某個使用者的消息轉發給其他的使用者。forwardMessage()
-
:判斷使用者是否準備退出。readyToQuit()
-
:關閉資源。close()
具體實作應該比較簡單,相信大家都能看懂。
子產品三
public void start(){
try {
// 建立ServerSocket,綁定和監聽端口
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("啟動伺服器,監聽端口"+DEFAULT_PORT+"...");
while(true){
// 等待用戶端連接配接
Socket socket = serverSocket.accept();
// 建立ChatHandler線程
new Thread(new ChatHandler(this , socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
start()
方法是多人聊天室伺服器的核心。
其實邏輯也很簡單,看過Java網絡程式設計-Socket程式設計初涉一(簡易用戶端-伺服器)這篇部落格的,應該很容易看懂,這裡比之前不過是多建立了一個線程,基于BIO模型,新建立一個線程用于與使用者的資料互動,主線程依舊是等待其他使用者的連接配接請求,這也很好的改善了上一個版本的弊端。
new Thread(new ChatHandler(this , socket)).start();
接下來看看
ChatHandler類
。
當伺服器與用戶端建立連接配接後,伺服器會建立一個線程,用來與用戶端進行資料互動,進而不幹擾主線程的任務(等待用戶端的連接配接請求),
ChatHandler類
就是實作這個功能的類,是以實作了
Runnable接口
,友善用于線程的建立,這裡的邏輯也很簡單,主要實作以下三個功能:
- 将新上線的使用者添加到存儲線上使用者的容器中。
- 讀取使用者發送過來的消息,并将消息轉發給其他的使用者。
- 等使用者退出後,将該使用者在存儲線上使用者的容器裡移除。
package bio.chatroom.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ChatHandler implements Runnable{
private ChatServer server;
private Socket socket;
public ChatHandler(ChatServer server , Socket socket){
this.server = server;
this.socket = socket;
}
@Override
public void run() {
try {
// 存儲新上線使用者
server.addClient(socket);
// 讀取使用者發送的消息
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String msg = null;
while((msg = reader.readLine()) != null){
String fwdMsg = "用戶端["+socket.getPort()+"]:"+msg+"\n";
System.out.print(fwdMsg);
// 将消息轉發給聊天室裡線上的其他使用者
server.forwardMessage(socket , fwdMsg);
// 檢查使用者是否準備退出
if(server.readyToQuit(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
// 從伺服器移除退出的使用者
server.removeClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
伺服器的代碼就全部完成了,其實很簡單吧,主要的邏輯就是下面三個:
- 建立ServerSocket,綁定、監聽一個端口。
- 伺服器主線程阻塞,并且等待使用者連接配接請求。
- 有使用者連接配接後,就建立一個新的線程用于與該使用者進行資料互動,主線程依然等待使用者連接配接請求,新開的線程,會将使用者添加到存儲線上使用者的容器中,并且一直讀取使用者發送過來的消息,再将該消息轉發給其他使用者,直到該使用者退出,當該使用者退出時,就将該使用者在存儲線上使用者容器中移除,再關閉資源。
用戶端代碼
先給出全部代碼,然後分子產品進行解釋。
ChatClient類
是多人聊天室的用戶端部分。
package bio.chatroom.client;
import java.io.*;
import java.net.Socket;
public class ChatClient {
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
// 發送消息給伺服器
public void send(String msg) throws IOException {
if(!socket.isOutputShutdown()){
writer.write(msg+"\n");
writer.flush();
}
}
// 接收伺服器的消息
public String receive() throws IOException {
String msg = null;
if(!socket.isInputShutdown()){
msg = reader.readLine();
}
return msg;
}
// 檢查使用者是否準備退出
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public void close(){
if(writer != null){
try {
writer.close();
System.out.println("關閉socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
// 建立socket
socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);
// 建立IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 處理使用者的輸入
new Thread(new UserInputHandler(this)).start();
// 讀取伺服器轉發的消息
String msg = null;
while((msg = receive()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
close();
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
client.start();
}
}
子產品一
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
DEFAULT_SERVER_HOST
是伺服器
ip
(本地),
DEFAULT_PORT
是伺服器監聽的端口,用戶端根據這兩個屬性可以向伺服器發送連接配接請求。
QUIT
的作用和在伺服器代碼中的作用一樣。
socket
是用戶端與伺服器建立連接配接後建立的
Socket
,
reader
是用于用戶端讀取伺服器回複的消息,
writer
是用于用戶端向伺服器發送消息。
子產品二
// 發送消息給伺服器
public void send(String msg) throws IOException {
if(!socket.isOutputShutdown()){
writer.write(msg+"\n");
writer.flush();
}
}
// 接收伺服器的消息
public String receive() throws IOException {
String msg = null;
if(!socket.isInputShutdown()){
msg = reader.readLine();
}
return msg;
}
// 檢查使用者是否準備退出
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public void close(){
if(writer != null){
try {
writer.close();
System.out.println("關閉socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
這些方法的作用也正如方法名一樣。
-
:用戶端向伺服器發送消息。send()
-
:用戶端接收伺服器的消息。receive()
-
:判斷使用者是否準備退出。readyToQuit()
-
:關閉資源。close()
實作也都非常簡單,就不多說了。
子產品三
public void start(){
try {
// 建立socket
socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);
// 建立IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 處理使用者的輸入
new Thread(new UserInputHandler(this)).start();
// 讀取伺服器轉發的消息
String msg = null;
while((msg = receive()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
close();
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
client.start();
}
和上一個版本的用戶端差不多,也是多建立了一個線程,用于監聽使用者在控制台的輸入,而主線程就用于接收伺服器的消息,主要邏輯如下:
- 先與伺服器建立連接配接。
- 建立與伺服器互動的IO流。
- 建立新線程,用于監聽使用者的控制台輸入。
- 接收伺服器的消息。
UserInputHandler類
,是用戶端建立線程用于監聽使用者的控制台輸入,是以它也實作了
Runnable接口
,友善建立線程,當它監聽到使用者在控制台輸入資訊後,會将使用者輸入的資訊發送給伺服器,伺服器當然也會将該資訊轉發給其他的使用者。它還會判斷使用者是否準備退出,當使用者退出後,這個線程也就差不多結束了,伺服器也會将存儲線上使用者的容器中移除該使用者。
package bio.chatroom.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class UserInputHandler implements Runnable{
private ChatClient client;
public UserInputHandler(ChatClient client){
this.client = client;
}
@Override
public void run() {
try {
// 等待使用者輸入消息
BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in)
);
while(true){
String input = consoleReader.readLine();
// 向伺服器發送消息
client.send(input);
//檢查使用者是否準備退出
if(client.readyToQuit(input)){
break;
}
}
} catch (IOException e){
e.printStackTrace();
}
}
}
這裡我們便完成了一個基于BIO模型的簡易多人聊天室,大家可以自己去實作一下。
測試