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
学习阶段主要以实现功能为主,界面不够美观以及代码不够完善的地方请多包涵。