IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 上班聊天,摸鱼神器,手写一款即时通讯工具(附源码!!!) -> 正文阅读

[Java知识库]上班聊天,摸鱼神器,手写一款即时通讯工具(附源码!!!)

认真工作不叫做赚钱,那叫做用劳动换取报酬,上班摸鱼才是真的赚钱。

在这里插入图片描述

即时通讯工具

如果上班有空闲时间,最喜欢做的事情自然是和熟悉的朋友一起聊聊天,互相吐槽工作中遇到的人和事,缓解工作的压力。

如果直接在桌面上打开 QQ 或者微信,那目标无疑是巨大的,QQ 和微信的桌面客户端明晃晃地占据整个电脑桌面,只要有同事或者领导从你身边经过,或是在你后面看一眼,就立刻能够知道你在上班摸鱼,那场面不亚于公开处刑…… (@_@)

领导:看来工作还是不饱和啊 ┑( ̄Д  ̄)┍

针对这种情况,技术人自然不甘落后,总是可以想出各种方法躲避同事和领导发现你在上班摸鱼 ≡ω≡

思量再三,最终还是放弃了 IDEA 的各种插件,转而决定还是自己手写一款简易的即时通信工具。

既然要自己动手,那自然也要先对这款即时通讯工具做个简单的规划。

  • 这款即时通讯工具分为客户端和服务端的,每个用户可以使用客户端进行即时通讯。
  • 通讯工具尽可能简单,只依赖于 JDK,即完全使用 Java 网络编程功能实现,不依赖其他的第三方库。
  • 通讯工具不需要桌面,使用 Java 自带的 Scanner 控制台输入即可。

这样一款基于 Java 网络编程的即时通讯工具,只要在 IDEA 运行客户端代码,即可在控制台与其他朋友快乐地聊天。只要不是同事或者领导贴着你的电脑屏幕观看,他绝对想不到你是在使用 IDEA 上班摸鱼聊天。

在这里插入图片描述

客户端

客户端是给使用这款即时通讯工具的用户使用的,从安全和用户体验的角度上来说,客户端应该尽可能精简,只负责发送和接受数据即可。

因为这是一款即时通讯工具,客户端需要做的有两件事:

  • 监听客户端的输入和发送。
  • 监听服务端发送过来的消息。

因为我们使用 JDK 自带的 Scanner 类来进行客户端的输入,而这个输入是一个阻塞的操作,所以我们需要创建一条额外的线程来进行服务端的监听工作。

客户端需要两条用户线程:

  • main 线程用来监听客户端的输入和发送。
  • 另外创建一条线程用来监听服务端的消息发送。

思路已经设计好了,可以使用代码来实现了:

/**
 * 聊天室客户端
 *
 * @author herenpeng
 * @since 2021-07-09 12:00:00
 */
public class ChatClient {

    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", 12345)) {
            // 读取服务端发的消息
            new Thread(() -> readMsg(socket)).start();
            OutputStream os = socket.getOutputStream();
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入您的聊天室昵称:");
            while (true) {
                String chat = scanner.next();
                System.out.println("---------------------------");
                os.write(chat.getBytes());
            }
        } catch (Exception e) {
            System.out.println("【系统消息】聊天室炸了,BUG之神降临了");
            e.printStackTrace();
            System.exit(0);
        }
    }

    private static void readMsg(Socket socket) {
        try {
            while (true) {
                InputStream is = socket.getInputStream();
                byte[] bytes = new byte[1024];
                int len = is.read(bytes);
                System.out.println(new String(bytes, 0, len));
            }
        } catch (Exception e) {
            System.out.println("【系统消息】你已退出聊天室,开始认真工作吧");
            System.exit(0);
        }
    }

}

服务端

服务端的设计比客户端要困难很多,为了便于开发和理解,我直接使用了 Java 阻塞式的网络 IO 来进行实现,即每一个客户端连接都创建一个线程来进行处理。

这种阻塞式的网络 IO 的好处在于便于理解和开发,而缺点也非常明显,因为这是一个通讯工具,即每一个链接都是长链接。

