Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说用JavaSocket编程开发聊天室,附超详细注释「建议收藏」,希望能够帮助你!!!。
大二下册的JavaWeb课程设计,使用的是eclipse。
登录:用Java图形用户界面编写聊天室服务器端和客户端,支持多个客户端连接到一个服务器。每个客户端能够输入账号。
群聊:可以实现群聊(聊天记录显示在所有客户端界面)。
好友列表:完成好友列表在各个客户端上显示。
私聊:可以实现私人聊天,用户可以选择某个其他用户,单独发送信息,接受私聊消息方可以直接弹出消息框。
踢人:服务器能够群发系统消息,能够强行让某些用户下线。
更新:客户端的上线下线要求能够在其他客户端上面实时刷新。
分为服务器端和客户端。
服务器端相当于一个转发器的功能,所有客户端的消息都先发给服务器端,由服务器端再转发给对应的客户端。
不同类型的消息格式不同,服务器端根据消息的格式来判断事件类型,再执行相应的功能。
因为运行的过程中随时会有客户端连上服务器,所以服务器端需要一个线程来等待客户端的链接。其次,每一个服务器端的用户随时都有可能和服务器就发送消息,因此每新增一个用户就需要为该用户建立一个聊天的线程。
服务器端还需要具备踢人、群发消息、发送消息的功能。这些功能的本质其实就是发送对应格式消息(消息格式见下文),只是发送的消息格式不同罢了。
客户端需要实现的主要功能是群发消息和私发消息,并且通过收到的消息格式判断服务器发送过来的消息,再进行响应的代码。
1.上线 : login#nickName
2.下线 : offline#nickName
3.系统发消息 : All#content
4.用户群发消息 : msg#nickName#content
5.用户私发消息 : smsg#sender#receiver#content
6.用户第一次私发消息 : fsmsg#sender#receiver#content
7.服务器端为新加入的客户端建立好友列表 : users#nickName
消息通过split("#")函数将字符串转换成数组,根据strs[0],即第一个值可以判断消息的类型,后面的值判断消息的客户端/发送者/接收者。
客户端/服务器端界面:
私聊界面:
server.java:
package chat;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
class server extends JFrame implements Runnable, ListSelectionListener, ActionListener {
private Socket s = null;
private ServerSocket ss = null;
private ArrayList<ChatThread> users = new ArrayList<ChatThread>(); //容量能够动态增长的数组
DefaultListModel<String> dl = new DefaultListModel<String>();
private JList<String> userList = new JList<String>(dl);//显示对象列表并且允许用户选择一个或多个项的组件。单独的模型 ListModel 维护列表的内容。
private JPanel jpl = new JPanel();
private JButton jbt = new JButton("踢出聊天室");
private JButton jbt1 = new JButton("群发消息");
//群发消息输入栏
private JTextField jtf = new JTextField();
public server() throws Exception{
this.setTitle("服务器端");
this.add(userList, "North");//放在北面
this.add(jpl, "South");
//仅将群发消息输入栏设为一栏
jtf.setColumns(1);
jpl.setLayout(new BorderLayout());
jpl.add(jtf, BorderLayout.NORTH);
jpl.add(jbt,BorderLayout.EAST);//踢出聊天室
jpl.add(jbt1, BorderLayout.WEST);//群发消息
//实现群发
jbt1.addActionListener(this);
//实现踢人
jbt.addActionListener(this);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setLocation(400,100);
this.setSize(500, 400);
this.setVisible(true);
this.setAlwaysOnTop(true);
ss = new ServerSocket(9999);
new Thread(this).start();//监听用户端的加入
}
@Override
public void run() {
while(true){
try{
s = ss.accept();
ChatThread ct = new ChatThread(s); //为该客户开一个线程
users.add(ct); //将每个线程加入到users
//发送Jlist里的用户登陆信息,为了防止后面登陆的用户无法更新有前面用户的好友列表
ListModel<String> model = userList.getModel();//获取Jlist的数据内容
for(int i = 0; i < model.getSize(); i++){
ct.ps.println("USERS#" + model.getElementAt(i));
}
ct.start();
}catch (Exception ex){
ex.printStackTrace();
javax.swing.JOptionPane.showMessageDialog(this,"服务器异常!");
System.exit(0);
}
}
}
//群发消息按钮点击事件监听
@Override
public void actionPerformed(ActionEvent e) {
String label = e.getActionCommand();
if(label.equals("群发消息")){
handleAll();
}else if(label.equals("踢出聊天室")){
try {
handleExpel();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
public void handleAll(){
if(!jtf.getText().equals("")){
sendMessage("ALL#" + jtf.getText());
//发送完后,是输入框中内容为空
jtf.setText("");
}
}//群发消息
public void handleExpel() throws IOException {
sendMessage("OFFLINE#" + userList.getSelectedValuesList().get(0));
dl.removeElement(userList.getSelectedValuesList().get(0));//更新defaultModel
userList.repaint();//更新Jlist
}//踢人
public class ChatThread extends Thread{
Socket s = null;
private BufferedReader br = null;
private PrintStream ps = null;
public boolean canRun = true;
String nickName = null;
public ChatThread(Socket s) throws Exception{
this.s = s;
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
ps = new PrintStream(s.getOutputStream());
}
public void run(){
while(canRun){
try{
String msg = br.readLine();//接收客户端发来的消息
String[] strs = msg.split("#");
if(strs[0].equals("LOGIN")){
//收到来自客户端的上线消息
nickName = strs[1];
dl.addElement(nickName);
userList.repaint();
sendMessage(msg);
}else if(strs[0].equals("MSG") || strs[0].equals("SMSG") || strs[0].equals("FSMSG")){
sendMessage(msg);
}else if(strs[0].equals("OFFLINE")){
//收到来自客户端的下线消息
sendMessage(msg);
//System.out.println(msg);
dl.removeElement(strs[1]);
// 更新List列表
userList.repaint();
}
}catch (Exception ex){
}
}
}
}
public void sendMessage(String msg){
//服务器端发送给所有用户
for(ChatThread ct : users){
ct.ps.println(msg);
}
}
@Override
public void valueChanged(ListSelectionEvent e) {
// TODO 自动生成的方法存根
}
public static void main(String[] args) throws Exception{
new server();
}
}
client.java:
package chat;
import javax.swing.*;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.awt.*;
import java.awt.event.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class client extends JFrame implements Runnable,ActionListener {
//north
private JMenuBar bar = new JMenuBar();
private JMenu menu = new JMenu("关于");
private JMenuItem about = new JMenuItem("关于本软件");
private JMenuItem exit = new JMenuItem("退出");
JPanel north = new JPanel();
//west
JPanel west = new JPanel();
DefaultListModel<String> dl = new DefaultListModel<String>();//用来修改JList
private JList<String> userList = new JList<String>(dl);//用来展示和选择
JScrollPane listPane = new JScrollPane(userList);
//center
JPanel center = new JPanel();
JTextArea jta = new JTextArea(10,20);
JScrollPane js = new JScrollPane(jta);
JPanel operPane = new JPanel();//发送消息的操作面板
JLabel input = new JLabel("请输入:");
JTextField jtf = new JTextField(24);
JButton jButton = new JButton("发消息");
private JButton jbt = new JButton("发送消息");
private JButton jbt1 = new JButton("私发消息");
private BufferedReader br = null;
private PrintStream ps = null;
private String nickName = null;
//私聊面板
JTextArea jTextArea = new JTextArea(11,45);
JScrollPane js1 = new JScrollPane(jTextArea);
JTextField jTextField = new JTextField(25);
String suser = new String();
double MAIN_FRAME_LOC_X;//父窗口x坐标
double MAIN_FRAME_LOC_Y;//父窗口y坐标
boolean FirstSecret = true;//是否第一次私聊
String sender=null;//私聊发送者的名字
String receiver=null;//私聊接收者的名字
public client() throws Exception{
//north 菜单栏
bar.add(menu);
menu.add(about);
menu.add(exit);
about.addActionListener(this);
exit.addActionListener(this);
BorderLayout bl = new BorderLayout();
north.setLayout(bl);
north.add(bar,BorderLayout.NORTH);
add(north,BorderLayout.NORTH);
//east 好友列表
Dimension dim = new Dimension(100,150);
west.setPreferredSize(dim);//在使用了布局管理器后用setPreferredSize来设置窗口大小
//Dimension dim2 = new Dimension(100,150);
//listPane.setPreferredSize(dim2);
BorderLayout bl2 = new BorderLayout();
west.setLayout(bl2);
west.add(listPane,BorderLayout.CENTER);//显示好友列表
add(west,BorderLayout.EAST);
userList.setFont(new Font("隶书",Font.BOLD,18));
//center 聊天消息框 发送消息操作面板
jta.setEditable(false);//消息显示框是不能编辑的
jTextArea.setEditable(false);
BorderLayout bl3 = new BorderLayout();
center.setLayout(bl3);
FlowLayout fl = new FlowLayout(FlowLayout.LEFT);
operPane.setLayout(fl);
operPane.add(input);
operPane.add(jtf);
operPane.add(jbt);
operPane.add(jbt1);
center.add(js,BorderLayout.CENTER);//js是消息展示框JScrollPane
center.add(operPane,BorderLayout.SOUTH);
add(center,BorderLayout.CENTER);
js.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);//需要时才显示滚动条
//鼠标事件,点击
jbt.addActionListener(this);
jbt1.addActionListener(this);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
//this.setAlwaysOnTop(true);
nickName = JOptionPane.showInputDialog("用户名:");
this.setTitle(nickName + "的聊天室");
this.setSize(700,400);
this.setVisible(true);
Socket s = new Socket("127.0.0.1", 9999);
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
ps = new PrintStream(s.getOutputStream());
new Thread(this).start();//run()
ps.println("LOGIN#" + nickName);//发送登录信息,消息格式:LOGIN#nickName
jtf.setFocusable(true);//设置焦点
//键盘事件,实现当输完要发送的内容后,直接按回车键,实现发送
//监听键盘相应的控件必须是获得焦点(focus)的情况下才能起作用
jtf.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ENTER) {
ps.println("MSG#" + nickName + "#" + jtf.getText());//发送消息的格式:MSG#nickName#message
//发送完后,是输入框中内容为空
jtf.setText("");
}
}
});
//私聊消息框按回车发送消息
jTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ENTER) {
handleSS();
}
}
});
//监听系统关闭事件,退出时给服务器端发出指定消息
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
ps.println("OFFLINE#" + nickName);//发送下线信息,消息格式:OFFLINE#nickName
}
});
this.addComponentListener(new ComponentAdapter() {
//监听父窗口大小的改变
public void componentMoved(ComponentEvent e) {
Component comp = e.getComponent();
MAIN_FRAME_LOC_X = comp.getX();
MAIN_FRAME_LOC_Y = comp.getY();
}
});
}
public void run(){
//客户端与服务器端发消息的线程
while (true){
try{
String msg = br.readLine();//读取服务器是否发送了消息给该客户端
String[] strs = msg.split("#");
//判断是否为服务器发来的登陆信息
if(strs[0].equals("LOGIN")){
if(!strs[1].equals(nickName)){
//不是本人的上线消息就显示,本人的不显示
jta.append(strs[1] + "上线啦!\n");
dl.addElement(strs[1]);//DefaultListModel来更改JList的内容
userList.repaint();
}
}else if(strs[0].equals("MSG")){
//接到服务器发送消息的信息
if(!strs[1].equals(nickName)){
//别人说的
jta.append(strs[1] + "说:" + strs[2] + "\n");
}else{
jta.append("我说:" + strs[2] + "\n");
}
}else if(strs[0].equals("USERS")){
//USER消息,为新建立的客户端更新好友列表
dl.addElement(strs[1]);
userList.repaint();
} else if(strs[0].equals("ALL")){
jta.append("系统消息:" + strs[1] + "\n");
}else if(strs[0].equals("OFFLINE")){
if(strs[1].equals(nickName)) {
//如果是自己下线的消息,说明被服务器端踢出聊天室,强制下线
javax.swing.JOptionPane.showMessageDialog(this, "您已被系统请出聊天室!");
System.exit(0);
}
jta.append(strs[1] + "下线啦!\n");
dl.removeElement(strs[1]);
userList.repaint();
}else if((strs[2].equals(nickName) || strs[1].equals(nickName)) && strs[0].equals("SMSG")){
if(!strs[1].equals(nickName)){
jTextArea.append(strs[1] + "说:" + strs[3] + "\n");
jta.append("系统提示:" + strs[1] + "私信了你" + "\n");
}else{
jTextArea.append("我说:" + strs[3] + "\n");
}
}else if((strs[2].equals(nickName) || strs[1].equals(nickName)) && strs[0].equals("FSMSG"))
{
sender = strs[1];
receiver = strs[2];
//接收方第一次收到私聊消息,自动弹出私聊窗口
if(!strs[1].equals(nickName)) {
FirstSecret = false;
jTextArea.append(strs[1] + "说:" + strs[3] + "\n");
jta.append("系统提示:" + strs[1] + "私信了你" + "\n");
handleSec(strs[1]);
}
else {
jTextArea.append("我说:" + strs[3] + "\n");
}
}
}catch (Exception ex){
//如果服务器端出现问题,则客户端强制下线
javax.swing.JOptionPane.showMessageDialog(this, "您已被系统请出聊天室!");
System.exit(0);
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
//鼠标点击事件
String label = e.getActionCommand();
if(label.equals("发送消息")){
//群发
handleSend();
}else if(label.equals("私发消息") && !userList.isSelectionEmpty()){
//未点击用户不执行
suser = userList.getSelectedValuesList().get(0);//获得被选择的用户
handleSec(suser);//创建私聊窗口
sender = nickName;
receiver = suser;
}else if(label.equals("发消息")){
handleSS();//私发消息
}else if(label.equals("关于本软件")){
JOptionPane.showMessageDialog(this, "1.可以在聊天框中进行群聊\n\n2.可以点击选择用户进行私聊");
}else if(label.equals("退出")){
JOptionPane.showMessageDialog(this, "您已成功退出!");
ps.println("OFFLINE#" + nickName);
System.exit(0);
} else{
System.out.println("不识别的事件");
}
}
public void handleSS(){
//在私聊窗口中发消息
String name=sender;
if(sender.equals(nickName)) {
name = receiver;
}
if(FirstSecret) {
ps.println("FSMSG#" + nickName + "#" + name + "#" + jTextField.getText());
jTextField.setText("");
FirstSecret = false;
}
else {
ps.println("SMSG#" + nickName + "#" + name + "#" + jTextField.getText());
jTextField.setText("");
}
}
public void handleSend(){
//群发消息
//发送信息时标识一下来源
ps.println("MSG#" + nickName + "#" + jtf.getText());
//发送完后,是输入框中内容为空
jtf.setText("");
}
public void handleSec(String name){
//建立私聊窗口
JFrame jFrame = new JFrame();//新建了一个窗口
JPanel JPL = new JPanel();
JPanel JPL2 = new JPanel();
FlowLayout f2 = new FlowLayout(FlowLayout.LEFT);
JPL.setLayout(f2);
JPL.add(jTextField);
JPL.add(jButton);
JPL2.add(js1,BorderLayout.CENTER);
JPL2.add(JPL,BorderLayout.SOUTH);
jFrame.add(JPL2);
jButton.addActionListener(this);
jTextArea.setFont(new Font("宋体", Font.PLAIN,15));
jFrame.setSize(400,310);
jFrame.setLocation((int)MAIN_FRAME_LOC_X+20,(int)MAIN_FRAME_LOC_Y+20);//将私聊窗口设置总是在父窗口的中间弹出
jFrame.setTitle("与" + name + "私聊中");
jFrame.setVisible(true);
jTextField.setFocusable(true);//设置焦点
jFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
jTextArea.setText("");
FirstSecret = true;
}
});
}//私聊窗口
public static void main(String[] args)throws Exception{
new client();
}
}
这个课设的核心应该是线程,消息的格式发送和分解。
其中需要想一下的地方是自动弹出私聊那一部分。需要理清什么时候弹出私聊框,是发送方第一次向接受方发送私聊消息时,因此需要设置一个变量来标记是否是第一次发送消息。其次,因为私聊的接收方发送方的变化,需要设置两个变量来记录两方的昵称,然后根据客户自己的nickName来设置消息格式,否则的话私聊的消息格式有一方可能会变成自己发给自己的,这样另一方就接收不到对方的消息,因而只能单方面发送消息。