利用Java基础实现一个聊天室(思路、步骤、遇到的常见问题)

聊天室的任务目标:

        1. 创建一个服务器和客户端,并且能让客户端成功的连接到服务端

        2. 在服务端和客户端上创建"听筒"和"麦克风" 让其分别可以接收和输出数据

        3.让客户端输入内容到服务端,服务端将客户端发来的信息原样返回给客户端,客户端完成接收

        4. 实现服务端能够与多个客户端链接

        5. 实现群聊:服务端与多个客户端连接之后,服务端接收每个客户端发来的消息,并将接收到的消息返回给所有客户端,完成群聊。

        6. 给每个客户端定义一个名字,知道是谁发出来的信息(Map)

        8. 完成私信:客户端之间可以私聊,同时显示上、下线剩余人数,解决并发安全问题

        9. 完成

提示:本篇文章内容比较多,但是每个任务的步骤和思路,以及会遇到的问题都有标注出来,在最后我也会放上完整的代码给大家提供对照,因为只是实现一个简易的小聊天室是一个比较基础的小项目,里面可能存在很多漏洞,因为比较基础嘛,就没有太多的功能┮﹏┭(以后会不断地完善,比如添加数据库,或者前后端交互)。

        大家根据需求阅读吧,文章中如果有什么错误,大家可以指正,或者遇到什么问题也可以评论留言,我也会尽力的帮助大家解决问题!

一、创建服务端和客户端

 1.创建项目(熟悉的话可以跳过)

        1. 打开IDEA软件,我们一起创建一个新的项目c69fb2c2048b488d822d0508576d39ff.png

        2.选择一个盘符,存放项目(存放的位置自己能找到就可以,名字可以任意最好全英文路径)

        大家的IDEA的版本可能不同,可能有的是社区版,有的是终极版,但是具体操作步骤都相差不大,这里我把两种创建项目的方式都列出来了,有需要的同学可以看一下,我这里用的是终极版,不过没关系,这两个版本操作流程,基本就是一样的,大家不必担心。

a8fd9d14e23d4f47b9b552bafe5640bc.png

                                             这个是终极版创建项目的中间步骤  上图;

fff3a46f6b624c918de5e1e50a03af71.png

这个是社区版创建项目的中间步骤 上图;

        使用社区版的同学点一下Next,然后把项目创建到自己能找到位置就可以,具体选项和设置,大家仔细看一下图片,根据图片设置即可。项目创建完成之后界面应该是这样的。

c0628a86fb5b48a28cc0991834538c4f.png

 2.创建socket包和包下的类

        1)我们创建好项目之后,鼠标选中java目录  --->  鼠标右键  --->  new  --->  package

我们新建一个包,包名叫做socket。点击回车创建成功,这时候java目录下会出现一个名字叫做

socket的包。

6cfc5f015b234ddd97d5bc07b224abca.png

        2)鼠标选中刚刚创建的socket包,鼠标右键 在包下创建两个类,分别叫做Sever和Client

一个是服务端的类(Sever),一个客户端的类(Client)。创建完之后会发现socket包下多了两个

类分别是Sever和Client

404fe1a3de4141f89eabb57a2b148b8e.png

        准备工作做好之后,要正式开始写代码了!✌

3.创建服务端Sever

        双击打开Sever类写入代码,每条代码后面有注释,特殊的地方会另外解释。

注意要点

        1. 端口号选则的时候最好选择在8000往后的端口,因为靠前的端口可能会被系统占用

        2. serverSocket.accept() 方法会阻塞,直到有客户端连接到服务器之后代码才会继续向下执行

        3. try...catch捕获异常的时候最好冷处理,不然一会测试的时候会一直报异常

        4. 提示一下:try...catch..的快捷键是【ctrl + alt + t】;

                              构造器(constructor)的快捷键:【alt + insert】;会弹出列表自己选择一下即可    

package socket;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Sever {
    private ServerSocket serverSocket;
    
    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务器,等待客户端连接
     * 该方法目前只处理一个客户端连接,接受连接后打印成功消息
     */
    public void start() {
        try {
            System.out.println("服务端启动成功,正在等待客户端连接。。。");
            Socket socket = serverSocket.accept();//等待客户端链接
            System.out.println("客户端连接成功");
        } catch (IOException e) {
            //冷处理
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

 4.创建客户端Client

        双击打开socket包下的Client类,接下来搭建一下客户端的代码

注意要点

        1.  Socket socket = new Socket("localhost",8086); localhost是本地主机的ip地址,8086是刚刚我们搭建服务器的时候创建的窗口(大家设定的值可能不同,输入自己在服务器中搭建的端口号就好,这里我设定的是8086)。

package socket;

import java.io.IOException;
import java.net.Socket;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            Socket socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }

    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
    }
}

        到这里服务端和客户端的基础框架基本搭建完毕,大家可以测试一下,先运行一下服务端(Sever),然后再运行一下客户端(Client),你可以发现客户端和服务端可以成功链接了,具体运行结果如下: 上图是服务端(Sever)的运行结果,下图是客户端(Client)的运行结果。

57a3ed9eb1b94e8f9e439d0ee5fc26e0.png

205574e0e88247fe84f1b6e9818b1c5d.png

任务1:1. 创建一个服务器和客户端,并且能让客户端成功的连接到服务端  顺利完成 √ 很棒!✌

二、服务端接收客户端数据(io流)

        接下来将要完成第二个任务:在服务端和客户端上创建"听筒"和"麦克风" 让其分别可以接收和输出数据,同时 要测试:让客户端输入内容到服务端,服务端将客户端发来的信息原样返回给客户端,客户端完成接收。

 1. 在Sever服务端中搭建输入流

        首先,打开Sever类,接着写入以下代码,(为了方便大家看在哪里插入了新的代码,我用两个分割线做了标注,两个分割线之间就是新添加的代码)

