IO-BIO实现聊天室

IO-BIO实现聊天室(Socket全双工)

概述

本次实现的是一个聊天室,使用的是TCP/IP协议,使用自定义传输协议。多个客户端连接上服务端,客户端发送消息到服务端,服务端将客户端加入到客户端队列,通过loop队列,向所有的客户端转发消息。此处转发为同步转发,意味着先加入的客户端将优先收到消息。当客户端离线的时候,服务端将从队列中移除,并且终止针对该客户端的轮询。

流程图

在这里插入图片描述

服务端

在这里插入图片描述

  1. 获取客户端连接
  2. 将客户端Socket放入线程安全的非阻塞队列
  3. 一个客户端提供一个线程去相互通信

具体实现

package com.yzz.studyio.bio;

import com.yzz.studyio.bio.protocol.BioChartRoomProtocol;
import com.yzz.studyio.bio.protocol.Message;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * describe: 聊天室服务端
 * E-mail:yzzstyle@163.com  date:2019/1/12
 * 你可以简单的这样去启动一个服务端
 * @Since 0.0.1
 *
 *   public class Server {
 *       public static void main(String[] args) {
 *           try {
 *               BioServer bioServer = BioServer.newServerByPort(9090);
 *               bioServer.listen();
 *           } catch (Exception e) {
 *               e.printStackTrace();
 *           }
 *       }
 *   }
 */
public class BioServer {

    //默认端口
    public static final int DEFAULT_PORT = 8080;

    //默认地址
    public static final String DEFAULT_LOCATION = "127.0.0.1";

    //客户端连接(socket连接)的队列最大长度
    public static final int DEFAULT_BACKLOG = 50;

    //日志
    private final Logger logger = Logger.getLogger(BioServer.class.getName());

    //线程池,用于执行接收客户端消息并转发消息至所有在线的客户端,这里最大可以接收10 + Integer.MAX_VALUE 个客户端
    private final Executor pool = Executors.newFixedThreadPool(10);

    //服务端口
    private final int port;

    //连接队列的最大长度
    private final int backlog;

    //地址
    private final String location;

    //Socket监听服务对象
    private ServerSocket serverSocket;

    //Socket服务监听地址包装类
    private InetAddress inetAddress;

    //这是一个线程安全的非阻塞的队列
    private final ConcurrentLinkedQueue<Socket> clientSockets = new ConcurrentLinkedQueue<>();

    /**
     * 创建一个默认的聊天室服务端
     *
     * @return
     * @throws IOException
     */
    public static BioServer createServerByDefault() throws IOException {
        return new BioServer(DEFAULT_PORT, DEFAULT_BACKLOG, DEFAULT_LOCATION);
    }

    /**
     * 创建一个自定义端口的服务端
     *
     * @param port
     * @return
     * @throws IOException
     */
    public static BioServer newServerByPort(int port) throws IOException {
        return new BioServer(port, DEFAULT_BACKLOG, DEFAULT_LOCATION);
    }

    /**
     * 创建一个自定义端口和连接队列深度的服务
     *
     * @param port    端口
     * @param logback 连接队列最大num
     * @return
     * @throws IOException
     */
    public static BioServer newServerWithDefaultLocation(int port, int logback) throws IOException {
        return new BioServer(port, logback, DEFAULT_LOCATION);
    }

    /**
     * 自定义端口、连接大小、地址的服务端对象
     *
     * @param port
     * @param logback
     * @param location
     * @return
     * @throws IOException
     */
    public static BioServer newServerDynamic(int port, int logback, String location) throws IOException {
        return new BioServer(port, logback, location);
    }

    /**
     * @param port     监听的端口号
     * @param backlog  客户端连接的队列最大长度
     * @param location 地址
     * @throws IOException
     */
    private BioServer(int port, int backlog, String location) throws IOException {
        this.port = port;
        this.location = location;
        this.backlog = backlog;
        inetAddress = InetAddress.getByName(location);
        //创建一个端口监听对象,监听客户端连接,此刻服务端已经就绪
        serverSocket = new ServerSocket(port, backlog, inetAddress);
        logger.log(Level.INFO, "BIO server has started in " + location + " listening port " + port);
    }

