多线程及TCP通信 实例之 聊天室

多线程及TCP通信 实例之 聊天室
使用Java的Socket实现客户端和服务端之间的连接,并使得客户端向服务端发送一条消息。
步骤:

1、 创建客户端类

/**
 * 客户端应用程序
 * 
 * @author Administrator
 *
 */
public class Client {
    ....
}

2、 创建Socket对象

// Socket 用于连接服务器的ServerSocket
    private Socket socket;

    /**
     * 客户端构造方法,用于初始化客户端
     */

    public Client() throws Exception {
        try {
            /**
             * 创建Socket对象时,就会尝试根据给定的 地址与端口连接服务端。 所以,若该对象创建成功,说明与服务端连接正常。
             */
            System.out.println("正在连接服务器。。。");
            socket = new Socket(
                    "localhost", 8088);

        } catch (Exception e) {
            throw e;
        }
    }

3、创建客户端方法 start()

/**
     * 客户端启动方法
     */
    public void start() {
        try {
            //创建并启动线程,来接收服务端的信息
            Runnable runn=new GetServerInfoHandler();

            Thread t=new Thread(runn);
            t.start();

            OutputStream out = socket.getOutputStream();
            /**
             * 使用字符流来格局指定的编码集将字符串转换为字节后 在通过out发送给服务器端
             */
            OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");

            /**
             * 将字符流包装为缓冲字符流,就可 以按行为单位写出字符串了
             */
            PrintWriter pw = new PrintWriter(osw,true);
            /**
             * 创建一个Scanner,用于接收
             * 用户的输入的字符串
             */
            Scanner scanner = new Scanner(System.in);

            //输出欢迎用语
            System.out.println("欢迎来到小邓的聊天室");
            while(true){
            /**
             * 首先输入昵称
             */
            System.out.println("请输入昵称:");
                String nickname=scanner.nextLine();
                if(nickname.trim().length()>0){
                    pw.println(nickname);
                    break;
                }
                System.out.println("昵称不能为空!");
            }
            //循环读取并输出用户输入的内容
            while (true) {
                String str = scanner.nextLine();
                pw.println(str);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

4、为客户端类定义main()方法

public static void main(String[] args) {
        try {
            Client client = new Client();
            client.start();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("客户端初始化失败!");
        }
    }
    /**
     * 该线程的作用是循环接收服务端发送来的信息,并
     * 输出到控制台来
     * @author Administrator
     *
     */

5、创建线程实现字符串的输出

class GetServerInfoHandler implements Runnable{

        public void run() {
             try{
                 /**
                  * 通过Socket获取输入流
                  */
                 InputStream in=socket.getInputStream();

                 InputStreamReader isr=new InputStreamReader(in,"utf-8");
                 //将字符流转换为缓冲流
                 BufferedReader br=new BufferedReader(isr);

                 String message=null;
                 //循环读取服务端发送的每一个字符串
                 while ((message=br.readLine())!=null){
                    //将服务端发送的字符串输出到控制台
                     System.out.println(message);
                 }

             }catch(Exception e){

             }

        }

    }

6、 创建服务器端类

/**
 * 服务端应用程序
 * 
 * @author Administrator
 *
 */
public class Server {
    ....
}

7、 创建ServerSocket类的对象,构造方法,创建线程池,集合

// 运行在服务端的Socket
    private ServerSocket server;
    // 线程池,用于管理客户端连接的交互线程
    private ExecutorService threadPool;
    // 保存所有客户端输出流的集合
    private List<PrintWriter> allOut;

    /**
     * 构造方法,用于初始化服务端
     */
    public Server() {
        try {
            System.out.println("初始化服务端");
            server = new ServerSocket(8088);
            // 初始化线程池
            threadPool = Executors.newFixedThreadPool(50);

            // 初始化存放所有客户端输出流的集合
            allOut = new ArrayList<PrintWriter>();

            System.out.println("服务端初始化完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

8、创建服务端工作方法 start()

/**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端连接。。。");
                // ServerSocket的accept方法
                /**
                 * 用于监听8088端口,等待客户端的连接 该方法是阻塞方法,直到一个客户端连接,否则发I方法一直紫塞
                 * 若一个客户端连接了,会返回该客户端的Socket
                 */
                Socket socket = server.accept();
                /**
                 * 当一个客户端连接后,启动一个线程ClientHandler,将 该客户端的Socket传入,使得该线程处理与该客户端的交互
                 * 这样,我们能再次进入循环,接收下一个客户端的连接了
                 */
                Runnable handler = new ClientHandler(socket);

                // Thread t=new Thread(handler);
                /**
                 * 使用线程池分配空闲线程来处理 当前连接的客户端
                 */
                threadPool.execute(handler);
                // t.start();


            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

9、 为服务端类定义main 方法


    public static void main(String[] args) {

        Server server;
        server = new Server();
        server.start();
    }

10、实现消息通信及转发

/**
     * 将给定的输出流存入共享集合 加synchronized锁,使异步操作变成同步操作
     * 
     * 在不同一方法加上synchronized之后,变成互斥锁 只能执行一个方法,此时可保存线程安全
     */
    public synchronized void addOut(PrintWriter pw) {
        allOut.add(pw);

    }

    /**
     * 将给定的输出流从共享集合中删除 加synchronized锁,使异步操作变成同步操作
     * 
     * @param pw
     */
    public synchronized void removeOut(PrintWriter pw) {
        allOut.remove(pw);

    }

    /**
     * 将给定的消息转发给所有客户端 加synchronized锁,使异步操作变成同步操作
     * 
     * @param message
     */
    public synchronized void sendMessage(String message) {
        for (PrintWriter pw : allOut) {
            pw.println(message);

        }

    }
    /**
     * 服务端中的一个线程,用于与某个客户交互 使用线程的目的是使得服务端可以处理多个客户端了。
     * 
     * @author Administrator
     *
     */
    // 创建线程
    class ClientHandler implements Runnable {
        // 当前线程处理的客户端的Socket
        private Socket socket;

        // 当前客户端的IP
        private String ip;

        // 当前用户的昵称
        private String nickname;

        /**
         * 根据给定的客户端的Socket,创建线程体
         * 
         * @param socket
         */
        public ClientHandler(Socket socket) {
            this.socket = socket;

            // 获取远端的的地址
            InetAddress address = socket.getInetAddress();

            // 获取本端的地址信息
            // socket.getLocalAddress();

            // 获取远端的计算机ip地址
            ip = address.getHostAddress();
            // 获取客户端的端口号
            int port = socket.getPort();

        }

        /**
         * 该线程会将Socket中的输入流获取 用来循环读取客户端发送过来的消息
         */
        public void run() {
            /**
             * 定义在try语句外的目的是,为了在 finally中也可以引用到
             */
            PrintWriter pw = null;
            try {
                /**
                 * 为了让服务端与客户端发送信息, 我们需要通过socket获取输出流
                 */
                OutputStream out = socket.getOutputStream();

                // 转换为字符流,用于指定编码集
                OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");

                // 创建缓冲字符输出流
                pw = new PrintWriter(osw, true);

                /**
                 * 将该客户端的输出流存入共享集合以便 使得该客户端也能接收服务端转发的 消息
                 */

                // 存在线程安全问题,不推荐
                // allOut.add(pw);

                addOut(pw);

                // 是输出在线人数
                System.out.println("当前在线人数为:" + allOut.size());

                // System.out.println("客户端连接成功");

                /**
                 * 通过刚刚连上的客户端的Socket获取输入流 读取客户端发送的· 数据
                 */
                InputStream in = socket.getInputStream();

                InputStreamReader isr = new InputStreamReader(in, "utf-8");
                /**
                 * 将字符流转换为缓冲字符输入流 这样就可以以行为单位读取字符串了
                 */
                BufferedReader br = new BufferedReader(isr);

                /**
                 * 当创建好当前客户端的输入流后 读取的第一个字符串,应当是昵称
                 * 
                 */
                nickname = br.readLine();

                // 通知所有客户端,当前用户上线了

                sendMessage("[" + nickname + "]上线了");

                // 读取客户端发过来的字符串
                /**
                 * windows与linux存在一定的差异: linux:当客户端与服务端断开连接后
                 * 我们通过输入流会读到null但这是合乎逻辑的,因为缓冲流的
                 * readLine()方法若返回null就表示无法通过该流再读取到 信息。参考之前服务文本文件的判断
                 * windows:当客户端与服务端断开连接后 readLine()方法会抛出异常
                 * 
                 */
                String message = null;

                while ((message = br.readLine()) != null) {
                    //pw.println(message);
                    /**
                     * 当读取到客户端发送过来的一条消息后, 将该消息装发给所有客户端
                     */
                    // 存在线程安全问题,遍历过程中不允许有删改
                    // for(PrintWriter o:allOut){
                    // o.println(message);
                    // }

                    sendMessage(nickname + "说:" + message);
                }

            } catch (Exception e) {
                // 在windows中的客户端,报错通常是因为客户端断开了连接
                // 通知其他用户,该用户下线了
                sendMessage("[" + nickname + "]下线了");
            } finally {
                /**
                 * 首先将该客户端的输出流从共享 集合中删除。
                 */
                // 存在现存安全问题
                // allOut.remove(pw);

                removeOut(pw);

                // 输出当前在线人数
                System.out.println("当前在线人数为:" + allOut.size());
                // 通知其他用户,该用户下线了
                sendMessage("[" + nickname + "]下线了");
                /**
                 * 无论Linux还是windows用户,当与服务器断开连接后 我们都应该在服务器断开与客户端断开连接
                 */
                try {
                    socket.close();
                } catch (IOException e) {
                    System.out.println("一个客户下线了。。。");
                }

            }

        }

    }

11、完整代码

Client类

package day06;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

/**
 * 客户端应用程序
 * 
 * @author Administrator
 *
 */
public class Client {
    // Socket 用于连接服务器的ServerSocket
    private Socket socket;

    /**
     * 客户端构造方法,用于初始化客户端
     */

    public Client() throws Exception {
        try {
            /**
             * 创建Socket对象时,就会尝试根据给定的 地址与端口连接服务端。 所以,若该对象创建成功,说明与服务端连接正常。
             */
            System.out.println("正在连接服务器。。。");
            socket = new Socket(
                    "localhost", 8088);

        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * 客户端启动方法
     */
    public void start() {
        try {
            //创建并启动线程,来接收服务端的信息
            Runnable runn=new GetServerInfoHandler();

            Thread t=new Thread(runn);
            t.start();

            OutputStream out = socket.getOutputStream();
            /**
             * 使用字符流来格局指定的编码集将字符串转换为字节后 在通过out发送给服务器端
             */
            OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");

            /**
             * 将字符流包装为缓冲字符流,就可 以按行为单位写出字符串了
             */
            PrintWriter pw = new PrintWriter(osw,true);
            /**
             * 创建一个Scanner,用于接收
             * 用户的输入的字符串
             */
            Scanner scanner = new Scanner(System.in);

            //输出欢迎用语
            System.out.println("欢迎来到小邓的聊天室");
            while(true){
            /**
             * 首先输入昵称
             */
            System.out.println("请输入昵称:");
                String nickname=scanner.nextLine();
                if(nickname.trim().length()>0){
                    pw.println(nickname);
                    break;
                }
                System.out.println("昵称不能为空!");
            }
            //循环读取并输出用户输入的内容
            while (true) {
                String str = scanner.nextLine();
                pw.println(str);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        try {
            Client client = new Client();
            client.start();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("客户端初始化失败!");
        }
    }
    /**
     * 该线程的作用是循环接收服务端发送来的信息,并
     * 输出到控制台来
     * @author Administrator
     *
     */
    class GetServerInfoHandler implements Runnable{

        public void run() {
             try{
                 /**
                  * 通过Socket获取输入流
                  */
                 InputStream in=socket.getInputStream();

                 InputStreamReader isr=new InputStreamReader(in,"utf-8");
                 //将字符流转换为缓冲流
                 BufferedReader br=new BufferedReader(isr);

                 String message=null;
                 //循环读取服务端发送的每一个字符串
                 while ((message=br.readLine())!=null){
                    //将服务端发送的字符串输出到控制台
                     System.out.println(message);
                 }

             }catch(Exception e){

             }

        }

    }
}

Server类

package day06;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务端应用程序
 * 
 * @author Administrator
 *
 */
public class Server {
    // 运行在服务端的Socket
    private ServerSocket server;
    // 线程池,用于管理客户端连接的交互线程
    private ExecutorService threadPool;
    // 保存所有客户端输出流的集合
    private List<PrintWriter> allOut;

    /**
     * 构造方法,用于初始化服务端
     */
    public Server() {
        try {
            System.out.println("初始化服务端");
            server = new ServerSocket(8088);
            // 初始化线程池
            threadPool = Executors.newFixedThreadPool(50);

            // 初始化存放所有客户端输出流的集合
            allOut = new ArrayList<PrintWriter>();

            System.out.println("服务端初始化完毕");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务端开始工作的方法
     */
    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端连接。。。");
                // ServerSocket的accept方法
                /**
                 * 用于监听8088端口,等待客户端的连接 该方法是阻塞方法,直到一个客户端连接,否则发I方法一直紫塞
                 * 若一个客户端连接了,会返回该客户端的Socket
                 */
                Socket socket = server.accept();
                /**
                 * 当一个客户端连接后,启动一个线程ClientHandler,将 该客户端的Socket传入,使得该线程处理与该客户端的交互
                 * 这样,我们能再次进入循环,接收下一个客户端的连接了
                 */
                Runnable handler = new ClientHandler(socket);

                // Thread t=new Thread(handler);
                /**
                 * 使用线程池分配空闲线程来处理 当前连接的客户端
                 */
                threadPool.execute(handler);
                // t.start();


            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 将给定的输出流存入共享集合 加synchronized锁,使异步操作变成同步操作
     * 
     * 在不同一方法加上synchronized之后,变成互斥锁 只能执行一个方法,此时可保存线程安全
     */
    public synchronized void addOut(PrintWriter pw) {
        allOut.add(pw);

    }

    /**
     * 将给定的输出流从共享集合中删除 加synchronized锁,使异步操作变成同步操作
     * 
     * @param pw
     */
    public synchronized void removeOut(PrintWriter pw) {
        allOut.remove(pw);

    }

    /**
     * 将给定的消息转发给所有客户端 加synchronized锁,使异步操作变成同步操作
     * 
     * @param message
     */
    public synchronized void sendMessage(String message) {
        for (PrintWriter pw : allOut) {
            pw.println(message);

        }

    }

    public static void main(String[] args) {

        Server server;
        server = new Server();
        server.start();
    }

    /**
     * 服务端中的一个线程,用于与某个客户交互 使用线程的目的是使得服务端可以处理多个客户端了。
     * 
     * @author Administrator
     *
     */
    // 创建线程
    class ClientHandler implements Runnable {
        // 当前线程处理的客户端的Socket
        private Socket socket;

        // 当前客户端的IP
        private String ip;

        // 当前用户的昵称
        private String nickname;

        /**
         * 根据给定的客户端的Socket,创建线程体
         * 
         * @param socket
         */
        public ClientHandler(Socket socket) {
            this.socket = socket;

            // 获取远端的的地址
            InetAddress address = socket.getInetAddress();

            // 获取本端的地址信息
            // socket.getLocalAddress();

            // 获取远端的计算机ip地址
            ip = address.getHostAddress();
            // 获取客户端的端口号
            int port = socket.getPort();

        }

        /**
         * 该线程会将Socket中的输入流获取 用来循环读取客户端发送过来的消息
         */
        public void run() {
            /**
             * 定义在try语句外的目的是,为了在 finally中也可以引用到
             */
            PrintWriter pw = null;
            try {
                /**
                 * 为了让服务端与客户端发送信息, 我们需要通过socket获取输出流
                 */
                OutputStream out = socket.getOutputStream();

                // 转换为字符流,用于指定编码集
                OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");

                // 创建缓冲字符输出流
                pw = new PrintWriter(osw, true);

                /**
                 * 将该客户端的输出流存入共享集合以便 使得该客户端也能接收服务端转发的 消息
                 */

                // 存在线程安全问题,不推荐
                // allOut.add(pw);

                addOut(pw);

                // 是输出在线人数
                System.out.println("当前在线人数为:" + allOut.size());

                // System.out.println("客户端连接成功");

                /**
                 * 通过刚刚连上的客户端的Socket获取输入流 读取客户端发送的· 数据
                 */
                InputStream in = socket.getInputStream();

                InputStreamReader isr = new InputStreamReader(in, "utf-8");
                /**
                 * 将字符流转换为缓冲字符输入流 这样就可以以行为单位读取字符串了
                 */
                BufferedReader br = new BufferedReader(isr);

                /**
                 * 当创建好当前客户端的输入流后 读取的第一个字符串,应当是昵称
                 * 
                 */
                nickname = br.readLine();

                // 通知所有客户端,当前用户上线了

                sendMessage("[" + nickname + "]上线了");

                // 读取客户端发过来的字符串
                /**
                 * windows与linux存在一定的差异: linux:当客户端与服务端断开连接后
                 * 我们通过输入流会读到null但这是合乎逻辑的,因为缓冲流的
                 * readLine()方法若返回null就表示无法通过该流再读取到 信息。参考之前服务文本文件的判断
                 * windows:当客户端与服务端断开连接后 readLine()方法会抛出异常
                 * 
                 */
                String message = null;

                while ((message = br.readLine()) != null) {
                    //pw.println(message);
                    /**
                     * 当读取到客户端发送过来的一条消息后, 将该消息装发给所有客户端
                     */
                    // 存在线程安全问题,遍历过程中不允许有删改
                    // for(PrintWriter o:allOut){
                    // o.println(message);
                    // }

                    sendMessage(nickname + "说:" + message);
                }

            } catch (Exception e) {
                // 在windows中的客户端,报错通常是因为客户端断开了连接
                // 通知其他用户,该用户下线了
                sendMessage("[" + nickname + "]下线了");
            } finally {
                /**
                 * 首先将该客户端的输出流从共享 集合中删除。
                 */
                // 存在现存安全问题
                // allOut.remove(pw);

                removeOut(pw);

                // 输出当前在线人数
                System.out.println("当前在线人数为:" + allOut.size());
                // 通知其他用户,该用户下线了
                sendMessage("[" + nickname + "]下线了");
                /**
                 * 无论Linux还是windows用户,当与服务器断开连接后 我们都应该在服务器断开与客户端断开连接
                 */
                try {
                    socket.close();
                } catch (IOException e) {
                    System.out.println("一个客户下线了。。。");
                }

            }

        }

    }
}

12、测试

  • 服务端运行
    这里写图片描述
  • 客户端运行
    提示请输入昵称,若不输入昵称,提示昵称为空,强制输入。
    这里写图片描述
    客户端显示在线人数
    这里写图片描述
    -消息通信
    这里写图片描述
    其他客户端进行转发
    这里写图片描述
    下线通知
    这里写图片描述
    这里写图片描述
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值