系统功能

使用NIO实现一个多人聊天室。聊天室包含以下功能。

服务端

处理客户连接

新连接客户端注册名字,并进行重名判断

新用户注册后向客户端广播用户列表

接收客户端消息并单播或广播

客户端

向服务端发起连接

用户注册名称

接收服务端广播消息

发送聊天消息,支持单播和广播

系统设计

系统类设计

系统包括四个类,分别为:

消息处理类:Message,处理消息的编解码

消息枚举:MessageType,定义消息类型

聊天服务端:ChatServer

聊天客户端:ChatClient

系统业务流程

服务端启动

客户端启动

客户端注册

服务端向客户端发送注册用户名提示,消息类型:REG_SERVER_SYN

客户端向服务端发送注册用户名,消息类型:REG_CLIENT_ACK

服务端向客户端发送注册确认消息,消息类型:REG_SERVER_ACK

服务端向所有客户端广播用户列表,消息类型:BROADCAST_USER_LIST

发送聊天信息

客户端向服务端发送聊天信息,指定toUser为单播,否则广播,消息类型:CHAT_MSG_SEND

服务端接收聊天信息,进行单播或关闭,消息类型:CHAT_MSG_RECEIVE

客户端显示消息内容

消息设计

系统消息采用简单的特殊字符串 String MSG_SPLIT = "#@@#" 分割字段的格式,消息格式分两种:

message_type#@@#message_content格式,即命令+数据的格式

message_type#@@#option#@@#message_content,其中option为附加消息,比如客户端发送单播聊天信息时指定toUser。

程序注意点

服务端为单线程模式,由一个Selector处理所有消息。

客户端注册后,用户名信息保存在服务端对应SelectionKey.attachment属性中。

通过Selector.keys可获取所有向Selector注册的客户端,获取客户端连接列表时,需要过滤掉ServerSocketChannel和关闭的Channel selector.keys().stream().filter(item -> item.channel() instanceof SocketChannel && item.channel().isOpen()).collect(Collectors.toSet());。

当Socket连接的一端关闭时,另一端会触发 OP_READ 事件,但此时 socketChannel.read(byteBuffer) 返回-1或抛IOException,需要捕获这个异常并关闭socketChannel。

客户端因为要同时处理服务端发送的数据和接收客户端消息输入,如果单线程,在客户端输入消息时,线程阻塞,无法接受服务端消息。所以客户端使用2个线程,主线程处理服务端消息,启动一个子线程接收用户输入并处理。

客户端分为两个阶段

初始为注册阶段 messageType = MessageType.REG_CLIENT_ACK

收到服务器端注册成功消息REG_SERVER_ACK后,进入聊天阶段 messageType = MessageType.CHAT_MSG_SEND。

上述第3步,通过Selector.keys获取所有向Selector注册的客户端时,特别注意要过滤已经关闭的Channel,不然处理客户端下线事件时,取到的用户列表会包含刚下线的这个用户,可能是因为Selector只有执行select时才会去刷新并删除关闭的Channel的原因吧。

程序代码

代码还是比较简单的,设计点上面基本都描述了,代码没有注释,将就着看吧。

Message 和 MessageType

package chart;

import java.nio.ByteBuffer;

import java.nio.charset.Charset;

import java.nio.charset.StandardCharsets;

import java.util.Arrays;

import java.util.List;