注意要点

        1. 解释一下新添加的这几串代码:

                ①:接收客户端客户端发来的输入流(InputStream in = socket.getInputStream();)

                ②:InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);

                        将接收到的字节流转换为字符流,同时设定为UTF8字符集

                ③:BufferedReader br = new BufferedReader(isr); 将字符存储到缓冲区中高效读取字                           符数据。

                ④:while ((message = br.readLine()) != null){
                                        System.out.println("客户端说:" + message);
                       }

                        从br对象中读取文本,赋值给message,遇到换行(\n)就输出message

                  最终可以看到客户端发送来的文字

   做一个比喻,好理解些:

       就像一个外国人和你说话,你首先需要接收外国人说了什么话(代码①的作用),

       但是老外说话你有一点点听不懂,因此就给你配了一个翻译官(代码②的作用),

       如果外国人说一句话,翻译官就给你翻译一句,说一句,翻译一句,这样效率会很低,          为了提高效率,翻译官就直接把外国人说的话全部都写在纸上,让你一次性读取(代码③的作用)这样效率就会变得很高了,然后你就从纸上一行一行的读,直到你读取完全部的内容。

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Sever {
    private ServerSocket serverSocket;

    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            System.out.println("服务端启动成功,正在等待客户端连接。。。");
            // 等待客户端链接
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接成功");
//=================================================================================
            // 获取客户端发送的输入流
            InputStream in = socket.getInputStream();
            // 创建输入流读取器
            InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
            // 创建缓冲流
            BufferedReader br = new BufferedReader(isr);
            String message = null;
            // 循环读取客户端发送的消息,直到消息为空
            while ((message = br.readLine()) != null){
                System.out.println("客户端说:" + message);
            }
//=================================================================================
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

 2. 在Client客户端中搭建输出流

        现在服务端可以收到客户端发来数据,那么接下来我们需要让客户端能够发出数据,与上面一样,新添加的代码我用分割线分割出来了,这里相当于给客户端提供了一个”麦克风“ ,让客户端可以向服务端发出信息,然后服务端接收数据。

        这里实现的主要功能就是:让客户端可以发送信息,提示在控制台输入信息,输入”exit“退出聊天。

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {
    private Socket socket;

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }
//=======================================================================================
    /**
     * 启动客户端,读取用户输入并发送到服务器
     * 此方法将一直运行,直到用户输入"exit"为止
     */
    public void start(){
        try {
            // 获取Socket的输出流
            OutputStream out = socket.getOutputStream();
            // 使用UTF-8字符集创建OutputStreamWriter
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            // 创建BufferedWriter以提高写入效率
            BufferedWriter bw = new BufferedWriter(osw);
            // 创建PrintWriter,设置自动刷新,用于向服务器发送消息
            PrintWriter pw = new PrintWriter(bw,true);
            // 创建Scanner对象以读取用户输入
            Scanner scanner = new Scanner(System.in);
            String data;
            // 提示用户输入内容
            System.out.println("请在控制台中输入内容,点击回车发送,输入”exit“退出:");
            // 无限循环,读取用户输入并发送到服务器,直到用户输入"exit"
            while (true){
                data = scanner.nextLine();
                if ("exit".equalsIgnoreCase(data)){
                    break;
                }
                pw.println(data);
            }
            // 用户输入"exit"后,打印退出消息
            System.out.println("退出成功!");
        } catch (IOException e) {
            //冷处理
        }finally {
            try {
                // 关闭Socket连接
                socket.close();
            } catch (IOException e) {
                //冷处理
            }
        }
    }
//=======================================================================================

    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
//==========================
        // 启动客户端
        client.start();
//==========================
    }
}

        完成这两步之后,我们可以测试一下,先运行(Sever)服务端,然后再运行(client)客户端,在客户端的控制窗口中输入一些测试消息,检测运行结果,运行结果如下:

注意:这里测试结束的时候,最好是在客户端(Client)上输入"exit" 结束测试,不然有的时候端口进程不会被杀死,端口会被占用,导致下一次运行的时候会报异常(很恶心,很麻烦)。

54d7f3d46c924674bb3fc57cb1710583.png

9af75ffe01474412a9a6bbdc54b81847.png

至此,任务二: 在服务端和客户端上创建"听筒"和"麦克风" 让其分别可以接收和输出数据 恭喜🎇

三、客户端接收服务端数据(线程)

        现在开始第三个任务:

     让客户端输入内容到服务端,服务端将客户端发来的信息原样返回给客户端,客户端完成接收

        这个任务很好理解,就是让服务端发送数据,客户端接收,相当于把任务二反过来再实现一遍,但是这个任务中有很多隐藏的“坑”,我们需要利用多线程去解决,这里代码变化会很大,大家一定要仔细的看好。

        大家脑海中的第一个想法应该是,直接在服务端添加一个输出流,在客户端添加一个输入流,秉着这样的想法,我先把代码给大家写出来,

1. 实现服务端向客户端发送消息

