Java—简易聊天室

Java—聊天室的实现

在学习了IO流,多线程以及网络编程的知识之后,我们可以利用所学到的知识做一个小项目,这里我做了一个多人聊天室,实现了群聊和私聊功能,看完分享之后也可以自己去做一个练练手。

首先是整个项目的大体架构:首先要分为服务器端和客户端两个端口。如下图所示

image.png

客户端可以向服务器发送信息,并接受服务器返回的信息。而服务器实际上是作为一个中转站:在群聊模式时,将一个客户端的发送的信息转发至其他客户端。而在私聊时,服务器将信息发送到指定的客户端处,达到私聊的效果。

我们先依次附上服务器端与客户端的代码,再讲解实现的具体过程

服务器端(这里用本地主机作为服务器):

public class Server {
    private List<MyChannel> all = new ArrayList<MyChannel>();

    public static void main(String[] args) throws IOException {
        new Server().start();
    }

    public void start() throws IOException {
        ServerSocket server = new ServerSocket(7777);
        while(true) {
            Socket client = server.accept();
            MyChannel channel = new MyChannel(client);
            all.add(channel);
            new Thread(channel).start();        //一条道路
        }
    }

    /**
     * 一个客户一条道路
     * 建立服务器与客户端之间的数据通道
     *
     */
    private  class MyChannel implements Runnable {

        private DataInputStream dis;
        private DataOutputStream dos;
        private boolean flag = true;
        private String name;