public class Message {

public static final String MSG_SPLIT = "#@@#";

public static final Charset CHARSET = StandardCharsets.UTF_8;

private MessageType action;

private String option;

private String message;

public Message(MessageType action, String option, String message) {

this.action = action;

this.option = option;

this.message = message;

}

public Message(MessageType action, String message) {

this.action = action;

this.message = message;

}

public ByteBuffer encode() {

StringBuilder builder = new StringBuilder(action.getAction());

if (option != null && option.length() > 0) {

builder.append(MSG_SPLIT);

builder.append(option);

}

builder.append(MSG_SPLIT);

builder.append(message);

return CHARSET.encode(builder.toString());

}

public static Message decode(String message) {

if (message == null || message.length() == 0)

return null;

String[] msgArr = message.split(MSG_SPLIT);

MessageType messageType = msgArr.length > 1 ? MessageType.getActionType(msgArr[0]) : null;

switch (msgArr.length) {

case 2:

return new Message(messageType, msgArr[1]);

case 3:

return new Message(messageType, msgArr[1], msgArr[2]);

default:

return null;

}

}

public static ByteBuffer encodeRegSyn() {

return encodeRegSyn(false);

}

public static ByteBuffer encodeRegSyn(boolean duplicate) {

MessageType action = MessageType.REG_SERVER_SYN;

String message = "Please input your name to register.";

if (duplicate) {

message = "This name is used, Please input another name.";

}

return new Message(action, message).encode();

}

public static ByteBuffer encodeSendMsg(String msg) {

return encodeSendMsg(null, msg);

}

public static ByteBuffer encodeSendMsg(String toUser, String msg) {

MessageType action = MessageType.CHAT_MSG_SEND;

String option = toUser;

String message = msg;

return new Message(action, option, message).encode();

}

public static ByteBuffer encodeReceiveMsg(String fromUser, String msg) {

MessageType action = MessageType.CHAT_MSG_RECEIVE;

String option = fromUser;

String message = msg;

return new Message(action, option, message).encode();

}

public static ByteBuffer encodeRegClientAck(String username) {

MessageType action = MessageType.REG_CLIENT_ACK;

String message = username;

return new Message(action, message).encode();

}

public static ByteBuffer encodeRegServerAck(String username) {

MessageType action = MessageType.REG_SERVER_ACK;

String message = username + ", Welcome to join the chat.";

return new Message(action, message).encode();

}

public static ByteBuffer encodePublishUserList(List userList) {

MessageType action = MessageType.BROADCAST_USER_LIST;

String message = Arrays.toString(userList.toArray());

return new Message(action, message).encode();

}

public MessageType getAction() {

return action;

}

public void setAction(MessageType action) {

this.action = action;

}

public String getOption() {

return option;

}

public void setOption(String option) {

this.option = option;

}

public String getMessage() {

return message;

}

public void setMessage(String message) {

this.message = message;

}

}

enum MessageType {

REG_SERVER_SYN("reg_server_syn"),

CHAT_MSG_SEND("chat_send"),

CHAT_MSG_RECEIVE("chat_receive"),

UNKNOWN("unknown"),

REG_SERVER_ACK("reg_server_ack"),

REG_CLIENT_ACK("reg_client_ack"),

BROADCAST_USER_LIST("broadcast_user_list");

private String action;

MessageType(String action) {

this.action = action;

}

public String getAction() {

return action;

}

public static MessageType getActionType(String action) {

for (MessageType messageType : MessageType.values()) {

if (messageType.getAction().equals(action)) {

return messageType;

}

}

return UNKNOWN;

}

}

ChatServer

package chart;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.util.Iterator;

import java.util.List;

import java.util.Set;

import java.util.stream.Collectors;

public class ChatServer {

public static final int SERVER_PORT = 8080;

Selector selector;

ServerSocketChannel serverSocketChannel;

boolean running = true;

public void runServer() throws IOException {

try {

selector = Selector.open();

serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.bind(new InetSocketAddress(SERVER_PORT));

serverSocketChannel.configureBlocking(false);

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server started.");

while (running) {

int eventCount = selector.select(100);

if (eventCount == 0)

continue;

Set set = selector.selectedKeys();

Iterator keyIterable = set.iterator();

while (keyIterable.hasNext()) {

SelectionKey key = keyIterable.next();

keyIterable.remove();

dealEvent(key);

}

}

} finally {

if (selector != null && selector.isOpen())

selector.close();

if (serverSocketChannel != null && serverSocketChannel.isOpen())

serverSocketChannel.close();

}

}

private void dealEvent(SelectionKey key) throws IOException {

if (key.isAcceptable()) {

System.out.println("Accept client connection.");

SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();

socketChannel.configureBlocking(false);

socketChannel.register(selector, SelectionKey.OP_READ);

socketChannel.write(Message.encodeRegSyn());

}

if (key.isReadable()) {

SocketChannel socketChannel = null;

try {

System.out.println("Receive message from client.");

socketChannel = (SocketChannel) key.channel();

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

socketChannel.read(byteBuffer);

byteBuffer.flip();

String msg = Message.CHARSET.decode(byteBuffer).toString();

dealMsg(msg, key);

} catch (IOException e) {

socketChannel.close();

String username = (String) key.attachment();

System.out.println(String.format("User %s disconnected", username));

broadcastUserList();

}

}

}

private void dealMsg(String msg, SelectionKey key) throws IOException {

System.out.println(String.format("Message info is: %s", msg));

Message message = Message.decode(msg);

if (message == null)

return;

SocketChannel currentChannel = (SocketChannel) key.channel();

Set keySet = getConnectedChannel();

switch (message.getAction()) {

case REG_CLIENT_ACK:

String username = message.getMessage();

for (SelectionKey keyItem : keySet) {

String channelUser = (String) keyItem.attachment();

if (channelUser != null && channelUser.equals(username)) {

currentChannel.write(Message.encodeRegSyn(true));

return;

}

}

key.attach(username);

currentChannel.write(Message.encodeRegServerAck(username));

System.out.println(String.format("New user joined: %s,", username));

broadcastUserList();

break;

case CHAT_MSG_SEND:

String toUser = message.getOption();

String msg2 = message.getMessage();

String fromUser = (String) key.attachment();

for (SelectionKey keyItem : keySet) {

if (keyItem == key) {

continue;

}

String channelUser = (String) keyItem.attachment();

SocketChannel channel = (SocketChannel) keyItem.channel();

if (toUser == null || toUser.equals(channelUser)) {

channel.write(Message.encodeReceiveMsg(fromUser, msg2));

}

}

break;

}

}

public void broadcastUserList() throws IOException {

Set keySet = getConnectedChannel();

List uList = keySet.stream().filter(item -> item.attachment() != null).map(SelectionKey::attachment)

.map(Object::toString).collect(Collectors.toList());

for (SelectionKey keyItem : keySet) {

SocketChannel channel = (SocketChannel) keyItem.channel();

channel.write(Message.encodePublishUserList(uList));

}

}

private Set getConnectedChannel() {

return selector.keys().stream()

.filter(item -> item.channel() instanceof SocketChannel && item.channel().isOpen())

.collect(Collectors.toSet());

}

public static void main(String[] args) throws IOException {

new ChatServer().runServer();

}

}