    /**
     * accept 监听客户端的连接
     * 1. accept()获取客户端Socket
     * 2. provideService()提供处理客户端消息的服务
     *
     * @throws IOException
     */
    public void listen() throws IOException {
        while (true) {
            Socket client = serverSocket.accept();
            clientSockets.add(client);
            logger.log(Level.INFO, client.getInetAddress().getHostAddress() + "加入了聊天室");
            logger.log(Level.INFO, "当前在线人数:" + clientSockets.size());
            pool.execute(() -> provideService(client));
        }
    }

    /**
     * 提供服务 轮询去客户端处理消息
     *
     * @param client
     */
    public void provideService(Socket client) {
        while (true) {
            //自定义协议 消息对象
            Message message = null;
            try {
                //1. 获取客户端发送至服务端的消息
                message = readRequest(client);
                //1.如果客户端队列的size为0,就说明所有客户端都已经下线,直接结束轮询
                if (clientSockets.size() == 0) {
                    break;
                } else {
                    //转发消息至所有客户端
                    dispatch(message);
                }
            } catch (IOException e) {
                //这里的异常表示客户端已经下线,此时需要去清除客户端连接资源
                logger.log(Level.WARNING, e.getMessage());
                remove(client);
                break;
            }
        }
    }

    /**
     * 读取客户端发送来的消息
     *
     * @param client 客户端Socket
     * @return
     * @throws IOException
     */
    public Message readRequest(Socket client) throws IOException {
        InputStream in = client.getInputStream();
        //自定义协议负责去解析消息
        return BioChartRoomProtocol.parse(in);
    }

    /**
     * 转发请求至所有客户端
     *
     * @param message
     */
    public void dispatch(Message message) {
        Iterator<Socket> clients = clientSockets.iterator();
        Socket client = null;
        //遍历发送至客户端,该方法是同步方法,so,客户端收到的消息是先后顺序和其进入聊天室的顺序是一致的
        while (clients.hasNext()) {
            try {
                client = clients.next();
                logger.log(Level.INFO, "转发消息至" + message.getHeader().toString());
                //协议去写入消息至客户端
                BioChartRoomProtocol.write(client.getOutputStream(), message);
            } catch (IOException e) {
                //这里的异常表示客户端已经下线,此时需要去清除客户端连接资源
                e.printStackTrace();
                logger.log(Level.INFO, e.getMessage());
                remove(client);
            }
        }
    }

    /**
     * 清除资源 从队列中去移除
     *
     * @param client
     */
    public void remove(Socket client) {
        try {
            boolean result = clientSockets.remove(client);
            if (result && client != null && !client.isClosed()) {
                //关闭连接
                client.close();
                logger.log(Level.WARNING, "移除client" + client.getInetAddress().getHostAddress() + ":" + client.getPort());
            }
        } catch (IOException e) {
            e.printStackTrace();
            logger.log(Level.WARNING, e.getMessage());
        }
    }

}

客户端

  1. 连接上服务端
  2. 开辟线程去轮询接收服务端的消息
  3. 用户发送消息至客户端

具体实现

package com.yzz.studyio.bio;

import com.yzz.studyio.bio.protocol.BioChartRoomProtocol;
import com.yzz.studyio.bio.protocol.Header;
import com.yzz.studyio.bio.protocol.Message;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Logger;

/**
 * describe:
 * E-mail:yzzstyle@163.com  date:2019/1/12
 * 你可以简单这样测试
 *
 * @Since 0.0.1
 *
 *   public class Client {
 *       public static void main(String[] args) {
 *           try {
 *               Scanner sc = new Scanner(System.in);
 *               BioClient bioClient = new BioClient(9090, "127.0.0.1", args[0]);
 *               bioClient.listen12n(message -> {
 *                   System.out.println(message.toString());
 *               });
 *               while (true) {
 *                   String content = sc.next();
 *                   bioClient.sendMsg(content);
 *               }
 *           } catch (Exception e) {
 *               e.printStackTrace();
 *           }
 *       }
 *   }
*/
public class BioClient {

    //服务端端口
    private final int port;

    //服务端地址
    private final String location;

    //连接服务端的Socket对象
    private Socket socket;

    //创建的一个单线程
    private Executor pool = Executors.newSingleThreadExecutor();

    //用户昵称,必填
    private final String nickName;

    //消息头
    private final Header header;

    //日志
    private Logger logger = Logger.getLogger(BioClient.class.getName());