        public MyChannel(Socket client) {
            try {
                dis = new DataInputStream(client.getInputStream());
                dos = new DataOutputStream(client.getOutputStream());
            } catch (IOException e) {
                e.printStackTrace();
                flag = false;

                try {
                    dis.close();
                    dos.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }

        //接收客户端的信息
        private String receive() {
            String msg = "";
            try {
                msg = dis.readUTF();
            } catch (IOException e) {
                flag = false;
                e.printStackTrace();
                all.remove(this);
                try {
                    dis.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            return msg;
        }

        //向客户端发送信息
        private void send(String msg) {
            if (null == msg || msg.equals("")) {
                return;
            }
            try {
                dos.writeUTF(time());
                dos.writeUTF(msg);
            } catch (IOException e) {
                flag = false;
                e.printStackTrace();
                all.remove(this);  //移除自身
                try {
                    dos.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }

        private void sendOthers(String msg) {
            //判断是否是私聊
            if (msg.contains("@") && msg.indexOf(":") > msg.indexOf("@")){
                String spot = null;

                String secreName = msg.substring(msg.indexOf("@") + 1, msg.indexOf(":"));
                String secretMsg = msg.substring(msg.indexOf(":") + 1);
//                System.out.println(secreName);
//                System.out.println(secretMsg);

                for (MyChannel other : all) {
                    if (secreName.equals(other.name)) {
                        other.send(name + "悄悄地对你说:" + secretMsg);
                    }

                }
            }else{

                    for (MyChannel other : all) {
                        if (other == this) {
                            continue;
                        }
                        other.send(msg);
                    }
                }
            }


                private String time () {
                    Date now = new Date(System.currentTimeMillis());
                    String time = new SimpleDateFormat("yyyy.MM.dd  hh:mm:ss").format(now);
                    return time;
                }

                @Override
                public void run () {
                    send("欢迎加入群聊");
                    name = receive();
                    sendOthers(name + "加入了群聊");
                    while (flag) {
                        sendOthers(receive());
                    }
                }
            }
        }

现在我们来分析这段代码

  • 1> 首先,Server类中包含一个ArrayList容器,其中保存的是MyChannel类元素,实际上每一个MyChannel类的对象就是一条 连接服务器与客户端的路径(其中数据以流的形式传输)
  • 2> 在主函数中实例化了Server对象之后,调用了start()方法,我们看到方法体中首先为服务器端创建了ServerSocket对象,并指定了端口。紧接着就是一个死循环,接受连接到服务器的客户端,并将其信息添加至ArrayList容器中,并为其创建一条线程(线程就绪并开始运行)
  • 3> 那么MyChannel类内部又是什么呢?我们画图来分析
    image.png
  • 4> 构造器:只有一个以Socket类的客户端对象为参数的构造器,在该构造器中,传入了客户端对象后,建立于客户端对象的数据通道。
  • 5> receive()方法:从数据通道中读取从客户端发送来的信息(字符串)
  • 6> time()方法:生成当前的具体时间,并以字符串形式返回
  • 7> send(String msg)方法:将time()与msg信息依次发送到客户端
  • 8> sendOthers(String msg):私聊部分(私聊形式:@客户端名:私聊信息):先对传入的msg进行分析,若字符串首字符为 ‘@’ 并且字符串中含有 ‘:’时,即判断这条信息是一条私聊信息,将私聊客户端名与私聊内容从msg中分离出来,并将其单独发送给指定的客户端(遍历容器并匹配客户端名)。群聊部分(直接发送内容):遍历整个容器,除了当前客户端,向其他所有客户端发送消息。
  • 9> 线程体部分(run()方法 ):在一个客户端接入服务器后,首先向其发送一条”欢迎加入群聊”的信息,再将其加入聊天室的信息发送给其他聊天室内的用户,然后执行死循环 sendOthers(receive()) 方法

客户端(包含三个类):

  • 客户端类:
public class Client {
    public static void main(String[] args) throws IOException {
        System.out.println("请输入昵称");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String name = br.readLine();

        Socket client = new Socket("localhost",7777);
        //客户端发送
        new Thread(new Send(client,name)).start();
        //客户端接收
        new Thread(new Receive(client)).start();
    }

}

我们对客户端代码进行分析:

  • Client 类中只有一个主方法,刚开始会要求你为客户端起名,紧接着创建Socket类实例,并与本地服务器的指定端口连接。
  • 多线程的问题:我们不能规定客户端是先读取再发送还是先发送再读取,所以为两个功能分别建立一条线程,即读与写可以同时实现,而接受和发送信息的类就是下面要说的Receive类和Send类

  • Receive类:
public class Receive implements Runnable{
    //输入流
    private DataInputStream dis;
    //线程标识,判断线程运行状态
    private boolean flag = true;


    public Receive(Socket client){
        try {
            dis = new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
            flag = false;
            try {
                dis.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
    //接收数据
    public String receive(){
        String msg = "";
        try {
            msg = dis.readUTF();
        } catch (IOException e) {
            e.printStackTrace();
            flag = false;
            try {
                dis.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        return msg;
    }

    @Override
    public void run() {
        while(flag){
            System.out.println(receive());
        }
    }
}
  • Receive类是负责接收从服务器发来的消息的类,主要有三个地方使用:1.刚与服务器连接后服务器发来的欢迎信息的接收。2.群聊信息的接收(时间+内容)。3.私聊信息的接收(时间+私聊内容)
  • receive()方法,从输入流中读取信息。
  • 线程体(run()方法):只有一个始终接收信息并打印到控制台的循环体

  • Send 类:
public class Send implements Runnable{
    //控制台输入流
    private BufferedReader console;
    //管道输出流
    private DataOutputStream dos;
    //控制线程
    private boolean flag= true;
    //聊天昵称
    private String name;

    public Send() {
        console = new BufferedReader(new InputStreamReader(System.in));
    }

    public Send(Socket client,String name){
        this();
        this.name = name;
        try {
            dos = new DataOutputStream(client.getOutputStream());
        } catch (IOException e) {
            //e.printStackTrace();
            flag = false;
        }

    }
    //从控制台接收数据
    private String getMsgFromConsole() {
        try {
            return console.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    //发送数据
    public void send(String msg){
        if (null != msg && !msg.equals("")){
            try {
                dos.writeUTF(name +": " + msg);
                Date now = new Date(System.currentTimeMillis());
                String time = new SimpleDateFormat("hh:mm:ss   yyyy/MM/dd  ").format(now);
                System.out.println(time);
                System.out.println(name + ":" + msg);
                dos.flush();
            } catch (IOException e) {
                e.printStackTrace();
                try {
                    flag = false;
                    dos.close();
                    console.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }

            }
        }else{
            try {
                dos.writeUTF(name);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        //线程体
        send("");
        while (flag){
            send(getMsgFromConsole());
        }
    }
}
  • Send类负责从控制台读入客户端所输入的信息,并将信息发送至服务器,由服务器判断是私聊信息还是群聊信息。
  • 构造器:有一个无参构造器和一个参数为客户端对象和客户端名的构造器,建立对服务器的数据输出流,并保存客户端名
  • 线程体(run()方法):先发送一个空字符串,send()方法会自动判断为发送客户端名至服务器,然后再循环体中始终执行发送从控制台读取的字符至服务器的方法
  • send()方法:初次发送时,会将客户端名发送至服务器,在之后将从控制台读取信息并发送至服务器,同时在自己的控制台上打印自己发送消息的时间和消息的内容

调试结果

在IDEA 的控制台上运行服务器,将客户端类打jar包在本地Powershell上运行

image.png

  • 第一个客户端接入:
    image.png

  • 服务器控制台信息:
    image.png

  • 多个客户端接入
    image.png

  • 服务器控制台信息:
    image.png

  • 群聊实现:
    image.png

  • 私聊实现:
    image.png

功能还是都能实现的,可能会有一些小Bug,界面也没有做出来,不过主要是为了综合前面所学到的知识并去运用

猜你喜欢

转载自blog.csdn.net/wintershii/article/details/81231642