ChatClient

package chart;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.SocketChannel;

import java.util.Iterator;

import java.util.Set;

public class ChatClient {

Selector selector;

SocketChannel socketChannel;

boolean running = true;

MessageType messageType = MessageType.REG_CLIENT_ACK;

String prompt = "User Name:";

public void runClient() throws IOException {

try {

selector = Selector.open();

socketChannel = SocketChannel.open();

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress("127.0.0.1", ChatServer.SERVER_PORT));

System.out.println("Client connecting to server.");

socketChannel.register(selector, SelectionKey.OP_CONNECT);

while (running) {

int eventCount = selector.select(100);

if (eventCount == 0)

continue;

Set set = selector.selectedKeys();

Iterator keyIterable = set.iterator();

while (keyIterable.hasNext()) {

SelectionKey key = keyIterable.next();

keyIterable.remove();

dealEvent(key);

}

}

} finally {

if (selector != null && selector.isOpen())

selector.close();

if (socketChannel != null && socketChannel.isConnected())

socketChannel.close();

}

}

private void dealEvent(SelectionKey key) throws IOException {

if (key.isConnectable()) {

SocketChannel channel = (SocketChannel) key.channel();

if (channel.isConnectionPending()) {

channel.finishConnect();

}

channel.register(selector, SelectionKey.OP_READ);

new Thread(() -> {

try {

Thread.sleep(500);

printMsgAndPrompt("Start to interconnect with server.");

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

while (running) {

System.out.print(prompt);

String msg = reader.readLine();

if (msg == null || msg.length() == 0)

continue;

if (messageType == MessageType.REG_CLIENT_ACK) {

ByteBuffer bufferMsg = Message.encodeRegClientAck(msg);

channel.write(bufferMsg);

} else {

String[] msgArr = msg.split("#", 2);

ByteBuffer bufferMsg = Message.encodeSendMsg(msg);

if (msgArr.length == 2) {

bufferMsg = Message.encodeSendMsg(msgArr[0], msgArr[1]);

}

channel.write(bufferMsg);

}

}

} catch (IOException e) {

e.printStackTrace();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

}

}).start();

} else if (key.isReadable()) {

try {

SocketChannel channel = (SocketChannel) key.channel();

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

channel.read(byteBuffer);

byteBuffer.flip();

String msg = Message.CHARSET.decode(byteBuffer).toString();

dealMsg(msg);

} catch (IOException e) {

e.printStackTrace();

System.out.println("Server exit.");

System.exit(0);

}

}

}

private void dealMsg(String msg) {

Message message = Message.decode(msg);

if (message == null)

return;

switch (message.getAction()) {

case REG_SERVER_SYN:

printMsgAndPrompt(message.getMessage());

break;

case CHAT_MSG_RECEIVE:

printMsgAndPrompt(String.format("MSG from %s: %s", message.getOption(), message.getMessage()));

break;

case REG_SERVER_ACK:

messageType = MessageType.CHAT_MSG_SEND;

prompt = "Input your message:";

printMsgAndPrompt(message.getMessage());

break;

case BROADCAST_USER_LIST:

printMsgAndPrompt(String.format("User list: %s", message.getMessage()));

break;

default:

}

}

private void printMsgAndPrompt(String msg) {

System.out.println(msg);

System.out.print(prompt);

}

public static void main(String[] args) throws IOException {

new ChatClient().runClient();

}

}