    /**
     * 该构造初始化了 port、location、nicName 并创建了与服务端通信的Socket连接
     * @param port
     * @param location
     * @param nicName
     * @throws IOException
     */
    public BioClient(int port, String location, String nicName) throws IOException {
        this.port = port;
        this.location = location;
        this.nickName = nicName;
        InetAddress inetAddress = InetAddress.getByName(location);
        socket = new Socket(inetAddress, port);
        header = new Header(inetAddress.getHostAddress(), socket.getLocalPort(), nicName);
    }

    /**
     * 向服务端发送消息
     * @param msg
     * @throws IOException
     */
    public void sendMsg(String msg) throws IOException {
        OutputStream outputStream = socket.getOutputStream();
        //自定义协议去发送消息
        BioChartRoomProtocol.write(outputStream, header, msg);
    }

    /**
     * 从服务端接收消息
     * @return
     * @throws IOException
     */
    public Message getResponse() throws IOException {
        InputStream inputStream = socket.getInputStream();
        //自定义协议去接收消息,返回消息对象
        Message message = BioChartRoomProtocol.parse(inputStream);
        return message;
    }

    /**
     * 清除资源
     */
    public void clear() {
        if (socket != null && !socket.isClosed()) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 监听
     * 开辟一个工作线程去接收服务端转发的消息
     * @param callback
     */
    public void listen12n(MSGCallback callback) {
        pool.execute(() -> {
            while (true) {
                try {
                    Message message = getResponse();
                    //该回调面向用户,提供服务端发送的消息
                    callback.message(message);
                } catch (IOException e) {
                    e.printStackTrace();
                    clear();
                    return;
                }
            }
        });

    }

    /**
     * 回调接口
     */
    public interface MSGCallback {
        void message(Message message);
    }
}

自定义协议

组成

  • Header 封装消息头
  • Message 封装消息 包括消息头和消息体
  • BioChartRoomProtocol 负载CS读写

Header

package com.yzz.studyio.bio.protocol;

import java.io.Serializable;

/**
 * describe: 自定义协议中的消息头
 * E-mail:yzzstyle@163.com  date:2019/1/12
 *
 * @Since 0.0.1
 */
public class Header implements Serializable {

    //消息来源地
    private String host;
    //消息来源地的端口
    private int port;
    //发送消息用户的昵称
    private String nickName;

    public Header(String host, int port, String nickName) {
        this.host = host;
        this.port = port;
        this.nickName = nickName;
    }

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }

    public String getNickName() {
        return nickName;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public void setPort(int port) {
        this.port = port;
    }

    @Override
    public String toString() {
        return " host: " + host + " port: " + port + " nickName: " + nickName;
    }
}

Message

package com.yzz.studyio.bio.protocol;

import java.io.Serializable;

/**
 * describe:自定义协议的消息对象,包括消息头和消息体
 * E-mail:yzzstyle@163.com  date:2019/1/12
 *
 * @Since 0.0.1
 */
public class Message implements Serializable {
    private Header header;
    private String message;

    public Message(Header header, String message) {
        this.header = header;
        this.message = message;
    }

    public Header getHeader() {
        return header;
    }

    public void setHeader(Header header) {
        this.header = header;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(header.getNickName())
                .append("(")
                .append(header.getHost())
                .append(":")
                .append(header.getPort())
                .append(")")
                .append("\r\n")
                .append("\t")
                .append(message);
        return sb.toString();
    }
}

BioChartRoomProtocol

package com.yzz.studyio.bio.protocol;

import java.io.*;

/**
 * describe: 自定义协议的操作类包括 读和写
 * E-mail:yzzstyle@163.com  date:2019/1/12
 * 使用 jdk自带的序列化机制,将Message消息对象作为CS交互的介质
 * @Since 0.0.1
 */
public class BioChartRoomProtocol {

    /**
     * 些消息 适用于客户单发消息
     * @param outputStream
     * @param header
     * @param content
     * @throws IOException
     */
    public static void write(OutputStream outputStream, Header header, String content) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        Message message = new Message(header, content);
        objectOutputStream.writeObject(message);
    }

    /***
     * 写消息 使用服务端转发消息
     * @param outputStream
     * @param message
     * @throws IOException
     */
    public static void write(OutputStream outputStream, Message message) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(message);
    }

    /**
     * 读操作,将消息转化成消息对象
     * @param inputStream
     * @return
     * @throws IOException
     */
    public static Message parse(InputStream inputStream) throws IOException {
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        Message message = null;
        try {
            message = (Message) objectInputStream.readObject();
        } catch (ClassNotFoundException e) {
            throw new Error(e.getMessage());
        }
        return message;
    }

}

