一、实验描述

本实验实现基于TCP的多人聊天室。多个用户同时访问服务端,各自可以不断请求服务端获取响应的数据,并可以实现群聊和私聊功能。服务器则实现对数据的转发功能。

本文工具:IDEA2020、JDK1.8

本文源代码在文章末尾

二、分析

1.客户端

(1)数据发送
(2)数据接收
技术:socket、输入流和输出流、多线程
聊天:群聊、私聊
注意:私聊格式:@服务器用户ID号:msg

2.服务端

(1)数据转发
(2)用户注册
技术:ServerScoket、每个用户对应Socket对象、多线程同时在线
数据转发:私聊前缀判断、群聊所有人发送
三、创建客户端

创建完客户端结构如下:

(1)创建基础项目

打开IDEA,点击File—>New—>Project

下面这里选择Java,然后右侧选择SDK版本,这里选择1.8版本

下一步输入Project name,这里 项目名称为 ClientWithTCP,右下角选择“Finish”

右击src文件夹,选择New—>Package

这里创建一个包,包名为com.guo.www (或者其他包名)

(2)创建工具类

主要用于结束所有的连接。

右击www文件夹,选择New—>Java Class

名字为ChatUtils,参考代码如下:

import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.Socket;

public class ChatUtils {
    /**
     * 安全关闭多个资源(支持可变参数,自动处理null和异常)
     * @param resources 可变参数,可以是 DataOutputStream、DataInputStream、BufferedReader、Socket 等
     */
    public static void close(AutoCloseable... resources) {
        if (resources == null) {
            return;
        }

        for (AutoCloseable resource : resources) {
            try {
                if (resource != null) {
                    resource.close();
                }
            } catch (Exception e) {
                System.err.println("关闭资源时发生异常: " + e.getMessage());
            }
        }
    }

}

(3)编写客户端发送线程类

右击www文件夹,选择New—>Java Class

名字为SendThreadClient

参考代码如下:


import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class SendThreadClient implements Runnable{
    private BufferedReader reader;
    private DataOutputStream dos;
    private Socket clientSocket;
    private boolean isRunning;
    private String name;

    public SendThreadClient(Socket client,String name){
        this.clientSocket=client;
        this.isRunning=true;
        this.name=name;
        reader=new BufferedReader(new InputStreamReader(System.in));
        try {
            dos=new DataOutputStream(client.getOutputStream());
            //发送用户给服务器,用户名用于注册,需要调用send方法
            send(name);
        }catch (IOException ex){
            ex.printStackTrace();
            //释放资源
            release();
        }
    }
    /**
     *
     * 线程代码,只要当前连接状态,就一直读取字符串和发送信息
     * */
    @Override
    public void run(){
        while (isRunning){
            String msg = null;
            try {
                msg=reader.readLine();
            }catch (IOException e){
                System.out.println("数据写入失败"+e.toString());
                release();
            }
            send(msg);
        }
    }


    /***
     *
     * @param msg 需要发送的信息
     */
    public void send(String msg){
        try {
            System.out.println("本机发送信息:"+msg);
            dos.writeUTF(msg);
            dos.flush();
        }catch (IOException e){
            System.out.println("数据发送失败"+e.toString());
            release(); //释放资源
        }
    }
    //释放资源
    public void release(){
        ChatUtils.close(dos,clientSocket,reader);
    }

}

(4)编写客户端接收线程类

右击www文件夹,选择New—>Java Class

名字为ReceiveThreadClient,

参考代码如下:


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

public class ReceiveThreadClient implements Runnable{
    private boolean isRunning;
    private DataInputStream dis;
    private Socket client;
    /**
     * 构造方法
     * */
    public ReceiveThreadClient(Socket client){
        this.client=client;
        this.isRunning=true;
        try {
            dis= new DataInputStream(client.getInputStream());
        }catch (IOException ex){
            System.out.println("DataInputStream对象创建失败");
            release();
        }
    }

    @Override
    public void run(){
        while (isRunning){
            String msg = "";
            msg=receive();
            if(!msg.equals("")){
                System.out.println(msg);
            }
        }
    }

    /***
     * 从服务端接收数据
     * @return
     */
    public String receive(){
        String msg=null;
        try {
            msg=dis.readUTF();
            return msg;
        }catch (IOException e){
            System.out.println("数据接收失败");
            release();
        }
        return "";
    }

    //释放资源
    public void release(){
        isRunning=false;
        ChatUtils.close(dis,client);
    }
}

(5)编写客户端类

右击www文件夹,选择New—>Java Class

名字为ClientMultiUser

参考代码如下:



import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ClientMultiUser {
    static String ServerIP="127.0.0.1";
    static  int Port=8888;

    public static  void main (String[] args) throws IOException{
        System.out.println("TCP客户端启动");
        System.out.println("请输入用户名:");
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String name = reader.readLine(); //控制台输入数据

        //创建Socket,绑定服务器IP地址和端口
        Socket client = new Socket(ServerIP,Port);
        //使用线程池管理两个线程,一个是发送线程,一个是接收线程
        ExecutorService pool = Executors.newFixedThreadPool(2);

        pool.submit(new SendThreadClient(client,name));  //发送线程
        pool.submit(new ReceiveThreadClient(client)); //接收线程

    }
}
四、创建服务端

服务端建议重新创建一个新项目,单独运行。项目名称为ReceiveWithTCP

前面的都一样,创建包和ChatUtils工具类,整个项目结构如下:

右击www文件,选择New—>Java Class

类名为ServerMultiUser,参考代码如下:


import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;

public class ServerMultiUser {
    //用于存储所有客户端的一个容器,涉及多线程的并发操作
    //使用CopyOnWriteArrayList保证线程安全
    private static CopyOnWriteArrayList<Channel> allClient= new CopyOnWriteArrayList<Channel>();

    public static void main(String[] args) throws Exception{
        System.out.println("服务端启动");
        //建立ServerSocket对象,并绑定本地端口
        ServerSocket server = new ServerSocket(8888);
        //一直循环,接收来自多个客户端的请求
        while (true){
            //监听
            Socket clientSocket = server.accept();
            Channel client = new Channel(clientSocket);
            allClient.add(client);
            System.out.println(client.name+"建立了连接");
            new Thread(client).start();
        }
    }


     static class Channel implements Runnable {
        private DataInputStream dis;
        private DataOutputStream dos;
        private Socket client;
        private boolean isRunning;
        private String name;
        //构造方法
        public Channel(Socket client){
            this.client = client;
            this.isRunning = true;
            try{
                dis = new DataInputStream(client.getInputStream());
                dos=new DataOutputStream(client.getOutputStream());
                this.name=reveive();//接收客户端的名称
                this.send("欢迎来到聊天室。。。");
                this.sendOther(this.name+"来到了聊天室。。。",true);
            }catch (IOException e){
                release(); //释放资源
            }
        }

        @Override
        public void run(){
            while (isRunning){
                String msg= reveive();
                if(!msg.equals("")){
                    sendOther(msg,false);
                }
            }
        }

        //发送数据
         public  void send(String msg){
            try {
                dos.writeUTF(msg);
                dos.flush();
            }catch (IOException e){
                System.out.println("数据发送失败");
                release();
            }
         }

         /**
          * 获取自己的消息,然后发送给其他人
          * isSys 表示是否为系统消息
          * 私聊:可以向某一特定的用户发送数据
          * 约定格式:@服务器用户ID:消息
          */
         public void sendOther(String msg,boolean isSys){
            boolean isPrivate = msg.startsWith("@");
            if(isPrivate){
                //寻找冒号
                int index = msg.indexOf(":");
                //截取ID
                String targetName = msg.substring(1,index);
                //截取消息内容
                String datas = msg.substring(index+1);
                for(Channel other:allClient){
                    if(other.name.equals(targetName)){
                        other.send(this.name+"悄悄对你说:"+datas);
                    }
                }
            }else {
                for(Channel other:allClient){
                    if(other==this){
                        //自己不能给自己发
                        continue;
                    }
                    if(!isSys){
                        other.send(this.name+"对大家说:"+msg);
                    }else{
                        other.send(msg);
                    }
                }
            }
         }
         //接收数据
         public String reveive(){
            try {
                String msg = "";
                msg=dis.readUTF();
                return msg;
            }catch (IOException e){
                isRunning =false;
                System.out.println("接收数据失败");
                release();
            }
            return "";
         }

         //释放资源
         public void release(){
            this.isRunning=false;
            ChatUtils.close();
            allClient.remove(this);
            this.sendOther(this.name+"离开了聊天室...",true);
         }
     }


}
五、测试效果

先启动服务端,然后复制两个客户端,单独启动,分别在客户端发送数据,观察其他客户端接收数据的情况

最左侧是服务端,中间是第一个客户端,最右边是第二个客户端

效果如下:

服务端源码(访问密码: 5241):ReceiveWithTCP.zip: https://url47.ctfile.com/f/64055047-1508351275-1f6e0f?p=5241

客户端源码(访问密码: 6423):ClientWithTCP.zip: https://url47.ctfile.com/f/64055047-1508351575-000eff?p=6423