java 多人聊天室_Java高效NIO之多人聊天室相关推荐

  1. java tcp聊天程序_java实现基于Tcp的socket聊天程序

    对于步入编程行业不深的初学者或是已经有所领会的人来说,当学习一项新的技术的时候,非常渴望有一个附上注释完整的Demo.本人深有体会,网上的例子多到是很多,但是很杂不完整,写代码这种东西来不得半点马虎, ...

  2. java输入数量扑克牌排序_Java扑克游戏(多人多牌数比较游戏)的实现

    具体实现步骤如下: 实现扑克Card类,用于储存扑克牌,1-13代表点数,4-1代表花色(黑桃,红桃,梅花,分块) 实现扑克列表CardList类,用于实现生成一副牌,洗牌,发牌的功能 实现玩家Pla ...

  3. java指定存入arraylist值_Java高效打印出0000-9999之间所有的值存到arraylist集合中

    Java高效打印出0000-9999之间所有的值存到arraylist集合中public static void main(String[] args) { /** * 推荐用Java8 的新特性St ...

  4. Java 丢手绢游戏 求和_java入门小程序—17人游戏(丢手绢问题)

    一.问题描述: 17个人围成一个圈,编号为1~17,从第一号开始报数,报到3的倍数的人离开,一直数下去,直到最后只有一个人,求此人编号. 二.问题提示: 使用一维数组,数组元素初始为1,从1开始把数字 ...

  5. 怎么用java做随机选人软件_Java小程序:五人随机选一人并显示姓名

    该程序实现的是用空格键控制五个人编号的滚动(用Timer实现),当选定一个人的时候,显示其名字,具体界面如下: 此程序需要链接数据库,在此用的是Access数据库,数据链接的代码如下: import ...

  6. java 微信定位到市_java 实现微信搜索附近人功能

    最近给andorid做后台查询数据功能,有一个需求是模仿微信的查找附近人功能. 数据库中存储每个用户的经纬度信息及用户信息,通过当前用户传递过来的经纬度查询这个用户半径N公里以内的用户信息. 数据库表 ...

  7. java源码聊天软件_【原创】基于Java NIO的多人在线聊天工具源码实现(登录,单聊,群聊)...

    近来在学习Java NIO网络开发知识,写了一个基于Java NIO的多人在线聊天工具MyChat练练手.源码公开在Coding上: 编写一个基于Java NIO的多人在线聊天工具,需要以下几方面的知 ...

  8. java socket编程聊天室_Java Socket通信之聊天室功能

    Java Socket通信之聊天室功能 发布时间:2020-10-17 14:36:00 来源:脚本之家 阅读:73 作者:LY_624 本文实例为大家分享了Java Socket聊天室功能的具体代码 ...

  9. python基于udp的网络聊天室再用tkinter显示_Python实现网络聊天室的示例代码(支持多人聊天与私聊)...

    实验名称: 网络聊天室 功能: i. 掌握利用Socket进行编程的技术 ii. 掌握多线程技术,保证双方可以同时发送 iii. 建立聊天工具 iv. 可以和单人聊天 v. 可以和多个人同时进行聊天 ...

最新文章

  1. 使用python模拟Simple方式连接ldap
  2. 用 C 语言开发一门编程语言 — 语法解析器
  3. SpringMVC中的数据校验
  4. GIT项目管理工具(part3)--初始化仓库及查看仓库状况
  5. python导入文件列行_python读写csv文件并增加行列的实例代码
  6. 了解css中伪元素 before和after的用法
  7. Istio服务网格路由入门
  8. 阿里再发最严口罩禁令;铁路再次调整免费退票;iOS 13.4 测试版发布 | 极客头条...
  9. ASIHTTPRequest
  10. C语言学习笔记->const和define区别
  11. 全国实时公交查询API接口
  12. python 指定值的位置_python数组查找某个值的位置
  13. matlab求稳定时间ts,一阶方程调节时间ts
  14. 初涉Workflow(2)——XPDL
  15. python 图片分别保存至文件夹(深度学习图片数据集处理)
  16. OPEN3D(python)学习笔记-1.3 法线估计
  17. 委托学习——4. 委托的高级使用
  18. DELL 服务器 PCI-E 6IR 通道卡 6I阵列卡8口SAS SATA (整理)
  19. Windows10 Emacs-SML开发环境搭建
  20. [RK3128][Android 6.0] PWM backlight注册及调用流程

热门文章

  1. 朱丹为什么是文艺青年
  2. 3分钟学会 “今日头条” 富文本编辑
  3. Flex和Silverlight
  4. 15、ts之void,void(0),void 0
  5. MySQL必知必会05:正确设置主键
  6. Thead 多线程技术总结
  7. #今日论文推荐# 谷歌DreamBooth扩散模型实现“以假乱真”,让指定实物在图像中以各种方式展现
  8. 【商城应用】商城积分方案设计
  9. dell主机安装linux方法,DELL服务器安装linux系统.docx
  10. 高级文件系统 - Reiserfs简介