基于BIO实现简单的聊天室

[TOC]

前言

  • 环境:openjdk8
  • 参考书籍:java多线程编程实战指南(核心篇) 黄文海
  • 注意:这个主要是试用下BIO通讯,重点是使用BIO,不是实现聊天室,所以写的很糙,确实很丑......

分析

单线程版

特点:

  • 阻塞
  • 一对一

流程图如下
BIO.png

监听到一个client连接,只创建一个Socket线程,与之进行交互

同样,这里的交互也是阻塞的


阻塞读写-1586409046342.png

可以是

  • 先读后写
  • 先写后读

注意:客户端与服务端口不可同时读/同时写,以免造成死锁

这样的读写显然是无法满足需要的,聊天回合制可还行

可以另开一个线程完成读/写,达到异步的效果

(当然,这个理想情况下的图,读写并行)


BIO读写伪异步.png

依此,可以实现一个简单的,服务器与客户端一对一的交流

但是,这也是不满足需求的,哪有服务器和客户端聊天的,应为客户端对客户端.所以,

  • 每接受到一个客户端链接,就另开一个线程与之通信
  • 服务端收到客户端信息,将信息发往与之有关联的客户端线程
BIO聊天室.png

Server的读写就不需要分离了,收到信息后就发送给关联的client就行

这里只是简单实现,所以就不搞分组,都在一个组内好了.如何管理聊天呢?需要保证每个client都可以收到组内成员发的信息,我的想法是定一个线程共享的集合来存储创建的socket,每收到信息,就遍历socket集合,发送信息给每一个client.所以思路如下

分析总结

server层:

  • 循环监听连接
  • 每监听到一个连接,创建socket,将socket存储到线程共享且线程安全的socket集合中,另开一线程与client进行通信
  • 读写一个线程,先读后写,每收到一个信息,就遍历socket集合,给每一个client发送信息

client层:

  • 连接server
  • 连接到后,进行通信,通信循环,直到达到循环不成立的条件
  • 读写分离,各占一个线程

实现

server

初始化

设置监听队列长度,ip地址与端口.这个给个端口就行了,其它使用默认值好了

看了看源码,

public ServerSocket(int port) throws IOException {
    this(port, 50, null);
}

默认监听队列长是50,也就是监听50个连接请求,这个没关系,监听是监听,处理是处理.

跳一跳,看看默认的ip地址是啥

public synchronized InetAddress anyLocalAddress() {
    if (anyLocalAddress == null) {
        anyLocalAddress = new Inet4Address(); // {0x00,0x00,0x00,0x00}
        anyLocalAddress.holder().hostName = "0.0.0.0";
    }
    return anyLocalAddress;
}

扒到了,默认ip地址是0.0.0.0,也就是本机所有ip都可以被client访问.我一直以为默认的ip地址是127.0.0.1

好了,没啥说的了,看初始化代码如下:

private final static int PORT = 8080;
private static ServerSocket ss;
private static void init() {
    try {
        ss = new ServerSocket(PORT);
        Logger.getGlobal().info("server 初始化成功");
    } catch (IOException e) {
        Logger.getGlobal().severe("Server初始化失败" + e.getMessage());
    }
}
监听与创建socket

使用CopyOnWriteArrayList来存储socket来管理所有socket.为了保证线程的高效和不至于因为开了太多线程导致服务器崩掉,使用线程池.这几个参数分别是

  • 4:核心线程数,也就是保证活跃的数量至少为4个线程(不管有没有任务)
  • 8:线程池容量,也就是池中允许活跃的最大线程数
  • 10:线程池中除了核心线程之外的空余活跃线程,允许等待新任务的时间
  • TimeUnit.SECONDS:时间单位
  • new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8):阻塞的线程任务队列,容量为核心数*8
  • new MyThreadFactory():这个是自实现的线程工厂,主要用日志记录下线程的创建和抛出的异常,之前照着黄文海的java多线程编程实战指南(核心篇)写的.

其实这里不用线程池也行,把那个监听队列改小.也不怕线程开太多.当然线程池还是性能好些,就是写起来麻烦

通信退出后,得把那个socket从集合中移除