即每个客户端用户链接服务端后,都会在服务端专门有一个线程处理这个客户端相关的网络 IO 操作。如果用户量少的情况下还比较好,但是用户一旦多了起来,服务端将会创建 N 多个线程,而且在客户端不主动断开的情况下,服务器这些线程会一直占用服务器资源,服务器将会消费非常大的资源,而且很容易崩溃。

基于这种情况,我后面也实现了一个 Java NIO 版本的客户端和服务端,在文章末尾也会一起附上源码。

在这里插入图片描述

我将服务端的操作分为两个步骤:

1、链接

服务端阻塞地等待客户端的链接请求。

/**
 * 聊天室服务端
 *
 * @author herenpeng
 * @since 2021-07-09 12:00:00
 */
public class ChatServer {
    
	/**
     * 启动类
     *
     * @param args 启动参数
     * @throws IOException 抛出IO异常
     */
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(12345);
        new Thread(() -> start(server)).start();
        // 加载配置
        CHAT_CFG_RELOAD_PASSWORD = UUID.randomUUID().toString();
        logInfo("【系统消息】聊天室配置加载密钥:" + CHAT_CFG_RELOAD_PASSWORD);
        reloadChatCfg(args.length == 1 ? args[0] : null, null);
        logInfo("【系统消息】聊天室启动成功了!");
    }

    /**
     * 服务开始方法
     *
     * @param server 服务对象
     */
    private static void start(ServerSocket server) {
        try {
            while (true) {
                // 链接操作
                ChatSocket chatSocket = connection(server);
                // 登录操作
                login(chatSocket);
            }
        } catch (Exception e) {
            logInfo("【系统消息】聊天室发生了异常……");
            e.printStackTrace();
        } finally {
            logInfo("【系统消息】正在关闭聊天室资源……");
            close(server);
        }
    }

    /**
     * 链接客户端
     *
     * @param server 服务对象
     * @return ChatSocket 对象
     * @throws IOException 抛出异常
     */
    private static ChatSocket connection(ServerSocket server) throws IOException {
        Socket socket = server.accept();
        ChatSocket chatSocket = new ChatSocket(socket);
        userDB.add(chatSocket);
        sendMsgToUser(socket, "============================\n" +
                      "1、本聊天室仅为娱乐,请勿在该聊天室内谈论敏感内容,比如涉政,涉黄,账号密码等等!\n" +
                      "2、聊天室内容明文传输,聊天信息泄露本聊天室概不负责!\n" +
                      "3、本聊天室内容后台不做任何存储,聊天信息如果需要请自行保留!\n" +
                      "4、最终解释权归本聊天室所有!\n" +
                      "============================");
        return chatSocket;
    }
    
    /**
     * 保存所有用户socket的集合
     */
    private static final List<ChatSocket> userDB = new LinkedList<>();
    
}

2、登录

服务端获取到客户端请求后,将 Socket 包装为我们自定义的 ChatSocket,便于我们进行登录操作。

/**
 * 用户登录方法
 *
 * @param chatSocket ChatSocket 对象
 */
private static void login(ChatSocket chatSocket) {
    // 给每个用户一个线程处理
    new Thread(() -> {
        Socket socket = chatSocket.getSocket();
        String username = null;
        try {
            InputStream is = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len = readMsg(is, bytes);
            if (len == -1) {
                logout(chatSocket);
                return;
            }
            username = new String(bytes, 0, len);
            chatSocket.setUsername(username);
            // 刷新配置
            if (CHAT_CFG_RELOAD_PASSWORD.equals(username)) {
                reloadChatCfg(is, bytes, socket);
                return;
            }
            loginTip(username, socket);
            // 机器人欢迎
            robotWelcome(username);
            while (true) {
                len = readMsg(is, bytes);
                if (len == -1) {
                    logout(chatSocket);
                    return;
                }
                String msg = new String(bytes, 0, len);
                sendMsgToOtherUser(username, socket, msg);
                // 机器人回复消息
                randomRobotReply(msg);
            }
        } catch (IOException e) {
            try {
                logout(chatSocket);
            } catch (Exception ex) {
                remove(socket);
                ex.printStackTrace();
            }
            e.printStackTrace();
        }
    }).start();
}