这里是服务端(Sever)的代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Sever {
    private ServerSocket serverSocket;

    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            System.out.println("服务端启动成功,正在等待客户端连接。。。");
            // 等待客户端链接
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接成功");
            // 获取客户端发送的输入流
            InputStream in = socket.getInputStream();
            // 创建输入流读取器
            InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
            // 创建缓冲流
            BufferedReader br = new BufferedReader(isr);
//=======================================================================================
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw,true);
//=======================================================================================
            String message ;
            // 循环读取客户端发送的消息,直到消息为空
            while ((message = br.readLine()) != null){
                System.out.println("客户端说:" + message);
//==============================================
                pw.println("服务器说:"+message);
//==============================================
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

我们先分析服务端的执行过程:首先、等待客户端的链接

                                                   ------> 与客户端建立输入,输出的通道

                                                   ------> 读取客户端发来的消息,同时返回消息给客户端

执行过后发现服务端没有出现问题,接下来我们看一下客户端的代码;     

 2. 实现客户端接收服务端的消息

这里是客户端(Client)的代码:

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {
    private Socket socket;

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }

    /**
     * 启动客户端,读取用户输入并发送到服务器
     * 此方法将一直运行,直到用户输入"exit"为止
     */
    public void start(){
        try {
            // 获取Socket的输出流
            OutputStream out = socket.getOutputStream();
            // 使用UTF-8字符集创建OutputStreamWriter
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            // 创建BufferedWriter以提高写入效率
            BufferedWriter bw = new BufferedWriter(osw);
            // 创建PrintWriter,设置自动刷新,用于向服务器发送消息
            PrintWriter pw = new PrintWriter(bw,true);
//====================================================================================
            InputStream in = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
            BufferedReader br = new BufferedReader(isr);
            String message ;
            while ((message = br.readLine()) != null){//接收消息的while循环
                System.out.println(message);
            }
//====================================================================================
            // 创建Scanner对象以读取用户输入
            Scanner scanner = new Scanner(System.in);
            String data;
            // 提示用户输入内容
            System.out.println("请在控制台中输入内容,点击回车发送,输入”exit“退出:");
            // 无限循环,读取用户输入并发送到服务器,直到用户输入"exit"
            while (true){//输入消息的while循环
                data = scanner.nextLine();
                if ("exit".equalsIgnoreCase(data)){
                    break;
                }
                pw.println(data);
            }
            // 用户输入"exit"后,打印退出消息
            System.out.println("退出成功!");
        } catch (IOException e) {
            //冷处理
        }finally {
            try {
                // 关闭Socket连接
                socket.close();
            } catch (IOException e) {
                //冷处理
            }
        }
    }

    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
        // 启动客户端
        client.start();
    }
}

  (1)实现过程中,分析遇到的问题

我们再分析客户端的代码执行过程:首先、与服务端创建链接

                                                   ------> 与服务端创建输入输出的通道(到这都没问题)

                                                   ------> 读取服务端返回的消息,同时还想发消息给服务端

到这里就出现了问题,首先是 :

(代码中标注了,那个是接收消息的循环,那个是输出消息的循环,大家一定要仔细看有所区别)

                1.客户端想要接收服务端消息的前提是:需要客户端先发送一段消息给服务端,然后服务端才会把接收到的消息返回给客户端。

        这个时候我们观察代码发现,客户端还没给服务端发送消息的时候,第一个while接收消息的循环就已经结束了,所以无论怎么输入,客户端都接受不到服务端返回的消息。

这个时候可能有的同学会想,把接收消息的while循环,放在输出消息的while循环的后面不就可以接收到消息了嘛,正常的思维逻辑是没问题的,但是观察代码你会发现,输出消息的while循环是一个死循环,代码执行的时候会一直在循环里面打转,永远都不会执行接收消息的循环。

所以重点来了!!!!!!!!!!!🐒🐒🐒🐒🐒🐒🐒🐒🐒🐒🐒

既然放前放后都不可以,那要怎么样才能实现既能输出消息的同时还能接收到消息呢?

多线程来解决这个问题啦!!

  (2)多线程

        多线程简单地说就是,从人的感官上认为:计算机在同一时间一并完成多件事情,(大家把多线程先认为是计算机同一时间做很多事,以后会详细的说一下)。

        所以为了实现客户端即可以接收信息又可以发送信息的的功能,我们要使用多线程,首先,创建一个线程任务InMessage类,实现Runnable接口,将发送信息的功能封装在InMessage中,然后,在start();方法中创建并启动线程。

        这串代码变化比较大,大家一定要仔细比对一下!!!

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.TreeMap;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {
    private Socket socket;

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }

    /**
     * 启动客户端,读取用户输入并发送到服务器
     * 此方法将一直运行,直到用户输入"exit"为止
     */
    public void start(){
        try {

            //=====================================================
            // 创建一个线程用于接收服务器的消息
            Thread thread = new Thread(new InMessage());
            // 启动线程
            thread.start();
            //=====================================================

            // 获取Socket的输出流
            OutputStream out = socket.getOutputStream();
            // 使用UTF-8字符集创建OutputStreamWriter
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            // 创建BufferedWriter以提高写入效率
            BufferedWriter bw = new BufferedWriter(osw);
            // 创建PrintWriter,设置自动刷新,用于向服务器发送消息
            PrintWriter pw = new PrintWriter(bw,true);
            // 创建Scanner对象以读取用户输入
            Scanner scanner = new Scanner(System.in);
            String data;
            // 提示用户输入内容
            System.out.println("请在控制台中输入内容,点击回车发送,输入”exit“退出:");
            // 无限循环,读取用户输入并发送到服务器,直到用户输入"exit"
            while (true){
                data = scanner.nextLine();
                if ("exit".equalsIgnoreCase(data)){
                    break;
                }
                pw.println(data);
            }
            // 用户输入"exit"后,打印退出消息
            System.out.println("退出成功!");
        } catch (IOException e) {
            //冷处理
        }finally {
            try {
                // 关闭Socket连接
                socket.close();
            } catch (IOException e) {
                //冷处理
            }
        }
    }
    //=========================================================
    // 内部类,实现Runnable接口,用于接收服务器的消息
    private class InMessage implements Runnable{
        public void run() {
            try {
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                String message ;
                while ((message = br.readLine()) != null){
                    System.out.println(message);
                }
            } catch (IOException e) {
                //冷处理
            }
        }
    }
    //==========================================================
    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
        // 启动客户端
        client.start();
    }
}

        最终实现的效果如下:客户端既可以发送消息的同时又可以接收到服务端返回的消息,(有的同学可能产生疑惑,返回一个相同的消息到底有什么用,这一步是给后面的“群聊”功能做铺垫。)