效果图

  1. 客户端连接
    在这里插入图片描述
  2. 客户端发送消息,服务端转发
    在这里插入图片描述
  3. 客户端1发送消息
    在这里插入图片描述
  4. 客户端2发送消息
    在这里插入图片描述

命令行版本

server端参数

1. -p=xxx 设置端口号

cliet端参数

 1. -a=xxx 设置address
 2. -p=xxx 设置端口号
 3. -n=xxx 设置nickName

启动

//启动服务
java  -jar bio-chartroom-server.jar  -p=6666 
//启动客户端
java  -jar bio-chartroom-client.jar  -p=6666 -n=嘿嘿 -a=127.0.0.1 

客户端quick start

package com.yzz.chartroom;

import java.io.IOException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * describe:
 * E-mail:yzzstyle@163.com  date:2019/1/12
 *
 * @Since 0.0.1
 */
public class Client {
    private final static String MSG = "please input msg: \n 1. -a=xxx 设置address \n 2. -p=xxx 设置端口号 \n 3. -n=xxx 设置nickName ";
    private static Logger logger = Logger.getLogger("bio-chartroom-client");
    public static final String NICK_NAME = "尹忠政";


    public static void main(String[] args) {

        try {
            Scanner sc = new Scanner(System.in);
            BioClient bioClient = init(args);
            bioClient.listen12n(message -> System.out.println(message.toString()));
            while (true) {
                String content = sc.next();
                bioClient.sendMsg(content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            logger.log(Level.WARNING, e.getMessage());
        }
    }

    public static BioClient init(String[] args) throws IOException {
        logger.log(Level.WARNING, MSG);
        Argument argument = new Argument();
        for (String a : args) {
            initArguement(a, argument);
        }
        argument.check();
        return new BioClient(argument.getPort(), argument.getAddress(), argument.getNickName());
    }

    public static void initArguement(String arg, Argument argument) {
        if (arg.startsWith("-a")) {
            argument.setAddress(arg.replaceAll("-a=", ""));
            return;
        }
        if (arg.startsWith("-p")) {
            try {
                argument.setPort(Integer.valueOf(arg.replaceAll("-p=", "")));
            } catch (Exception e) {
                logger.log(Level.WARNING, e.getMessage());
                logger.log(Level.WARNING, MSG);
            }
            return;
        }
        if (arg.startsWith("-n")) {
            argument.setNickName(arg.replaceAll("-n=", ""));
            return;
        }
    }

    static class Argument {
        private String address;
        private int port;
        private String nickName;

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public String getNickName() {
            return nickName;
        }

        public void setNickName(String nickName) {
            this.nickName = nickName;
        }

        public void check() {
            if (null == address || address.equals("")) {
                address = BioServer.DEFAULT_LOCATION;
            }

            if (port == 0) {
                port = BioServer.DEFAULT_PORT;
            }

            if (null == nickName || nickName.equals("")) {
                nickName = NICK_NAME;
            }
        }
    }
}

服务端quick start

package com.yzz.chartroom;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * describe:
 * E-mail:yzzstyle@163.com  date:2019/1/12
 *
 * @Since 0.0.1
 */
public class Server {

    private final static String MSG = "please input msg: \n 1. -p=xxx 设置端口号 ";
    private static Logger logger = Logger.getLogger("bio-chartroom-client");

    public static void main(String[] args) {
        try {
            logger.log(Level.WARNING, MSG);
            BioServer bioServer = BioServer.newServerByPort(args.length == 0 ? BioServer.DEFAULT_PORT : Integer.valueOf(args[0].replaceAll("-p=","")));
            bioServer.listen();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

  1. BIO是阻塞的,接收客户端连接效率不高
  2. 这里我才用了线程池去处理没客户端的交互,性能良好.
  3. 这里我才用了线程安全的非阻塞队列,线程安全,新能很好
  4. 这里要特别注意客户端断开的情况和服务端挂掉的情况,其实关闭资源和中断轮询,否则轮询造成cpu被被沾满,严重情况下会导致机器crash。
  5. 这里采用的自定要传输协议,通过jdk序列化机制,通过对象来包装消息。

[ 项目地址](https://github.com/yinzhongzheng/bio-chatroom 学习分享

	git clone https://github.com/yinzhongzheng/bio-chatroom.git
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值