public static void start() {
    init();
    CopyOnWriteArrayList sockets = new CopyOnWriteArrayList<>();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 10, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8), new MyThreadFactory());

    while (true) {
        executor.execute(() -> {
            try (Socket socket = ss.accept()) {
                sockets.add(socket);
                communicate(socket,sockets);
                sockets.remove(socket);
                Logger.getGlobal().info("从sockets中移除一个socket");
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}
通信

这里没什么说的,注意的是,不要在写完发送的信息就把流关闭了,会把socket也给关闭的.

private static void send(byte[] msg, CopyOnWriteArrayList sockets) throws IOException {
    int num = 0;
    for (Socket socket : sockets) {
        if(socket.isClosed()){
            continue;
        }
        //这里的OutputStream不能关,关闭会释放所有与之相关的资源,也许这个资源包括socket,所以报错了...
        OutputStream out = socket.getOutputStream();
        out.write(msg);
        Logger.getGlobal().info(num++ + "socket接收到了");
    }
}

private static void communicate(Socket socket, CopyOnWriteArrayList sockets) {
    try (InputStream in = socket.getInputStream()) {
        while (socket.isConnected()) {
            byte[] msg = new byte[32];
            int len = in.read(msg);
            String s = new String(msg,0,len);
            System.out.println("接收到信息" +s);
            if(s.equals("exit") == true){
                System.out.println("接收到退出信息,准备退出");
                OutputStream out = socket.getOutputStream();
                out.write(s.getBytes());
                out.flush();
                System.out.println("退出");
                return;
            }
            send(Arrays.copyOf(msg, len), sockets);
        }
    } catch (IOException e) {
        Logger.getGlobal().severe("communicate失败" + e.getMessage());
    }
}

Client

client把读写分离就好了,就俩线程也用不着线程池,输入exit退出,主线程等读线程退出后再退出(避免抛出异常,毕竟使用的是try with resource,会自动关闭socket,然而此时读线程可能正在读中,未退出,就会抛出异常)

public static void main(String[] args){
    try(Socket socket = new Socket(HOST,PORT)){
        Logger.getGlobal().info("已经连接到服务器");
        Thread thread = new Thread(() -> {
            try(InputStream in = socket.getInputStream()){
                String words = "true";
                do{
                    byte[] b = new byte[32];
                    int len = in.read(b);
                    if(len != -1){
                        words = new String(b,0,len);
                        System.out.println(words);
                    }
                }while(words.equals("exit") != true);
            }catch (IOException e){
                e.printStackTrace();
            }
        });
        thread.start();

        OutputStream out = socket.getOutputStream();
        Scanner sc = new Scanner(System.in);
        System.out.print("请设置昵称:");
        String pre = sc.nextLine() + " : ";
        while(socket.isConnected()){
           String s = sc.nextLine();
            if(s.equals("exit") == true){
                out.write(s.getBytes());
                break;
            }
            out.write((pre+s).getBytes());
            out.flush();
        }
        thread.join();
        System.out.println("等待中");

    }catch(UnknownHostException e){
        e.printStackTrace();
    }catch(IOException e){
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

完整代码

Server

package cn.informal2019.stu.openjdk8.net.bio.multi;

import cn.informal2019.stu.openjdk8.thread.manage.MyThreadFactory;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Vector;
import java.util.concurrent.*;
import java.util.logging.Logger;

/**
 * @Author zjm
 * @Date 2020/3/28
 * @Description TODO
 * @Version 1.0
 */
public class ServerDemo {
    private final static int PORT = 8080;
    private static ServerSocket ss;
    private static CopyOnWriteArrayList socketTasks = new CopyOnWriteArrayList<>();


    private static void init() {
        try {
            ss = new ServerSocket(PORT);
            Logger.getGlobal().info("server 初始化成功");
        } catch (IOException e) {
            Logger.getGlobal().severe("Server初始化失败" + e.getMessage());
        }
    }


    public static void start() {
        init();
        CopyOnWriteArrayList sockets = new CopyOnWriteArrayList<>();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 8), new MyThreadFactory());

        while (true) {
            executor.execute(() -> {
                try (Socket socket = ss.accept()) {
                    sockets.add(socket);
                    communicate(socket,sockets);
                    sockets.remove(socket);
                    Logger.getGlobal().info("从sockets中移除一个socket");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    private static void send(byte[] msg, CopyOnWriteArrayList sockets) throws IOException {
        int num = 0;
        for (Socket socket : sockets) {
            if(socket.isClosed()){
                continue;
            }
            //这里的OutputStream不能关,关闭会释放所有与之相关的资源,也许这个资源包括socket,所以报错了...
            OutputStream out = socket.getOutputStream();
            out.write(msg);
            Logger.getGlobal().info(num++ + "socket接收到了");
        }
    }

    private static void communicate(Socket socket, CopyOnWriteArrayList sockets) {
        try (InputStream in = socket.getInputStream()) {
            while (socket.isConnected()) {
                byte[] msg = new byte[32];
                int len = in.read(msg);
                String s = new String(msg,0,len);
                System.out.println("接收到信息" +s);
                if(s.equals("exit") == true){
                    System.out.println("接收到退出信息,准备退出");
                    OutputStream out = socket.getOutputStream();
                    out.write(s.getBytes());
                    out.flush();
                    System.out.println("退出");
                    return;
                }
                send(Arrays.copyOf(msg, len), sockets);
            }
        } catch (IOException e) {
            Logger.getGlobal().severe("communicate失败" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        start();
    }
}

Client

package cn.informal2019.stu.openjdk8.net.bio.single;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.logging.Logger;

/**
 * @Author zjm
 * @Date 2020/2/27
 * @Description TODO
 * @Version 1.0
 */
public class ClientDemo {
    public final static int PORT = 8080;
    public final static String HOST = "localhost";

    public static void main(String[] args){
        try(Socket socket = new Socket(HOST,PORT)){
            Logger.getGlobal().info("已经连接到服务器");
            Thread thread = new Thread(() -> {
                try(InputStream in = socket.getInputStream()){
                    String words = "true";
                    do{
                        byte[] b = new byte[32];
                        int len = in.read(b);
                        if(len != -1){
                            words = new String(b,0,len);
                            System.out.println(words);
                        }
                    }while(words.equals("exit") != true);
                }catch (IOException e){
                    e.printStackTrace();
                }
            });
            thread.start();

            OutputStream out = socket.getOutputStream();
            Scanner sc = new Scanner(System.in);
            System.out.print("请设置昵称:");
            String pre = sc.nextLine() + " : ";
            while(socket.isConnected()){
               String s = sc.nextLine();
                if(s.equals("exit") == true){
                    out.write(s.getBytes());
                    break;
                }
                out.write((pre+s).getBytes());
                out.flush();
            }
            thread.join();
            System.out.println("等待中");

        }catch(UnknownHostException e){
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

MyThreadFactory

package cn.informal2019.stu.openjdk8.thread.manage;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class MyThreadFactory implements ThreadFactory {
    final  static Logger LOGGER = Logger.getAnonymousLogger();
    private final Thread.UncaughtExceptionHandler ueh;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public MyThreadFactory(Thread.UncaughtExceptionHandler ueh,String name) {
        this.ueh = ueh;
        this.namePrefix = name;
    }
    public MyThreadFactory(){
        this(new LoggingUncaughtExceptionHandler(),"thread");
    }
    
    protected Thread doMakeThread(final Runnable r){
        return new Thread(r){
            @Override
            public String toString() {
                ThreadGroup group  =getThreadGroup();
                String groupName = null == group ? "" : group.getName();
                String threadInfo = getClass().getSimpleName()+"["+getName()+","+getId()+"," + groupName + "]@" + hashCode();
                return threadInfo;
            }
        };
    }
    
    @Override
    public Thread newThread(Runnable r) {
        Thread t = doMakeThread(r);
        t.setUncaughtExceptionHandler(ueh);
        t.setName(namePrefix + "-" + threadNumber.getAndIncrement());
        if(t.isDaemon()){
            t.setDaemon(false);
        }
        if(t.getPriority() != Thread.NORM_PRIORITY){
            t.setPriority(Thread.NORM_PRIORITY);
        }
        if(LOGGER.isLoggable(Level.FINE)){
            LOGGER.fine("new thread created" + t);
        }
        return t;
    }
    static class LoggingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            LOGGER.log(Level.SEVERE,t + "terminated:",e);
        }
    }
}

先更后改

你可能感兴趣的:(基于BIO实现简单的聊天室)