3、其他方法

3.1、读取客户端的消息


/**
 * 读取消息的方法
 *
 * @param is    输入流
 * @param bytes 字节数组
 * @return 读取的长度
 */
private static int readMsg(InputStream is, byte[] bytes) {
    int len;
    try {
        len = is.read(bytes);
    } catch (Exception e) {
        return -1;
    }
    return len;
}

3.2、给客户端发送消息

/**
 * 给所有的用户发送系统消息
 *
 * @param msg 系统消息
 * @throws IOException 抛出异常
 */
private static void sendSysMsg(String msg) throws IOException {
    for (ChatSocket chatSocket : userDB) {
        String sysMsg = getCurrentTime() + "\n" + msg + "\n" + chatSeparate;
        Socket socket = chatSocket.getSocket();
        sendMsgToUser(socket, sysMsg);
    }
}

/**
 * 发送消息给其他用户
 *
 * @param username 消息发送用户名称
 * @param self     消息发送的用户socket
 * @param msg      消息
 * @throws IOException 抛出异常
 */
private static void sendMsgToOtherUser(String username, Socket self, String msg) throws IOException {
    for (ChatSocket chatSocket : userDB) {
        Socket socket = chatSocket.getSocket();
        if (socket.equals(self)) {
            continue;
        }
        String sendMsg = "(" + username + ") " + getCurrentTime() + "\n" + msg + "\n" + chatSeparate;
        sendMsgToUser(socket, sendMsg);
    }
}

/**
 * 给指定的用户发送消息,文本消息
 *
 * @param socket  消息发送的用户socket
 * @param sendMsg 消息
 * @throws IOException 抛出异常
 */
private static void sendMsgToUser(Socket socket, String sendMsg) throws IOException {
    OutputStream os = socket.getOutputStream();
    os.write(sendMsg.getBytes());
}

3.3、日志记录

虽然服务端不需要记录用户的聊天信息,但是还是需要记录一些服务器的日志信息。

/**
 * 打印日志
 *
 * @param message 日志信息
 */
private static void logInfo(String message) {
    System.out.println(getCurrentDateTime() + " " + message);
}

3.4、工具集合

/**
 * 获取当前在线的所有玩家名称
 *
 * @return 当前在线的所有玩家名称
 */
private static List<String> getLoginUsernames() {
    return userDB.stream().map(ChatSocket::getUsername).filter(Objects::nonNull).collect(Collectors.toList());
}


/**
 * 判断时间是否是 11:00 - 04:59 晚上
 *
 * @return 是返回true,否则返回false
 */
private static boolean isNight() {
    Calendar calendar = Calendar.getInstance();
    int hour = calendar.get(Calendar.HOUR_OF_DAY);
    return hour >= 23 || hour <= 4;
}

/**
 * 时间格式化对象
 */
private static final SimpleDateFormat timeSdf = new SimpleDateFormat("HH:mm:ss");
private static final SimpleDateFormat DateTimeSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

/**
 * 获取当前的时间的格式化字符串
 *
 * @return 当前的时间的格式化字符串
 */
private static synchronized String getCurrentTime() {
    return timeSdf.format(new Date());
}

/**
 * 获取当前的日期时间的格式化字符串
 *
 * @return 当前的日期时间的格式化字符串
 */
private static synchronized String getCurrentDateTime() {
    return DateTimeSdf.format(new Date());
}

/**
 * 判断一个字符串是否为空
 *
 * @param string 字符串
 * @return 为空返回true,否则返回false
 */
private static boolean isEmpty(String string) {
    return string == null || string.length() == 0;
}

/**
 * 判断一个字符串是否不为空
 *
 * @param string 字符串
 * @return 不为空返回true,否则返回false
 */
private static boolean isNotEmpty(String string) {
    return !isEmpty(string);
}

