用JavaSocket编程开发聊天室

大二下册的JavaWeb课程设计,使用的是eclipse。

一、实现功能

  1. 登录:用Java图形用户界面编写聊天室服务器端和客户端,支持多个客户端连接到一个服务器。每个客户端能够输入账号。

  2. 群聊:可以实现群聊(聊天记录显示在所有客户端界面)。

  3. 好友列表:完成好友列表在各个客户端上显示。

  4. 私聊:可以实现私人聊天,用户可以选择某个其他用户,单独发送信息,接受私聊消息方可以直接弹出消息框

  5. 踢人:服务器能够群发系统消息,能够强行让某些用户下线。

  6. 更新:客户端的上线下线要求能够在其他客户端上面实时刷新。

二、思路概述

  • 分为服务器端和客户端。

  • 服务器端相当于一个转发器的功能,所有客户端的消息都先发给服务器端,由服务器端再转发给对应的客户端。

  • 不同类型的消息格式不同,服务器端根据消息的格式来判断事件类型,再执行相应的功能。

服务器端

      因为运行的过程中随时会有客户端连上服务器,所以服务器端需要一个线程来等待客户端的链接。其次,每一个服务器端的用户随时都有可能和服务器就发送消息,因此每新增一个用户就需要为该用户建立一个聊天的线程
服务器端还需要具备踢人、群发消息、发送消息的功能。这些功能的本质其实就是发送对应格式消息(消息格式见下文),只是发送的消息格式不同罢了。

客户端

      客户端需要实现的主要功能是群发消息和私发消息,并且通过收到的消息格式判断服务器发送过来的消息,再进行响应的代码。

三、消息格式

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来设置消息格式,否则的话私聊的消息格式有一方可能会变成自己发给自己的,这样另一方就接收不到对方的消息,因而只能单方面发送消息。

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