Java实现聊天室

本文章主要介绍基于多线程的Java聊天室的整体思路以及如何通过代码一步步的实现多线程的Java聊天室。

1. 聊天室具有4大功能

  1. 1. 具有用户上线注册和下线注销功能
  2. 2. 具有私聊功能
  3. 3. 具有群聊功能
  4. 4. 具有统计聊天室在线人数的功能

2. 基于多线程的聊天室的整体思路

  • 1. 模式:对于聊天室就是处理多个客户端发送的请求与信息,从而需要一个服务器端去处理这些客户端请求与信息,故采用的是服务器端/客户端模式。
  • 2. 客户端与服务器端的连接:既然是多个客户端发送的请求与信息从而交给服务器端处理,那么客户端与服务器端之间需要进行连接,在我的博客:Socket编程 中介绍到两台计算机之间如何通过使用套接字建立TCP连接,从而进行通信操作。稍后会通过分析以及代码的方式进行详细操作。
  • 3. 处理多线程问题:关于客户端,客户端需要向服务器端发送信息,而客户端也需要接收服务器端的信息,故需要两个线程分别处理客户端向服务器发送信息和客户端接收服务器端信息。关于服务器端,由于服务器端是统一处理多个客户端发送的信息请求,故每当有一个客户端与该服务器端建立连接后,需要创建新的线程去单独处理该客户端发送的信息。
  • 4. 客户端方面:客户端在与服务器端建立连接后,通过Socket对象获取输入输出流从而与服务器端之间进行通信。
  • 5. 服务器端方面:服务器端的套接字ServerSocket对象在调用accept()方法侦听客户端的连接,当与客户端成功建立连接后,返回Socket对象,从而利用该Socket对象获取输入输出流从而与客户端进行通信。

3. 聊天室的具体实现

3.1 客户端与服务器端建立连接

利用Socket编程,客户端与服务器端建立连接的步骤如下:

(1)服务器端通过java.net.ServerSocket类的构造方法实例化ServerSocket对象,选择的构造方法如下:

public ServerSocket (int port) throws IOException

该构造方法需要传入参数:端口号,从而创建绑定到指定端口的服务器套接字。

(2)客户端通过java.net.Socket类的构造方法创建一个流套接字并将其连接到指定主机上的指定端口号,该构造方法如下:

public Socket(String host,int port) throws UnknowHostException,IOException

(3)服务器端的ServerSocket对象调用accept()方法侦听请求连接指定端口号的该服务器端的客户端,并在接收到客户端请求后返回服务器端的流套接字,即Socket对象,从而服务器端与客户端成功建立连接。

(4)客户端与服务器端之间的通信操作,java.net.Socket类就是提供客户端与服务器端相互通信的套接字。

获取Socket套接字的输入流的方法为:

public InputStream getInputStream() throws IOException

获取Socket套接字的输出流的方法为:

public OutputStream getOutputStream() throws IOException

在获取了服务器端与客户端的输入输出流之后,进行信息输入输出即可进行通信操作。

(5)服务器端与客户端之间通信结束后,需要关闭套接字,调用close()方法即可。close()方法如下:

public void close() throws IOException

经过以上的分析,下面通过代码的方式创建服务器端与客户端之间的连接以及通信操作。

  • 服务器端:
package chat.room.server;

import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class SingleServer {
    public static void main(String[] args) throws IOException {
        //1.创建服务器端的ServerSocket对象,等待客户端连接其端口
        ServerSocket serverSocket=new ServerSocket(6666);
        System.out.println("服务器的端口号为:6666,正在等待客户端的连接...");
        //2.侦听并接收服务器端的连接,返回套接字Socket对象
        Socket socket=serverSocket.accept();
        //3.获取客户端的输入流,读取客户端输入的内容
        Scanner scanner=new Scanner(socket.getInputStream());
        scanner.useDelimiter("\n");
        if(scanner.hasNext()){
            System.out.println("客户端发来消息:"+scanner.next());
        }
        //4.获取客户端的输出流,向客户端输出内容
        PrintStream printStream=new PrintStream(socket.getOutputStream());
        printStream.println("客户端你好!我是服务器端:"+serverSocket.getLocalPort());
        //5.关闭流
        serverSocket.close();
    }
}

  • 客户端:
package chat.room.client;