1bb2efc378624edb8b319da6ef75fd14.png

0499ae29ffea48c19141f0cb078bba68.png

        完成到这里,任务三:让客户端输入内容到服务端,服务端将客户端发来的信息原样返回给客户端,客户端完成接收,就顺利的完成了!!,这里比较难理解,完成到这里真的很棒!!✌.

四、多客户端连接服务端

        1.抢占资源问题

        在之前我们实现了客户端与服务端之间构建连接,顺利的完成了三个小任务,但是其中还有一个弊端:目前只能一个客户端与一个服务端连接,在日常生活中,肯定不可能一台服务器只服务一个客户,这样的话开销实在太大了(哈哈哈如果真是这样那就实现了天价上网了),所以我们需要完成第四个任务实现服务端能够与多个客户端链接。

       首先,要有一个大概的思路:

        1)之前服务端是通过这串代码,等待客户端的链接,直到有客户端连接成功之后,代码继续执行构建出客户端与服务端之间IO通道,完成接收和返回消息的功能。

public void start() {
        try {
            System.out.println("服务端启动成功,正在等待客户端连接。。。");
            // 等待客户端链接
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接成功");
            // 获取客户端发送的输入流
            InputStream in = socket.getInputStream();
            // 创建输入流读取器
            InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
            // 创建缓冲流
            BufferedReader br = new BufferedReader(isr);
//=======================================================================================
            OutputStream out = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw,true);
//=======================================================================================
            String message ;
            // 循环读取客户端发送的消息,直到消息为空
            while ((message = br.readLine()) != null){
                System.out.println("客户端说:" + message);
//==============================================
                pw.println("服务器说:"+message);
//==============================================
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

        2)如果想要多个客户端与服务端连接,那就需要让服务端一直等待连接,并且每连接一个客户端,服务端就会和这个客户端创建出新的IO通道(while循环)(大概就是下面这个意思,画的有点丑),这样的话就可以让每个客户端都可以与服务端交流了。

6a6cc92455574a0185dfe79461419ef9.png

        大概的思路有了把代码写出来,看一下start方法的执行的过程是否可以达成我们的目的:

                进入循环--->等待客户端连接--->连接成功后与第一个客户端构建输入和输出流--->进入内部循环当第一个客户端关闭连接或发送空消息时,内层的 while 循环结束,服务端继续等待下一个客户端连接。)---->等待第二个客户端连接--->出现了问题(当客户端关闭连接或发送空消息时,内层的 while 循环结束,但外层的 while (true) 循环继续运行。由于没有关闭 socket 和相关的输入输出流,这些资源仍然占用着,导致无法正确处理第二个客户端的连接

                 简单的说就是:第一个连接的客户端一直占用着输入、输出的通道。导致后面连接的客户端没有办法与服务器之间创建输入、输出通道,虽然是可以正常的连接端口,但是不能完成信息的交互。

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Sever {
    private ServerSocket serverSocket;

    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                // 创建输入流读取器
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                // 创建缓冲流
                BufferedReader br = new BufferedReader(isr);
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
                String message ;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    System.out.println("客户端说:" + message);
                    pw.println("服务器说:"+message);
                }
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

        在测试之前大家按照步骤更改一个小设置(要不然没有办法多个窗口运行):

5f305421711d449aba1896f3610bb714.png

e461648429f5460c95148b9b7e08c102.png

f34f41828a2246aaa5da4ef3501643af.png

2915e01eec3e480b88e2dac16e57df2e.png

        设置完成之后,先运行服务端(Sever)代码,然后再运行几个客户端(Client),测试过程中可以发现,第一个与服务端连接的客户端可以正常的发送消息、接收消息,但是后面连接上的客户端,即没办法把消息发送到服务端,也不能接收到服务端返回来的消息,具体的测试效果如下:

服务端测试图片:

548568fd4349438facbfaff8370f5ff4.png

第一个连接的客户端测试图片:

b4d6607906004fd7ae08fb16ded4b3c9.png

第二个连接的客户端测试图片:

bf60fd4163134128bc813dd6bbedfda2.png

第三个连接的客户端测试图片:

15a93a0b6f834a70802cba5cec8d064b.png

       2.多线程解决抢占资源问题

        为了让服务端既可以与多个客户端创建链接的同时又可以与每个客户端建立起IO通道,所以这里也要用到多线程。

        创建一个线程任务CreateIO:将创建IO流的代码放在CreateIO中,在start()方法中实例化CreateIO对象并开始运行线程任务。这样做:避免了多个客户端抢占一个IO资源,同时也实现了服务端即可以与多个客户端连接,又可以接收并返回每一个客户端发送来的消息。

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Sever {
    private ServerSocket serverSocket;

    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
            //================================================================
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");
                CreatIo creatIo = new CreatIo(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
                                                            
           //================================================================

        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }
//================================================================
    private class CreatIo implements Runnable{
        private Socket socket;
        public CreatIo(Socket socket) {
            this.socket =socket;
        }

        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                // 创建输入流读取器
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                // 创建缓冲流
                BufferedReader br = new BufferedReader(isr);
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw=newOutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
                String message ;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    System.out.println("客户端说:" + message);
                    pw.println("服务器说:"+message);
                }
            } catch (IOException e) {
                //冷处理
            }
        }
    }
