Java實現簡單羣聊天
所用技術
Java基礎知識,JavaSwing,Java多線程,JavaTCP網絡編程
思路
服務器
寫兩個線程,一個實現接收客戶端發送的消息功能,並採用消息隊列的方式存儲消息;一個實現羣發消息隊列中消息的功能。服務器啓動時,使用while(true)不斷嘗試與客戶端建立連接,每當建立一個連接後,將該連接保存到一個隊列中,爲羣發消息時,找到發送對象做準備。
客戶端
同樣寫兩個線程,一個實現不斷接收服務器發回來的消息,一個實現在按下發送消息後,將消息發送出去。
服務器
接收消息線程
採用繼承Thread類的方式,循環與客戶端建立連接,並以一個內部類的方式來接收消息,每當建立一個連接,便將該連接加入到連接對列,這裏採用Set,並且啓動一個內部類線程開始接收該連接的消息,接收到的消息加入消息隊列,然後將消息隊列及連接隊列同步到發送消息線程,並啓動發送消息線程,發送一次消息
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.LinkedList;
/**
* 接收消息,線程
*/
public class ReceiveNews extends Thread{
//保存消息
private LinkedList<String> queue = new LinkedList();
//服務器Socket
private ServerSocket server;
//客戶端Socket
private Socket socket;
//保存Socket
HashSet<Socket> socketSet = new HashSet<>();
public ReceiveNews(ServerSocket server) {
this.server = server;
}
@Override
public void run(){
while(true){
//接收連接
try {
socket = server.accept();
socketSet.add(socket);
//爲該連接開啓接收消息線程
new Receive(socket).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Receive extends Thread {
//接收消息
private DataInputStream in;
//Socket對象
private Socket socket;
public Receive(Socket socket){
this.socket = socket;
try {
in = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run(){
while(true){
String temp = "";
try {
temp = in.readUTF();
} catch (IOException e) {
e.printStackTrace();
}
//加入消息隊列
queue.add(temp);
//將隊列同步到發送消息線程
new SendNews(queue,socketSet).start();
}
}
}
}
發送消息線程
通過構造器來初始化消息隊列及連接隊列,若消息隊列中存在未發送的消息,則取連接隊列中的每個連接,依次發送消息給每個客戶端,實現羣聊天
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.HashSet;
import java.util.LinkedList;
/**
* 發送消息線程
*/
public class SendNews extends Thread{
//保存消息
private LinkedList<String> queue = new LinkedList();
//發送消息
private DataOutputStream writer;
//客戶端Socket
private HashSet<Socket> socketSet;
public SendNews(LinkedList<String> queue,HashSet<Socket> socketSet){
this.queue = queue;
this.socketSet = socketSet;
}
@Override
public void run(){
//如果有消息未發
if(queue.size() > 0){
//獲取隊列中的消息
String message = queue.pollFirst();;
//給每個客戶端發送消息
for(Socket socket : socketSet){
try {
writer = new DataOutputStream(socket.getOutputStream());
//發送消息
System.out.println("發送消息");
writer.writeUTF(message);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
主窗體
該窗體中保存有用戶設置的端口號,並且以該端口號創建一個ServerSocket對象,並且通過接收線程類的構造器將這個對象傳給接收消息線程,在創建該窗體的同時啓動接收消息線程。
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.ServerSocket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import talking.thread.ReceiveNews;
/**
* 服務器窗體
*/
public class MainFrame extends JFrame implements ActionListener{
//關閉服務器按鈕
private JButton exit = new JButton("關閉服務器");
//保存端口號
private int post;
//顯示端口號
private JLabel show;
//接收消息線程
private ReceiveNews receive;
//服務器Socket
private ServerSocket server;
public MainFrame(int post) {
//初始化
this.post = post;
try {
server = new ServerSocket(post);
} catch (IOException e) {
e.printStackTrace();
}
show = new JLabel("服務器已啓動端口號:" + post);
init();
//接收消息線程啓動
receive = new ReceiveNews(server);
receive.start();
}
private void init(){
//設置窗口名稱
this.setTitle("服務器");
//窗口大小固定
this.setResizable(false);
//設置窗口關閉
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//設置字體
exit.setFont(new Font("宋體",1,18));
show.setFont(new Font("宋體",1,18));
//添加監聽
exit.addActionListener(this);
//設置佈局
this.setLayout(null);
this.setBounds(700,300,300,500);
exit.setBounds(70,300,150,30);
show.setBounds(20,150,300,90);
//添加組件
this.add(exit);
this.add(show);
//窗體可見
this.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource();
if(button.equals(exit)){//退出
System.exit(0);
}
}
}
窗體效果如下圖
設置端口窗體
啓動服務器時,首先顯示該窗體,用以設置連接服務器的端口號,該窗體有三個組件,關閉程序的按鈕,設置端口的按鈕,以及填寫要設置的端口號的文本框,當設置號端口號後,使該窗體銷燬,並且啓動主窗體
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
/**
* 設置端口號窗體
*/
public class SetPost extends JFrame implements ActionListener{
private static final long serialVersionUID = 1L;
//確定按鈕
private JButton set = new JButton("設置端口號");
//關閉窗口按鈕
private JButton exit = new JButton("關閉");
//窗口輸入
private JTextField write = new JTextField();
//端口號
private JLabel post = new JLabel("端口號");
public SetPost() {
//初始化
init();
}
private void init(){
//設置窗口名稱
this.setTitle("服務器");
//窗口大小固定
this.setResizable(false);
//設置窗口關閉
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//設置字體
exit.setFont(new Font("宋體",1,18));
set.setFont(new Font("宋體",1,18));
write.setFont(new Font("宋體",1,18));
post.setFont(new Font("宋體",1,18));
//添加監聽
exit.addActionListener(this);
set.addActionListener(this);
//設置佈局
this.setLayout(null);
this.setBounds(600,300,400,280);
set.setBounds(50,150,150,30);
exit.setBounds(250,150,80,30);
write.setBounds(100,80,200,20);
post.setBounds(30,80,150,20);
//添加組件
this.add(exit);
this.add(set);
this.add(write);
this.add(post);
//窗體可見
this.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource();
if(button.equals(exit)){//退出
System.exit(0);
}else if(button.equals(set)){//確認
String message = write.getText();
int post;
try{
post = Integer.parseInt(message);
}catch(NumberFormatException exception){
JOptionPane.showMessageDialog(null, "請輸入整數");
write.setText("");
return;
}
this.dispose();
new MainFrame(post);
}
}
}
該窗體效果如下圖
啓動服務器的類
一個簡單的main方法
import talking.frame.SetPost;
/**
* 服務器啓動
*/
public class Start {
public static void main(String[] args) {
//創建服務器窗體
new SetPost();
}
}
到此服務器就完成了,接下來開始寫客戶端
客戶端
選擇端口號及連接服務器名稱窗體
該窗體用於輸入需要連接的服務器的端口號,以及在羣聊中以什麼暱稱發送消息。
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
/**
* 設置端口號窗體
*/
public class SetPost extends JFrame implements ActionListener{
private static final long serialVersionUID = 1L;
//確定按鈕
private JButton set = new JButton("進入");
//關閉窗口按鈕
private JButton exit = new JButton("關閉");
//窗口輸入
private JTextField write = new JTextField();
private JTextField name = new JTextField();
//顯示端口,姓名
private JLabel showPost = new JLabel("端口號");
private JLabel showName = new JLabel("姓 名");
public SetPost() {
//初始化
init();
}
private void init(){
//設置窗口名稱
this.setTitle("客戶端");
//窗口大小固定
this.setResizable(false);
//設置窗口關閉
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//設置字體
exit.setFont(new Font("宋體",1,18));
set.setFont(new Font("宋體",1,18));
write.setFont(new Font("宋體",1,18));
name.setFont(new Font("宋體",1,18));
showPost.setFont(new Font("宋體",1,18));
showName.setFont(new Font("宋體",1,18));
//添加監聽
exit.addActionListener(this);
set.addActionListener(this);
//設置佈局
this.setLayout(null);
this.setBounds(700,300,400,280);
set.setBounds(70,150,80,30);
exit.setBounds(230,150,80,30);
write.setBounds(90,50,200,25);
name.setBounds(90,100,200,25);
showPost.setBounds(30,47,150,30);
showName.setBounds(36,97,120,30);
//添加組件
this.add(exit);
this.add(set);
this.add(write);
this.add(name);
this.add(showName);
this.add(showPost);
//窗體可見
this.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource();
if(button.equals(exit)){//退出
System.exit(0);
}else if(button.equals(set)){//確認
String message = write.getText();
int post;
try{
post = Integer.parseInt(message);
}catch(NumberFormatException exception){
JOptionPane.showMessageDialog(null, "請輸入整數");
write.setText("");
return;
}
if("".equals(name.getText())){
JOptionPane.showMessageDialog(null, "名稱不能爲空");
return;
}
this.dispose();
new MainFrame(post,name.getText());
}
}
}
該窗體效果如下圖
主窗體
以兩個內部類的形式來實現接收消息和發送消息線程,發送消息線程以while(true)不斷接收來自服務器的消息並將其顯示在歷史消息處,而發送消息線程,每次啓動會將此時消息框內的文本發送出去。窗體創建時,開啓接收消息線程,每當按下發送消息按鈕後,啓動一次接收消息線程。
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.ServerSocket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import talking.thread.ReceiveNews;
/**
* 服務器窗體
*/
public class MainFrame extends JFrame implements ActionListener{
//關閉服務器按鈕
private JButton exit = new JButton("關閉服務器");
//保存端口號
private int post;
//顯示端口號
private JLabel show;
//接收消息線程
private ReceiveNews receive;
//服務器Socket
private ServerSocket server;
public MainFrame(int post) {
//初始化
this.post = post;
try {
server = new ServerSocket(post);
} catch (IOException e) {
e.printStackTrace();
}
show = new JLabel("服務器已啓動端口號:" + post);
init();
//接收消息線程啓動
receive = new ReceiveNews(server);
receive.start();
}
private void init(){
//設置窗口名稱
this.setTitle("服務器");
//窗口大小固定
this.setResizable(false);
//設置窗口關閉
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//設置字體
exit.setFont(new Font("宋體",1,18));
show.setFont(new Font("宋體",1,18));
//添加監聽
exit.addActionListener(this);
//設置佈局
this.setLayout(null);
this.setBounds(700,300,300,500);
exit.setBounds(70,300,150,30);
show.setBounds(20,150,300,90);
//添加組件
this.add(exit);
this.add(show);
//窗體可見
this.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource();
if(button.equals(exit)){//退出
System.exit(0);
}
}
}
該窗體效果如下圖
上爲歷史消息框,下爲消息框。
啓動客戶端的類
import talking.frame.SetPost;
/**
* 服務器啓動
*/
public class Start {
public static void main(String[] args) {
//創建服務器窗體
new SetPost();
}
}
到此該項目就寫完了,每次啓動時先起服務器後起客戶端,客戶端可起多個。
最後附上源代碼及可執行jar包
鏈接:小聊天室
提取碼:jbk0
學習階段主要以實現功能爲主,界面不夠美觀以及代碼不夠完善的地方請多包涵。