最近老師叫我們做一個基于Socket的多人聊天室,網上很多教程都隻講了如何通過Socket來建立連接配接以及通過控制台一遍列印證明連接配接已經完成但還沒有具體實作多人聊天。這次我整理了一下自己的實作代碼,希望能幫助到和我一樣學習時感到困惑的兄弟姐妹。
這是基于TCP協定實作的Socket多人聊天室,分别用到ServerSocket(伺服器)和Socket(用戶端),說到TCP當然也離不開建立我們常常聽到的三次握手、四次揮手,這裡附上幾位大佬的總結。
https://blog.csdn.net/jia281460530/article/details/41901069
https://blog.csdn.net/u013782203/article/details/51767763
聊天室有一個簡單的圖形界面,每一次用戶端請求并完成連接配接時,伺服器端會顯示目前線上人數,同時在側邊欄添加了一個更新線上用戶端資訊的功能界面,顯示線上的用戶端(具體是顯示每個用戶端的名字,當然有一個局限就是用戶端的名字不能相同)。如果想要在多台計算機之間實作通信,可以以管理者身份運作cmd指令行輸入ipconfig檢視相應的ip位址。
伺服器主要功能:
伺服器端的任務是不斷地接收來自于多個用戶端的連接配接請求,并且為每一個請求連接配接的用戶端配置設定一個線程管理,每一個線程隻管理與它相連接配接的用戶端Socket。這裡我用了Client内部類來封裝每一個已經連接配接的用戶端,這樣子的好處是Socket的資訊及輸入輸出流都能夠一起儲存;并将它們添加到List中,用于記錄線上的用戶端資訊。同時,伺服器起到的一個最重要的作用是轉發,将A用戶端發出的消息轉發給B、C、D等多個線上用戶端,這個時候儲存了各個Client類的List就派上了關鍵用場,伺服器每次收到一個用戶端的消息就在List中查找每個線上用戶端并向他們轉發這個用戶端所發出的消息(當然,跳過它自己本身)。總的來說伺服器有以下幾個功能:
- 循環監聽并響應各個用戶端的連接配接并為他們配置設定線程 **
- 接受各個用戶端的消息并處理(代碼中主要是在消息前面加上這個用戶端的名字 如:Client_Name: + aLine(消息))
- 通過已儲存好各個用戶端資訊的List來轉發通過處理後的消息 **
- 如果有某個用戶端A下線,則告訴其他線上用戶端這個用戶端A下線(代碼中通過用戶端發送Good Bye來說明離線)
- 更新【線上用戶端資訊界面】并将更新通知發往每一個線上用戶端
用戶端功能:
- 發送消息
- 接收消息并處理顯示
- 更新線上用戶端
其中,接收到的消息為:【Client_Name:+消息 】的時候顯示在主文本框中;接收到的消息為:【update:+内容】的時候代表這是一個更新線上用戶端的通知,顯示在側邊的線上Client文本框中,内容中就是線上用戶端的全部資訊,并且每個用戶端是以一個空格分隔的(這樣子處理是因為我用BufferedReader讀取消息時每次隻讀一行,(因為本人對輸入輸出流也并非太了解。。。))這個地方感覺做的不太好,另外,如果大家有什麼建議或想法歡迎大家能跟我說說讨論讨論。
用到的技術:
- 圖形界面GUI及響應事件
- 基于TCP的ServerSocket、Socket套接字
- 輸入輸出流
- Thread類
- 其中,伺服器Thread主要是為每一個【封裝了Socket的Client類】配置設定一個線程,用戶端Thread主要是為了能實作不斷地發送消息和同時不斷地接受消息的功能。
以下是具體實作的代碼
伺服器:
package Server;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.border.LineBorder;
public class Server extends JFrame {
// server 對象
private ServerSocket server;
// Client's socket對象
private Socket socket;
// 存放用戶端Client
List<Client> list;
// 視窗标簽
private JLabel label;
// 面闆用于容納文本區和按鈕
private JPanel labelPanel, clientPanel;
// 顯示區
private JTextArea centerTextArea, clientTextArea;
public Server(int port) throws IOException {
server = new ServerSocket(port);
//控制台輸出伺服器端口号和ip位址
System.out.println(server.getLocalPort());
System.out.println(InetAddress.getLocalHost().getHostAddress());
//初始化存放Client的連結清單
list = new ArrayList<Client>();
setTitle("伺服器");
//圖形界面GUI
create();
//添加用戶端
addClient();
}
private void create() {
setSize(500, 500);
// 視窗label,顯示伺服器名字
label = new JLabel("10086 伺服器");
label.setFont(new Font("宋體", 5, 15));
labelPanel = new JPanel();
labelPanel.setSize(150, 20);
labelPanel.add(label, BorderLayout.CENTER);
// 視窗center部分,顯示接收到的Client的消息
centerTextArea = new JTextArea(" ---聊天室已連接配接--- " + "\r\n");
centerTextArea.setFont(new Font("微軟雅黑", 5, 13));
centerTextArea.setEditable(false); // 不可編輯
centerTextArea.setBackground(Color.LIGHT_GRAY);
// 視窗client部分,顯示目前線上的Client
clientTextArea = new JTextArea("--線上Client--" + "\r\n");
clientTextArea.setEditable(false); // 不可編輯
//裝載clientTextArea的容器
clientPanel = new JPanel(new FlowLayout());
clientPanel.setBorder(new LineBorder(null));
clientPanel.add(clientTextArea);
//JFrame架構用于添加各種容器
add(clientPanel, BorderLayout.EAST);
add(labelPanel, BorderLayout.NORTH);
add(new JScrollPane(centerTextArea), BorderLayout.CENTER);
setVisible(true);
setResizable(false); // 視窗大小不可調整
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
// 添加用戶端
private void addClient() throws IOException {
//循環監聽用戶端的連接配接請求并逐個配置設定線程
while (true) {
// 接受用戶端連接配接請求并儲存
socket = server.accept();
//圖形界面輸出目前用戶端ip位址以及線上人數
String ip = socket.getInetAddress().getHostAddress();
centerTextArea.append(ip + "連接配接成功,目前用戶端數為:" + (list.size() + 1) + "\r\n");
Client client = new Client(socket);
// 添加使用者到清單
list.add(client);
// 為使用者建立并啟動一個接受線程
new Thread(client).start();
//【線上用戶端】更新通知
client.update();
}
}
//用戶端處理類(【接受線程】用于不斷接受消息 同時不影響自己發送消息)
class Client implements Runnable {
String name;
Socket socket;
// 輸出流
private PrintWriter out;
// 輸入流
private BufferedReader in;
public Client(Socket socket) throws IOException {
this.socket = socket;
// 獲得相應Client's Socket 輸入流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 獲得相應Client's Socket 輸出流
out = new PrintWriter(socket.getOutputStream());
//擷取目前Client的名字并更新clientTextArea
name = in.readLine();
clientTextArea.append(name+"\r\n");
}
//向對應流發送消息
public void send(String str) {
out.println(str);
out.flush();
}
//給【每一個用戶端】發送更新clientTextArea的消息
public void update() {
StringBuffer sBuffer = new StringBuffer("update:");
clientTextArea.setText("--線上Client--\r\n");
for(int i = 0;i<list.size();i++) {
clientTextArea.append(list.get(i).name+"\r\n");
sBuffer.append(list.get(i).name+" ");
}
for(int i = 0;i<list.size();i++) {
list.get(i).out.println(sBuffer);
list.get(i).out.flush();
}
}
@Override
public void run() {
try {
String aLine;
boolean flag = true;
while (flag && (aLine = in.readLine()) != null) {
//處理并儲存目前用戶端發來的消息(消息前加上目前用戶端名字)
String str = this.name + "說:" + aLine;
//内容顯示
centerTextArea.append(str + "\r\n");
// 給每一個儲存在List中的用戶端(除了目前用戶端)轉發目前用戶端發來的消息
for (int i = 0; i < list.size(); i++) {
Client client = list.get(i);
if (client != this) {
client.send(str);
/*
* 如果目前用戶端發來的消息是“Good Bye”表示其要下線
* 服務端則轉告給【其他的用戶端】該用戶端下線的消息并進行對
* 目前用戶端Socket關閉的處理
*/
if (aLine.equals("Good Bye")) {
client.send(this.name + "已離線");
flag = false;
}
}
}
}
} catch (Exception e1) {
} finally {
try {
//目前用戶端Socket關閉處理
//1.連結清單中移除該Client
if (list.contains(this))
list.remove(this);
/*
* 2.更新伺服器界面中的【線上用戶端視窗】以及
* 為其他的用戶端發送【更新線上用戶端視窗】的消息
*/
update();
System.out.println(this.name+"已離開");
//3.輸出輸入流關閉處理
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
} catch (Exception e) {
System.out.println(this.name + "出現異常強行關閉");
}
}
}
}
public static void main(String[] args) throws IOException {
Server Test = new Server(10086);
}
}
用戶端:
package Client;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.LineBorder;
public class Client extends JFrame implements Runnable {
// Client_Name
private String name;
// Client's socket對象
private Socket socket;
// 輸出流
private PrintWriter out;
// 輸入流
private BufferedReader in;
// 用于關閉線程
Thread receivethread;
// 視窗标簽
private JLabel label;
// 面闆用于容納文本區和按鈕
private JPanel buttomPanel, inputPanel, labelPanel, centenPanel;
// 顯示區以及輸入區
private JTextArea centerTextArea, inputTextArea, clientTextArea;
// 發送與清除按鈕
private JButton send, clear;
//Client
public Client(String name, Socket socket) throws IOException {
this.name = name;
this.socket = socket;
//控制台輸出端口号以及主機位址
System.out.println(socket.getLocalPort());
System.out.println(InetAddress.getLocalHost().getHostAddress());
//得到輸入輸出流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream());
//發送名字給伺服器
out.println(name);
out.flush();
setTitle(name);
//Client端GUI
create();
//建立并啟動自己的接收線程
receivethread = new Thread(this);
receivethread.start();
//監聽程式啟動
setActionLister();
}
//GUI
private void create() {
setSize(500, 500);
// 視窗label
label = new JLabel("10086 聊天室");
label.setFont(new Font("宋體", 5, 15));
labelPanel = new JPanel();
labelPanel.setSize(150, 20);
labelPanel.add(label, BorderLayout.CENTER);
// 視窗center部分
centerTextArea = new JTextArea(" ---聊天室已連接配接--- " + "\r\n");
centerTextArea.setFont(new Font("微軟雅黑", 5, 13));
centerTextArea.setEditable(false); // 不可編輯
centerTextArea.setBackground(Color.LIGHT_GRAY);
// 視窗client部分
clientTextArea = new JTextArea("--線上Client--" + "\r\n");
clientTextArea.setEditable(false); // 不可編輯
clientTextArea.setBorder(new LineBorder(null));
// centerPanel 充當center和client的容器
centenPanel = new JPanel(new BorderLayout());
centenPanel.add(new JScrollPane(centerTextArea), BorderLayout.CENTER);
centenPanel.add(clientTextArea, BorderLayout.EAST);
// 視窗button按鈕
send = new JButton("發送");
clear = new JButton("清空");
buttomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5));
buttomPanel.add(clear);
buttomPanel.add(send);
// 視窗input部分
inputPanel = new JPanel(new BorderLayout());
inputTextArea = new JTextArea();
inputTextArea = new JTextArea(7, 20);
inputPanel.add(new JScrollPane(inputTextArea), BorderLayout.CENTER);
inputPanel.add(buttomPanel, BorderLayout.SOUTH);
add(labelPanel, BorderLayout.NORTH);
add(centenPanel, BorderLayout.CENTER);
add(inputPanel, BorderLayout.SOUTH);
setVisible(true);
setResizable(false); // 視窗大小不可調整
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
//inpuTextArea以及按鈕send&clear的監聽程式
private void setActionLister() {
//關閉視窗時通知伺服器本用戶端已關閉
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
try {
if(out!=null) {
out.println("Good Bye");
out.flush();
}
}
catch (Exception e2) {
System.out.println("用戶端已關閉");
}
}
});
//點選send按鈕将inputTextArea的消息全部發送
send.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String aLine = inputTextArea.getText();
inputTextArea.setText(""); //輸入框清空
centerTextArea.append("你說: " + aLine + "\r\n");
try {
out.println(aLine);
out.flush();
if (aLine.equals("Good Bye")) {
//接受線程中斷
receivethread.interrupt();
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
}
} catch (Exception e1) {
System.out.println("已斷開");
}
}
});
//鍵盤監聽事件
inputTextArea.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
//CTRL+ENTER相當于點選send按鈕
if (e.getKeyCode() == KeyEvent.VK_ENTER && e.isControlDown()) {
send.doClick();
}
}
});
//清除inputTextArea的内容
clear.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
inputTextArea.setText("");
}
});
}
@Override
public void run() {
try {
String aLine = "";
while ((aLine = in.readLine()) != null) {
//centerTextAre顯示接受的消息
if (!aLine.split(":")[0].equals("update"))
centerTextArea.append(aLine + "\r\n");
//在綫client更新
else {
String[] strings = (aLine.split(":")[1]).split(" ");
clientTextArea.setText("--線上Client--\r\n");
for (String s : strings)
clientTextArea.append(s + "\r\n");
}
}
} catch (Exception e) {
centerTextArea.append("目前連接配接已斷開\r\n");
System.out.println("目前連接配接已斷開");
}
}
public static void main(String[] args) throws IOException {
new Client("Client5", new Socket("localhost", 10086));
// new clientGUI("Client2", new Socket("localhost", 10086));
}
}