//================================================================

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

 测试结果如下:

5b25574342124bc7a3ccdf6f7370f1e0.png

b23a2152c2414edfb8fd3853bb691954.png

1421c3cd7b4e4e299fb051c539c5415a.png

        根据测试结果发现服务端既可以连接多个客户端,又可以接收并返回每一个客户端发送来的消息。到此顺利的完成了第四个任务:实现服务端能够与多个客户端链接  !!!

五、群聊

        通过上面的测试结果可以看出,虽然客户端可以发送消息并且可以接收到服务端返回的消息,但是客户端01看不到客户端02具体发送了什么内容,同样客户端02也看不到客户端01发送了什么内容,所以这小节的主要任务就是实现群聊,让每个客户端不仅仅只看到自己发出的内容,同时也可以看到其他客户端发送了什么内容。 

        写代码之前先整理一下思路:

        服务端把某一个客户端发来的消息,然后服务端把接收到的消息再转发给所有的客户端,这样就可以实现群聊了,如下图大致的思路就是这样,接下来用代码来具体实现一下。

7877c0591d6d439abfa7b6efe48fb0d2.png

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class Sever {
    private ServerSocket serverSocket;
//================================================================
    public List<PrintWriter> printWriterList = new ArrayList<>();
//================================================================
    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");
                CreateIO creatIo = new CreateIO(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    private class CreateIO implements Runnable{
        private Socket socket;
        public CreateIO(Socket socket) {
            this.socket =socket;
        }
        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                // 创建输入流读取器
                InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
                // 创建缓冲流
                BufferedReader br = new BufferedReader(isr);
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw=newOutputStreamWriter(out,StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
//================================================================
                printWriterList.add(pw);
//================================================================
                String message ;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    System.out.println("客户端说:" + message);
//================================================================
                    for (PrintWriter printWriter : printWriterList){
                        printWriter.println(message);
                    }
//================================================================
                }
            } catch (IOException e) {
                //冷处理
            }
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

最后实现的效果如下: 

c185f9c47e5f4f3a8d1ebba80c9d74b2.png

        这里创建了一个PrinterWriter类型的集合printerWriterList,用来存储每个客户端的输入流和输出流,遍历集合然后把消息通过每个客户端的IO流输出出去,让每个客户端都可以接收到消息,顺利的实现了群聊😄

 六、给客户端起个名字(Map)

        在之前的测试中发现每个用户的名称都叫客户端,在现实中不可能每个人的名字都是一模一样的,所以在这个小节中,要给每一个客户端都取一个独一无二的名字。接下来我们写一下代码,完成这个任务。

以下是服务端的代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Sever {
    private ServerSocket serverSocket;
//========================================================================
    private Map<String,PrintWriter> printWriterList = new HashMap<>();
//========================================================================
    public Sever() {//构造器
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");
                CreateIO creatIo = new CreateIO(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    private class CreateIO implements Runnable{
        private Socket socket;
//===================================================
        private String  clientName;
//===================================================
        public CreateIO(Socket socket) {
            this.socket =socket;
        }
        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                // 创建输入流读取器
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                // 创建缓冲流
                BufferedReader br = new BufferedReader(isr);
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
//========================================================
                clientName=br.readLine();
                printWriterList.put(clientName,pw);
//========================================================
                String message ;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    System.out.println(clientName+"说:"+message);//接收客户端发来的消息
//==========================================================================
                    //遍历每个客户端的输入输出流,把消息发给每个客户端
                    for (PrintWriter e: printWriterList.values()){
                        e.println(clientName+"说:"+message);
                    }
//==========================================================================
                }

            } catch (IOException e) {
                //冷处理
            }
        }
    }

    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

       每个客户端都有专有的IO通道与服务端交流,要给客户端起名字,就要把每个客户端的名字与其专有的IO通道匹配。为了达到这个效果,这里使用HashMap把之前的List集合取代,用Map的键(key)存储每个客户端的名称,用值(value)来存储每个客户端专有的IO通道,让每个客户端的名称和IO流匹配起来,一一对应。

        下面是客户端的代码:

        



package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {
    private Socket socket;

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }

    /**
     * 启动客户端,读取用户输入并发送到服务器
     * 此方法将一直运行,直到用户输入"exit"为止
     */
    public void start(){
        try {
            // 创建一个线程用于接收服务器的消息
            Thread thread = new Thread(new InMessage());
            // 启动线程
            thread.start();

            // 获取Socket的输出流
            OutputStream out = socket.getOutputStream();
            // 使用UTF-8字符集创建OutputStreamWriter
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            // 创建BufferedWriter以提高写入效率
            BufferedWriter bw = new BufferedWriter(osw);
            // 创建PrintWriter,设置自动刷新,用于向服务器发送消息
            PrintWriter pw = new PrintWriter(bw,true);
            // 创建Scanner对象以读取用户输入
            Scanner scanner = new Scanner(System.in);
            String data;
            // 提示用户输入内容
            System.out.println("请在控制台中输入内容,点击回车发送,输入”exit“退出:");
//==========================================================================
            System.out.println("请先输入你的昵称:");
            while (true){
                String nickname = scanner.nextLine();
                if (nickname.trim().isEmpty()){
                    System.out.println("名字不能为空!!请重新输入。。");
                }else {
                    pw.println(nickname);
                    break;
                }
            }
//==========================================================================
            // 无限循环,读取用户输入并发送到服务器,直到用户输入"exit"
            while (true){
                data = scanner.nextLine();
                if ("exit".equalsIgnoreCase(data)){
                    break;
                }
                pw.println(data);
            }
            // 用户输入"exit"后,打印退出消息
            System.out.println("退出成功!");
        } catch (IOException e) {
            //冷处理
        }finally {
            try {
                // 关闭Socket连接
                socket.close();
            } catch (IOException e) {
                //冷处理
            }
        }
    }
    // 内部类,实现Runnable接口,用于接收服务器的消息
    private class InMessage implements Runnable{
        public void run() {
            try {
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                String message ;
                while ((message = br.readLine()) != null){
//====================================================
                    System.out.println(message);
//====================================================
                }
            } catch (IOException e) {
                //冷处理
            }
        }
    }

    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
        // 启动客户端
        client.start();
    }
}