3.5、ChatSocket

/**
 * 封装的 ChatSocket
 */
private static class ChatSocket {

    private final Socket socket;

    private String username;

    public ChatSocket(Socket socket) {
        this.socket = socket;
    }

    public Socket getSocket() {
        return socket;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

服务端部署

为了方便在服务器上运行服务端代码,我还特意写了一个 Shell 脚本用来处理服务端代码的运行、停止、重启、查找等操作。

在这里插入图片描述

CHAT_SERVER_DIR=/usr/app/chat
CHAT_SERVER=ChatServer
CHAT_LOG_FILE=${CHAT_SERVER_DIR}/chat.log
# 聊天室启动参数
CHAT_CFG=${2}

help() {
	echo "=================="
	echo "start 启动服务"
	echo "stop 停止服务"
	echo "restart 重启服务"
	echo "find 查找服务"
	echo "help 帮助"
	echo "=================="
}

start() {
	javac -encoding UTF-8 ${CHAT_SERVER}\.java
	nohup java -Dfile.encoding=UTF-8 ${CHAT_SERVER} ${CHAT_CFG} >>${CHAT_LOG_FILE} 2>&1 &
	echo "服务${CHAT_SERVER}已启动"
}

stop() {
	PID=$(ps -ef | grep java | grep ChatServer | awk '{print $2}')
	if [ "${PID}" == "" ]
	then
		echo "服务${CHAT_SERVER}已停止"
	else
		kill ${PID}
		echo "服务${CHAT_SERVER}已停止"
	fi
}

restart() {
  	stop
	sleep 3
	start
	echo "服务${CHAT_SERVER}已重启"
}

find() {
  	PID=$(ps -ef | grep java | grep ChatServer | awk '{print $2}')
  	if [ "${PID}" == "" ]
	then
		echo "服务${CHAT_SERVER}已停止"
	else
		echo "服务${CHAT_SERVER}正在运行:PID=${PID}"
	fi
}

case ${1} in
	"")
		echo "=== 参数错误 ==="
		;;
	start)
		start
		;;
	stop)
		stop
		;;
	restart)
		restart
		;;
	find)
		find
		;;
	*)
		help
		;;
esac

exit 0

优点与缺点

优点

  • 简单便捷,无论是客户端还是服务端,都只依赖了 JDK 的环境,没有任何第三方依赖,客户端的代码只在复制到有 JDK 环境的电脑上即可运行,方便快捷。
  • 足够隐蔽,客户端在 CMD 或者 IDEA 环境下都可以运行,这样你身边的同事只要不仔细观察你的电脑屏幕,绝对想不到你是在和朋友聊天,只以为你是在认真工作。

缺点

  • 服务端基于阻塞式网络 IO 开发,服务端只能够承受有限个的客户端链接。(Java NIO 版本可以解决这个缺点)
  • 太过简陋,因为只是单纯地进行网络 IO 的写入和读取,所以对于一些复杂的网络环境问题都没有进行处理,比如网络黏包的问题,在客户端连接较多的情况下,可能会发生网络黏包的问题,导致一些消息粘黏在一起,发送给客户端。
  • 需要一个服务器,因为这款即使通讯工具是 CS 模式,需要一个服务器运行服务端代码。

最后

虽然这款即时通讯工具确实还不够完善,但是如果只是用于几个朋友之间简单地进行聊天,这款即时通讯工具还是非常给力的。

这款即时通讯工具的源代码已经被托管到了 GitHub 上,同时还附带了这款即时通讯工具的 Java NIO 版本,有兴趣的同学的可以访问我的 GitHub 下载源码。

GitHub:https://github.com/herenpeng/chat.git

同时,我还在 Gitee 上提供了仓库镜像。

Gitee:https://gitee.com/herenpeng/chat.git

如果你喜欢这款即时通讯工具,希望各位同学可以给我的 GitHub 或者 Gitee 仓库点一个 Star,非常感谢!

在这里插入图片描述

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-08 11:07:41  更:2021-08-08 11:08:32 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 14:07:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码