import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class SingleClient {
    public static void main(String[] args) throws IOException {
        //1.客户端连接服务器端,返回套接字Socket对象
        Socket socket=new Socket("127.0.0.1",6666);
        //2.获取服务器端的输出流,向服务器端输出内容
        PrintStream printStream=new PrintStream(socket.getOutputStream());
        printStream.println("我是客户端"+socket.getLocalPort());
        //3.获取服务器端的输入流,读取服务器端的内容
        Scanner scanner=new Scanner(socket.getInputStream());
        scanner.useDelimiter("\n");
        if(scanner.hasNext()){
            System.out.println(scanner.next());
        }
        //4.关闭流
        socket.close();
    }
}

3.2 多线程客户端的实现

客户端方面,需要做的只有两件事:1. 向服务器端发送信息    2. 接收服务器端信息

故创建两个线程:

3.2.1 客户端读取服务器端信息的线程

  • 既然客户端需要读取服务器端信息就需要客户端获取服务器端的输入流,从而将信息显示到客户端
//1.客户端读取服务器端信息的线程
class ClientReadServer implements Runnable{
    private Socket socket;
    public ClientReadServer(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        //1.获取服务器端输入流
        try {
            Scanner scanner=new Scanner(socket.getInputStream());
            while(scanner.hasNext()){
                System.out.println(scanner.next());
            }
            scanner.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.2.2 客户端向服务器端发送信息的线程

  • 既然客户端需要向服务器端发送信息就需要客户端获取服务器端的输出流,从而将信息发送到服务器端。
//2.客户端向服务器端发送信息的线程
class ClientSendServer implements Runnable{
    private Socket socket;
    public ClientSendServer(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        try {
            //1.获取服务器端的输出流
            PrintStream printStream=new PrintStream(socket.getOutputStream());
            //2.从键盘中输入信息
            Scanner scanner=new Scanner(System.in);
            while(true){
                String msg=null;
                if(scanner.hasNext()){
                    msg=scanner.next();
                    printStream.println(msg);
                }
                if(msg.equals("exit")){
                    scanner.close();
                    printStream.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.2.3 启动两个子线程

  • 在客户端的主方法中,首先创建连接服务器端的Socket对象,然后通过Thread类的start()方法启动两个子线程,从而完成客户端的操作。
public class MultiClient {
    public static void main(String[] args) throws IOException{
        //1.客户端连接服务器端,返回套接字Socket对象
        Socket socket=new Socket("127.0.0.1",6666);
        //2.创建读取服务器端信息的线程和发送服务器端信息的线程
        Thread read=new Thread(new ClientReadServer(socket));
        Thread send=new Thread(new ClientSendServer(socket));
        //3.启动线程
        read.start();
        send.start();
    }
}

3.3 多线程服务器端的实现

  • 对于多线程服务器端,多线程体现在每当有一个客户端与服务器端建立连接后,需要创建一个新的线程处理客户端发送的请求信息。那么服务器端在创建线程时覆写的run()方法中需要具有之前提到的四个功能:1. 具有用户上线注册和用户下线注销的功能  2. 具有私聊功能  3. 具有群聊功能  4. 具有统计聊天室在线人数的功能。
  • 服务器端如何保存客户端注册用户的信息?由于每当有一个客户端与服务器端建立连接后,服务器端就需要将客户端保存起来以便在私聊群聊以及统计在线人数时使用到每个客户端的信息,故选择全局的Map集合类的ConcurrentHashMap类将客户端信息保存起来,将客户端注册的用户名作为ConcurrentHashMap的key值,客户端的Socket对象作为ConcurrentHashMap的value值。
  • 服务器端如何识别客户端是需要注册、私聊、群聊还是注销?这需要对客户端的输入信息进行约束判断,从而进行不同的处理。      若输入格式为:userName:用户名  -------->   则进行用户注册处理                                                                                                          若输入格式为:G:群聊信息     ------->   则进行群聊操作                                                                                                                          若输入格式为:P:userName - 私聊信息    ------->   则进行私聊操作                                                                                                      若输入格式为:包含exit字符串    ------->    则进行用户注销操作                                                                                                                若输入格式为其他则提示“输入格式错误!”,请按照要求输入

3.3.1 用户上线注册的功能

客户端输入格式为userName:用户名时,服务器端识别到客户端需要进行的是注册用户操作后,将客户端输入的用户名保存起来,并将该客户端的用户名作为ConcurrentHashMap的key值,客户端的Socket对象作为ConcurrentHashMap的value值,将其添加到全局变量的ConcurrentHashMap对象中。

/**
     * 注册用户信息
     * @param userName 用户名
     * @param socket 用户客户端Socket对象
     */
    private void userRegist(String userName,Socket socket){
        map.put(userName,socket);
        System.out.println("[用户名为"+userName+"][客户端为"+socket+"]上线了!");
        System.out.println("当前在线人数为:"+map.size()+"人");
    }

3.3.2 用户下线注销的功能

客户端输入格式为输入信息中包含exit字符串时,服务器端识别到客户端需要进行注销用户操作后,通过遍历ConcurrentHashMap对象的key值,key值所对应的value值与当前客户端的Socket对象相等时,则找到了需要注销的客户端的用户名,从而利用其key值将客户端从ConcurrentHashMap对象中删除。

/**
     * 用户退出
     * @param socket
     */
    private void userExit(Socket socket){
        //1.利用socket取得对应的Key值
        String userName=null;
        for(String key:map.keySet()){
            if(map.get(key).equals(socket)){
                userName=key;
                break;
            }
        }
        //2.将userName,Socket元素从map集合中删除
        map.remove(userName,socket);
        //3.提醒服务器该客户端已下线
        System.out.println("用户:"+userName+"已下线!");
    }

3.3.3 私聊功能

客户端输入格式为P:userName - 私聊信息时,服务器端识别到客户端需要进行的私聊操作,服务器端通过遍历ConcurrentHashMap对象的key值,当找到与userName相等的key值时,利用get()方法取得userName对应的需要私聊的客户端Socket对象,从而将获取私聊的客户端的输出流并将私聊信息进行输出。但是为了客户体验,私聊的客户端需要知道是哪个用户对自己进行私聊操作,故可遍历ConcurrentHashMap对象的value值获取当前客户端的用户名给私聊客户端。

/**
     * 私聊流程(利用userName取得客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
     * @param socket 当前客户端
     * @param userName 私聊的用户名
     * @param msg 私聊的信息
     */
    private void privateChat(Socket socket,String userName,String msg) throws IOException {
        //1.取得当前客户端的用户名
        String curUser=null;
        Set> set=map.entrySet();
        for(Map.Entry entry:set){
            if(entry.getValue().equals(socket)){
                curUser=entry.getKey();
                break;
            }
        }
        //2.取得私聊用户名对应的客户端
        Socket client=map.get(userName);
        //3.获取私聊客户端的输出流,将私聊信息发送到指定客户端
        PrintStream printStream=new PrintStream(client.getOutputStream());
        printStream.println(curUser+"私聊说:"+msg);
    }

3.3.4 群聊功能

客户端输入格式为G:群聊信息时,服务器端识别到客户端需要进行的群聊操作,服务器端通过遍历ConcurrentHashMap对象的value值,获取客户端的Socket对象从而利用Socket对象取得输出流后将群聊信息发送给每一个客户端。同样为了客户体验,其他客户端需要知道是谁发送了群聊信息,故当前客户端在发送群聊信息的同时告诉其他客户端自己的用户名。

/**
     * 群聊流程(将Map集合转换为Set集合,从而取得每个客户端Socket,将群聊信息发送给每个客户端)
     * @param socket 发出群聊的客户端
     * @param msg 群聊信息
     */
    private void groupChat(Socket socket,String msg) throws IOException {
        //1.将Map集合转换为Set集合
        Set> set=map.entrySet();
        //2.遍历Set集合找到发起群聊信息的用户
        String userName=null;
        for(Map.Entry entry:set){
            if(entry.getValue().equals(socket)){
                userName=entry.getKey();
                break;
            }
        }
        //3.遍历Set集合将群聊信息发给每一个客户端
        for(Map.Entry entry:set){
            //取得客户端的Socket对象
            Socket client=entry.getValue();
            //取得client客户端的输出流
            PrintStream printStream=new PrintStream(client.getOutputStream());
            printStream.println(userName+"群聊说:"+msg);
        }
    }

3.3.5 统计聊天室在线人数功能

对于统计聊天室在线人数功能,用于服务器端将每一个注册用户名的客户端保存在ConcurrentHashMap对象中,故若统计聊天室在线人数只需调用ConcurrentHashMap对象的size()方法即可。该功能模块在客户端用户注册时已经实现,故不再单独实现。

3.3.6 主方法中的操作(创建套接字、创建线程池)

在完成了所有的功能模块之后,首先需要创建服务器端的套接字ServerSocket对象,从而客户端能够进行连接;其次由于服务器端处理多个客户端时需要创建多个线程,故可创建固定大小的线程池处理多个客户端。具体代码的实现如下:

public class MultiServer {
    public static void main(String[] args){
        try {
            //1.创建服务器端的ServerSocket对象,等待客户端连接
            ServerSocket serverSocket=new ServerSocket(6666);
            //2.创建线程池,从而可以处理多个客户端
            ExecutorService executorService= Executors.newFixedThreadPool(20);
            for(int i=0;i<20;i++){
                System.out.println("欢迎来到我的聊天室......");
                //3.侦听客户端
                Socket socket=serverSocket.accept();
                System.out.println("有新的朋友加入.....");
                //4.启动线程
                executorService.execute(new Server(socket));
            }
            //5.关闭线程池
            executorService.shutdown();
            //6.关闭服务器
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上就是基于多线程的Java聊天室!

下面将所有的代码进行整理,代码如下:

  • 客户端代码
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

//1.客户端读取服务器端信息的线程
class ClientReadServer implements Runnable{
    private Socket socket;
    public ClientReadServer(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        //1.获取服务器端输入流
        try {
            Scanner scanner=new Scanner(socket.getInputStream());
            while(scanner.hasNext()){
                System.out.println(scanner.next());
            }
            scanner.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//2.客户端向服务器端发送信息的线程
class ClientSendServer implements Runnable{
    private Socket socket;
    public ClientSendServer(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        try {
            //1.获取服务器端的输出流
            PrintStream printStream=new PrintStream(socket.getOutputStream());
            //2.从键盘中输入信息
            Scanner scanner=new Scanner(System.in);
            while(true){
                String msg=null;
                if(scanner.hasNext()){
                    msg=scanner.next();
                    printStream.println(msg);
                }
                if(msg.equals("exit")){
                    scanner.close();
                    printStream.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class MultiClient {
    public static void main(String[] args) throws IOException{
        //1.客户端连接服务器端,返回套接字Socket对象
        Socket socket=new Socket("127.0.0.1",6666);
        //2.创建读取服务器端信息的线程和发送服务器端信息的线程
        Thread read=new Thread(new ClientReadServer(socket));
        Thread send=new Thread(new ClientSendServer(socket));
        //3.启动线程
        read.start();
        send.start();
    }
}
  • 服务器端代码
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class Server implements Runnable{
    private static Map map=new ConcurrentHashMap<>();
    private Socket socket;
    public Server(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        //1.获取客户端的输入流
        try {
            Scanner scanner=new Scanner(socket.getInputStream());
            String msg=null;
            while(true){
                if(scanner.hasNextLine()){
                    //0.处理客户端输入的字符串
                    msg=scanner.nextLine();
                    Pattern pattern=Pattern.compile("\r");
                    Matcher matcher=pattern.matcher(msg);
                    msg=matcher.replaceAll("");
                    //1.注册用户流程,注册用户的格式为:userName:用户名
                    if(msg.startsWith("userName:")){
                        //将用户名保存在userName中
                        String userName=msg.split("\\:")[1];
                        //注册该用户
                        userRegist(userName,socket);
                        continue;
                    }
                    //2.群聊信息流程,群聊的格式为:G:群聊信息
                    else if(msg.startsWith("G:")){
                        //必须先注册才可以!
                        firstStep(socket);
                        //保存群聊信息
                        String str=msg.split("\\:")[1];
                        //发送群聊信息
                        groupChat(socket,str);
                        continue;
                    }
                    //3.私聊信息流程,私聊的格式为:P:userName-私聊信息
                    else if(msg.startsWith("P:")&&msg.contains("-")){
                        //必须先注册才可以!
                        firstStep(socket);
                        //保存需要私聊的用户名
                        String userName=msg.split("\\:")[1].split("-")[0];
                        //保存私聊的信息
                        String str=msg.split("\\:")[1].split("-")[1];
                        //发送私聊信息
                        privateChat(socket,userName,str);
                        continue;
                    }
                    //4.用户退出流程,用户退出格式为:包含exit
                    else if(msg.contains("exit")){
                        //必须先注册才可以!
                        firstStep(socket);
                        userExit(socket);
                        continue;
                    }
                    //其他输入格式均错误
                    else{
                        PrintStream printStream=new PrintStream(socket.getOutputStream());
                        printStream.println("输入格式错误!请按照以下格式输入!");
                        printStream.println("注册用户格式:[userName:用户名]");
                        printStream.println("群聊格式:[G:群聊信息]");
                        printStream.println("私聊格式:[P:userName-私聊信息]");
                        printStream.println("用户退出格式[包含exit即可]");
                        continue;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 第一步必须先注册!
     * @param socket 当前客户端
     */
    private void firstStep(Socket socket) throws IOException {
        Set> set=map.entrySet();
        for(Map.Entry entry:set){
            if(entry.getValue().equals(socket)){
                if(entry.getKey()==null){
                    PrintStream printStream=new PrintStream(socket.getOutputStream());
                    printStream.println("请先进行注册操作!");
                    printStream.println("注册格式为:[userName:用户名]");
                }
            }
        }
    }
    /**
     * 注册用户信息
     * @param userName 用户名
     * @param socket 用户客户端Socket对象
     */
    private void userRegist(String userName,Socket socket){
        map.put(userName,socket);
        System.out.println("[用户名为"+userName+"][客户端为"+socket+"]上线了!");
        System.out.println("当前在线人数为:"+map.size()+"人");
    }

    /**
     * 群聊流程(将Map集合转换为Set集合,从而取得每个客户端Socket,将群聊信息发送给每个客户端)
     * @param socket 发出群聊的客户端
     * @param msg 群聊信息
     */
    private void groupChat(Socket socket,String msg) throws IOException {
        //1.将Map集合转换为Set集合
        Set> set=map.entrySet();
        //2.遍历Set集合找到发起群聊信息的用户
        String userName=null;
        for(Map.Entry entry:set){
            if(entry.getValue().equals(socket)){
                userName=entry.getKey();
                break;
            }
        }
        //3.遍历Set集合将群聊信息发给每一个客户端
        for(Map.Entry entry:set){
            //取得客户端的Socket对象
            Socket client=entry.getValue();
            //取得client客户端的输出流
            PrintStream printStream=new PrintStream(client.getOutputStream());
            printStream.println(userName+"群聊说:"+msg);
        }
    }
    /**
     * 私聊流程(利用userName取得客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
     * @param socket 当前客户端
     * @param userName 私聊的用户名
     * @param msg 私聊的信息
     */
    private void privateChat(Socket socket,String userName,String msg) throws IOException {
        //1.取得当前客户端的用户名
        String curUser=null;
        Set> set=map.entrySet();
        for(Map.Entry entry:set){
            if(entry.getValue().equals(socket)){
                curUser=entry.getKey();
                break;
            }
        }
        //2.取得私聊用户名对应的客户端
        Socket client=map.get(userName);
        //3.获取私聊客户端的输出流,将私聊信息发送到指定客户端
        PrintStream printStream=new PrintStream(client.getOutputStream());
        printStream.println(curUser+"私聊说:"+msg);
    }

    /**
     * 用户退出
     * @param socket
     */
    private void userExit(Socket socket){
        //1.利用socket取得对应的Key值
        String userName=null;
        for(String key:map.keySet()){
            if(map.get(key).equals(socket)){
                userName=key;
                break;
            }
        }
        //2.将userName,Socket元素从map集合中删除
        map.remove(userName,socket);
        //3.提醒服务器该客户端已下线
        System.out.println("用户:"+userName+"已下线!");
    }
}
public class MultiServer {
    public static void main(String[] args){
        try {
            //1.创建服务器端的ServerSocket对象,等待客户端连接
            ServerSocket serverSocket=new ServerSocket(6666);
            //2.创建线程池,从而可以处理多个客户端
            ExecutorService executorService= Executors.newFixedThreadPool(20);
            for(int i=0;i<20;i++){
                System.out.println("欢迎来到我的聊天室......");
                //3.侦听客户端
                Socket socket=serverSocket.accept();
                System.out.println("有新的朋友加入.....");
                //4.启动线程
                executorService.execute(new Server(socket));
            }
            //5.关闭线程池
            executorService.shutdown();
            //6.关闭服务器
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

 

 

你可能感兴趣的:(项目)