这里客户端也有一点小小的改动,提示让每个客户端上线的时候设定自己的昵称并且名字不可以为空,同时接收服务端返回的消息更加的简洁,测试效果如下:

6b66b252b2254f89a7c704bcd16311fc.png

        到这里,每个客户端都有了属于自己的名字, 利用HashMap顺利的完成了任务六,

任务六:给每个客户端定义一个名字,知道是谁发出来的信息(Map)

距离完成简易的小聊天室又更进了一步!!加油!马上就要完成了

        

七、私聊

     1.代码优化

        群聊的功能已经实现了,并且每个客户端都拥有了属于自己的名字,现在还差最后一个私聊任务,简易的小聊天室马上就要完成了!

        在完成私聊功能之前,我们把代码优化一下,把各个不同的功能从“屎山代码”中抽离出来,让代码的结构更加的清晰,同时也增加了代码的扩展性,接下来代码会有一些小的调整,大家仔细比对一下。

        大家这里一定要有耐心阅读、比对,马上就要完成了,后面的代码改动有点大因为涉及到,所有任务就要完成了,一定要耐住性子!!!一定!

        服务端代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Sever {
    private ServerSocket serverSocket;
    private Map<String,PrintWriter> printWriterList = new HashMap<>();

    public Sever() {//利用无参构造器,在Sever实例化的同时创建端口
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");

                CreateIO creatIo = new CreateIO(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    private class CreateIO implements Runnable{
        private Socket socket;
        private String  clientName;
//===============================================
        private String host;//获取客户端的ip地址
//===============================================
        public CreateIO(Socket socket) {//接收客户端传来的socket对象
            this.socket =socket;
//===============================================================
            this.host = socket.getInetAddress().getHostAddress();
//===============================================================
        }
        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //创建输出流,将消息返回给服务端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
//===============================================================
                getNameAndSendMessage(br,pw);
//===============================================================
            } catch (IOException e) {
                //冷处理
            }
        }
//=============================================================================
        public void getNameAndSendMessage(BufferedReader br,PrintWriter pw){
            try {
                //获取客户端的昵称
                clientName=br.readLine();
                //存储每个客户端的昵称和输出流
                printWriterList.put(clientName,pw);
                String message;

                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    //接收客户端发来的消息
                    System.out.println(clientName+" ["+host+"] 说:"+message);
                    //遍历每个客户端的输入输出流,把消息发给每个客户端
                    
                    messageToAll(message);
                }
            } catch (IOException e) {
                //冷处理
            }
        }


        //遍历输出流,将消息发送到每个客户端上,实现群聊
        public void messageToAll(String message){
            for (PrintWriter e: printWriterList.values()){
                e.println(clientName+" ["+host+"] 说:"+message);
            }
        }

//=============================================================================

    }



    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

        变动位置提醒:

                ①:添加了一个host属性获取客户端的ip地址,并添加到CreateIO的构造方法中

                ②:把CreateIO中的接收客户端名字和发送信息的功能提取出来,放到新创建的getNameAndSendMessage方法中

                ③:再把群聊功能提取出来放在messageToAll方法中

        变动的位置我用分隔线给大家分割出来了,根据变动位置提醒,大家好好对照一下,这里我已经测试过了,是可以正常运行的,如果大家变动之后发现不能正常运行,一定要仔细比对一下。

  2.私聊

大家测试正常之后开始完成本小节的任务:群聊

下面是服务端代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Sever {
    private ServerSocket serverSocket;
    private Map<String,PrintWriter> printWriterList = new HashMap<>();

    public Sever() {//利用无参构造器,在Sever实例化的同时创建端口
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");

                CreateIO creatIo = new CreateIO(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    private class CreateIO implements Runnable{
        private Socket socket;
        private String  clientName;
        private String host;//获取客户端的ip地址
        public CreateIO(Socket socket) {//接收客户端传来的socket对象
            this.socket =socket;
            this.host = socket.getInetAddress().getHostAddress();
        }
        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //创建输出流,将消息返回给服务端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
                //获取客户端的名字和发来的信息
                getNameAndSendMessage(br,pw);
            } catch (IOException e) {
                //冷处理
            }
//========================================================添加了synchronized和finally
finally {
                synchronized (printWriterList) {
                    /*
                    HashMap不是线程安全的。如果多个线程同时对printWriterList实例进行修改,
                    可能会导致数据的不一致性,甚至可能导致死循环或数据丢失。所以使用synchronized,解决并发安全问题
                     */
                    printWriterList.remove(clientName);
                }
                    messageToAll("已下线,现在剩余人数:"+printWriterList.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    //冷处理
                }
            }
        }
//===================================================================================



        public void getNameAndSendMessage(BufferedReader br,PrintWriter pw){
            try {
                //获取客户端的昵称
                clientName=br.readLine();
                //存储每个客户端的昵称和输出流
//========================================================添加了synchronized
                synchronized (printWriterList) {
                    printWriterList.put(clientName, pw);
                }
//========================================================
                messageToAll("上线了,当前在线人数:"+printWriterList.size());
                String message;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    //遍历每个客户端的输入输出流,把消息发给每个客户端
                    if (message.startsWith("@")){
                        //如果对方发出的内容以@开头,则执行私聊
                        messageToSomeone(message);
                    }else{//否则就是群聊
                        messageToAll(message);
                    }

                }
            } catch (IOException e) {
                //冷处理
            }
        }


        //遍历输出流,将消息发送到每个客户端上,实现群聊
        private void messageToAll(String message){
            //接收客户端发来的消息
            System.out.println(clientName+" ["+host+"] 说:"+message);
//==============================================添加了synchronized
            synchronized (printWriterList) {
                /*
                    新循环遍历集合,属于利用迭代器原理,在便利的过程中不允许集合进行增删操作
                    为了正确遍历,防止客户端同时输入内容,这里要用synchronized,保证线程同步处理
                 */
                for (PrintWriter e : printWriterList.values()) {
                    e.println(clientName + " [" + host + "]:" + message);
                }
            }
//==============================================
        }
//=======================================================================================
        //输入正确的聊天格式,实现私聊
        private void messageToSomeone(String message){
            //私聊格式案例: @xx : 完成私聊任务
            //如果输入的私聊格式正确,并且私聊的那个人在线的话,那么就可以正常的私聊
            if (message.matches("@.+:.+")){
                String anotherName = message.substring(1,message.indexOf(":"));
                if(printWriterList.containsKey(anotherName)){
                    PrintWriter pw = printWriterList.get(anotherName);//获取对方的输出流
                    String toMessage = message.substring(message.indexOf(":")+1);//把对方发送的内容提取出来
                    pw.println(clientName+" ["+host+"] 悄悄的对你说:"+toMessage);//通过缓冲输出流发送给对方
                }else{
                    PrintWriter pw = printWriterList.get(clientName);
                    pw.println(anotherName+"对方不在线。。。");
                }
            }else {
                PrintWriter pw = printWriterList.get(clientName);
                pw.println("格式输入不正确,请输入正确格式:@xx:聊天内容 或 @xx:聊天内容");
            }
        }

    }
//=======================================================================================


    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

大致思路:

        ① 这里增加了私聊的方法messageToSomeone,在平时聊天的时候想要与人私聊会有一

个很醒目的符号“@”,所以这里我们使用了这个判断机制。

        ② 如果客户端发来的内容第一个符号是@,那么就会先进行判断,

        调用messageToSomeone方法,通过正则检测输入格式,

        ③ 如果输入格式正确并且用户名存在,则正常发送消息给对方,

        否则提示对方不在线或者输入格式不正确。

效果演示:

3.并发安全问题的处理

        代码中有很多对HashMap以及对新循环遍历的操作,因为HashMap和新循环不是并发安全

的,所以在遍历和对HashMap的增删操作的时候要用synchronized解决并发安全问题。

八、代码汇总

到这里简易的聊天室已经完成了,把代码给大家总结在这里,如果有需要的话可以看一下,代码已经测试过了,任务里的小功能都可以实现的,也恭喜大家顺利的完成了一个小项目!!✌

服务器代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class Sever {
    private ServerSocket serverSocket;
    private Map<String,PrintWriter> printWriterList = new HashMap<>();

    public Sever() {//利用无参构造器,在Sever实例化的同时创建端口
        try {
            System.out.println("正在创建服务端。。");
            serverSocket = new ServerSocket(8086);//监听8086端口
            System.out.println("服务端创建成功。。");
        } catch (IOException e) {
            //冷处理
        }
    }
    /**
     * 启动服务端并等待客户端连接
     * 该方法尝试启动服务端 socket 监听,并在接到客户端连接后打印客户端发送的消息
     * 注意:该方法将阻塞等待客户端连接,并在连接建立后持续读取客户端发送的数据
     */
    public void start() {
        try {
            while (true) {
                System.out.println("服务端启动成功,正在等待客户端连接。。。");
                // 等待客户端链接
                Socket socket = serverSocket.accept();
                System.out.println("客户端连接成功");

                CreateIO creatIo = new CreateIO(socket);
                Thread thread = new Thread(creatIo);
                thread.start();
            }
        } catch (IOException e) {
            // 冷处理:这里没有处理异常,可能需要在实际应用中添加适当的异常处理逻辑
        }
    }

    private class CreateIO implements Runnable{
        private Socket socket;
        private String  clientName;
        private String host;//获取客户端的ip地址
        public CreateIO(Socket socket) {//接收客户端传来的socket对象
            this.socket =socket;
            this.host = socket.getInetAddress().getHostAddress();
        }
        public void run() {
            try {
                // 获取客户端发送的输入流
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                //创建输出流,将消息返回给服务端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw,true);
                //获取客户端的名字和发来的信息
                getNameAndSendMessage(br,pw);
            } catch (IOException e) {
                //冷处理
            }finally {
                synchronized (printWriterList) {
                    /*
                    HashMap不是线程安全的。如果多个线程同时对printWriterList实例进行修改,
                    可能会导致数据的不一致性,甚至可能导致死循环或数据丢失。所以使用synchronized,解决并发安全问题
                     */
                    printWriterList.remove(clientName);
                }
                    messageToAll("已下线,现在剩余人数:"+printWriterList.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    //冷处理
                }
            }
        }

        public void getNameAndSendMessage(BufferedReader br,PrintWriter pw){
            try {
                //获取客户端的昵称
                clientName=br.readLine();
                //存储每个客户端的昵称和输出流
                synchronized (printWriterList) {
                    printWriterList.put(clientName, pw);
                }
                messageToAll("上线了,当前在线人数:"+printWriterList.size());
                String message;
                // 循环读取客户端发送的消息,直到消息为空
                while ((message = br.readLine()) != null){
                    //遍历每个客户端的输入输出流,把消息发给每个客户端
                    if (message.startsWith("@")){
                        //如果对方发出的内容以@开头,则执行私聊
                        messageToSomeone(message);
                    }else{//否则就是群聊
                        messageToAll(message);
                    }

                }
            } catch (IOException e) {
                //冷处理
            }
        }


        //遍历输出流,将消息发送到每个客户端上,实现群聊
        private void messageToAll(String message){
            //接收客户端发来的消息
            System.out.println(clientName+" ["+host+"] 说:"+message);
            synchronized (printWriterList) {
                /*
                    新循环遍历集合,属于利用迭代器原理,在便利的过程中不允许集合进行增删操作
                    为了正确遍历,防止客户端同时输入内容,这里要用synchronized,保证线程同步处理
                 */
                for (PrintWriter e : printWriterList.values()) {
                    e.println(clientName + " [" + host + "]:" + message);
                }
            }
        }

        //输入正确的聊天格式,实现私聊
        private void messageToSomeone(String message){
            //私聊格式案例: @xx : 完成私聊任务
            //如果输入的私聊格式正确,并且私聊的那个人在线的话,那么就可以正常的私聊
            if (message.matches("@.+:.+")){
                String anotherName = message.substring(1,message.indexOf(":"));
                if(printWriterList.containsKey(anotherName)){
                    PrintWriter pw = printWriterList.get(anotherName);//获取对方的输出流
                    String toMessage = message.substring(message.indexOf(":")+1);//把对方发送的内容提取出来
                    pw.println(clientName+" ["+host+"] 悄悄的对你说:"+toMessage);//通过缓冲输出流发送给对方
                }else{
                    PrintWriter pw = printWriterList.get(clientName);
                    pw.println(anotherName+"对方不在线。。。");
                }
            }else {
                PrintWriter pw = printWriterList.get(clientName);
                pw.println("格式输入不正确,请输入正确格式:@xx:聊天内容 或 @xx:聊天内容");
            }
        }

    }



    public static void main(String[] args) {
        Sever sever = new Sever();
        sever.start();
    }

}

 客户端代码:

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * Client类代表了一个简单的客户端,用于连接到服务器
 */
public class Client {
    private Socket socket;

    /**
     * 构造函数,尝试连接到指定的服务器
     * 此处隐藏了IOException异常,因为构造函数中不建议直接处理网络连接的异常
     * 应该在更高层次处理这些异常
     */
    public Client() {
        try {
            // 打印连接服务器的消息
            System.out.println("正在链接服务端。。请稍后。。");
            // 创建Socket对象,连接到本地主机的8086端口
            socket = new Socket("localhost",8086);
            // 连接成功后打印消息
            System.out.println("链接成功!");
        } catch (IOException e) {
            // 冷处理异常,此处不进行任何操作,应考虑更合适的异常处理方式
        }
    }

    /**
     * 启动客户端,读取用户输入并发送到服务器
     * 此方法将一直运行,直到用户输入"exit"为止
     */
    public void start(){
        try {
            // 创建一个线程用于接收服务器的消息
            Thread thread = new Thread(new InMessage());
            // 启动线程
            thread.start();

            // 获取Socket的输出流
            OutputStream out = socket.getOutputStream();
            // 使用UTF-8字符集创建OutputStreamWriter
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            // 创建BufferedWriter以提高写入效率
            BufferedWriter bw = new BufferedWriter(osw);
            // 创建PrintWriter,设置自动刷新,用于向服务器发送消息
            PrintWriter pw = new PrintWriter(bw,true);
            // 创建Scanner对象以读取用户输入
            Scanner scanner = new Scanner(System.in);
            String data;
            // 提示用户输入内容
            System.out.println("请在控制台中输入内容,点击回车发送,输入”exit“退出:");
            System.out.println("请先输入你的昵称:");
            while (true){
                String nickname = scanner.nextLine();
                if (nickname.trim().isEmpty()){
                    System.out.println("名字不能为空!!请重新输入。。");
                }else {
                    pw.println(nickname);
                    break;
                }
            }
            // 无限循环,读取用户输入并发送到服务器,直到用户输入"exit"
            while (true){
                data = scanner.nextLine();
                if ("exit".equalsIgnoreCase(data)){
                    break;
                }
                pw.println(data);
            }
            // 用户输入"exit"后,打印退出消息
            System.out.println("退出成功!");
        } catch (IOException e) {
            //冷处理
        }finally {
            try {
                // 关闭Socket连接
                socket.close();
            } catch (IOException e) {
                //冷处理
            }
        }
    }
    // 内部类,实现Runnable接口,用于接收服务器的消息
    private class InMessage implements Runnable{
        public void run() {
            try {
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                String message ;
                while ((message = br.readLine()) != null){
                    System.out.println(message);
                }
            } catch (IOException e) {
                //冷处理
            }
        }
    }

    /**
     * 程序的入口点
     * 创建一个Client实例来演示如何连接到服务器
     * @param args 命令行参数,未使用
     */
    public static void main(String[] args) {
        // 创建Client对象以连接服务器
        Client client = new Client();
        // 启动客户端
        client.start();
    }
}

 到这里就结束了,以后可能还会继续扩展,大家有什么问题可以在评论区留言,一起探讨!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值