开发工具与关键技术:MyEclipse 10、JAVA
作者:曾浩源
撰写时间:2019年05月27日
多人进行群聊,肯定分服务端和客户端。(这里是java的窗体应用JFrame)
一、群聊服务端怎么写,有什么作用。
群聊服务端相当于一个中转站,负责不断的接收客户端的请求,然后经过指定的处理,最后为所有客户转发信息(也就是响应)
以下所有变量皆为成员变量,在这里写成员变量
(1)启动服务器
1、创建在线用户集合,用于显示所有的在线用户
ArrayList<ClientThread> clients = new ArrayList<ClientThread>();
2、创建服务器
ServerSocket serverSocket = new ServerSocket(port);
3、创建线程
Thread serverThread = new ServerThread(serverSocket, max);
4、启动线程
serverThread.start();
5、设置服务器启动标识
boolean isStart = true;
6、启动服务器的公告消息(聊天室)
contentArea.append("服务器已成功启动!人数上限:" + max + ",端口:" + port + "\r\n");
7、给服务器的账号状态设置为在线
Serveruser.setState(true);
(2)服务器线程
继承线程,重写run方法
class ServerThread extends Thread
1、使用死循环,不断接收客户端请求登录
while (TheradServer)
2、侦听并接收此套接字的连接,如果没有客户端连接那么会阻塞
socket = serverSocket.accept();
3、设置对此客户服务的线程
ClientThread client = new ClientThread(socket);
client.start();// 开启对此客户端服务的线程
clients.add(client);// 添加进用户集合
listModel.addElement(client.getUser().getUsername());// 更新在线列表
// 在公告聊天面板 写入某某上线
contentArea.append(client.getUser().getUsername() + client.getUser().getIp() + "上线!\r\n");
(3)为客户(客户发送消息) 服务的线程
编写构造器和重写run方法
class ClientThread extends Thread
1、带参数Socket客户端套接字的构造器
// 给类的socket赋值
this.socket = socket;
// 设置线程启动的标识,用于关闭线程
this.clientServer = true;
// 获取用户信息
reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
writer = new PrintWriter(socket.getOutputStream());
// 接收客户端的基本用户信息
String inf = reader.readLine();
// 根据传过来的信息,使用@进行分割(信息看客户端)
StringTokenizer st = new StringTokenizer(inf, "@");
// 使用User实体类的构造器传参对其进行 赋值
user = new User(st.nextToken(), st.nextToken());
// 反馈连接成功信息
writer.println(user.getUsername() + user.getIp() + "与服务器连接成功!");
writer.flush();
// 反馈当前在线用户信息
if (clients.size() > 0) {
String temp = "";
// 遍历用户集合
for (int i = clients.size() - 1; i >= 0; i--) {
// 获取用户信息
temp += (clients.get(i).getUser().getUsername() + "/" + clients
.get(i).getUser().getIp()) + "@";
}
// 写入流
writer.println("USERLIST@" + clients.size() + "@" + temp);
writer.flush();
}
// 向所有在线用户发送该用户上线命令 根据@XXX传参到客户端,执行事件
for (int i = clients.size() - 1; i >= 0; i--) {
clients.get(i).getWriter().println("ADD@" + user.getUsername() + user.getIp());
clients.get(i).getWriter().flush();
}
2、重写run方法
//用于存放客户端传来的消息
String message = null;
while (clientServer) {// 死循环接收客户端的请求
try {
// 接收客户端的基本用户信息
message = reader.readLine();
if (message == null) {
continue;//为空跳出本次循环
}
if (message.equals("CLOSE"))// 下线命令
{
contentArea.append(this.getUser().getUsername()
+ this.getUser().getIp() + "下线!\r\n");
// 断开连接释放资源
reader.close();
writer.close();
socket.close();
// 向所有在线用户发送该用户的下线命令
for (int i = clients.size() - 1; i >= 0; i--) {
clients.get(i).getWriter()
.println("DELETE@" + user.getUsername());
clients.get(i).getWriter().flush();
}
listModel.removeElement(user.getUsername());// 更新在线列表
// 删除此条客户端服务线程
for (int i = clients.size() - 1; i >= 0; i--) {
if (clients.get(i).getUser() == user) {
ClientThread temp = clients.get(i);
clients.remove(i);// 删除此用户的服务线程
clientServer = false;
return;
}
}
} else {
// 转发消息 代码跳转到dispatcherMessage继续执行
dispatcherMessage(message);
}
3、转发消息方法
// 转发消息
public void dispatcherMessage(String message) {
// 根据传过来的信息,使用@进行分割(信息看客户端) 例如:姓名@ALL(执行方案)@消息内容
StringTokenizer stringTokenizer = new StringTokenizer(message, "@");
// 获取分割开后的下一个source来源 并标记(来自谁 即:姓名) 仙某人
String source = stringTokenizer.nextToken();
// 获取标记的下一个 owner 业务 并标记(执行方案)
String owner = stringTokenizer.nextToken();
// 获取标记的下一个 content 内容 并标记
String content = stringTokenizer.nextToken();
// 消息为:仙某人说:你好!
message = source + "说:" + content;
// 在服务器公告消息(聊天室)输出
contentArea.append(message + "\r\n");
if (owner.equals("ALL")) {// 群发 客户端发送过来的业务(执行方案)之后的私聊也可以实现了
for (int i = clients.size() - 1; i >= 0; i--) {
// 遍历所有用户,将信息写入流
clients.get(i).getWriter().println(message);
clients.get(i).getWriter().flush();
}
}
}
二、客户端
(1)连接服务端
1、根据端口号和服务器ip建立连接
socket = new Socket(user.getIp(), Constant.SERVER_PORT);
2、写入连接信息
writer = new PrintWriter(socket.getOutputStream());
3、读取连接信息
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
4、发送客户端用户基本信息(用户名和ip地址)
sendMessage(user.getUsername() + "@" + socket.getLocalAddress().toString());
5、开启接收消息的线程
messageThread = new MessageThread(reader, textArea);
6、启用接受信息线程
messageThread.start();
7、设置窗体标题为用户名称
frame.setTitle(user.getUsername());
isConnected = true;// 已经连接上了
8、设置用户登录状态
user.setState(true);
(2)客户端接收服务端信息线程
class MessageThread extends Thread
1、接收消息线程的构造方法
public MessageThread(BufferedReader reader, JTextArea textArea) {
// 获取读取到的客户连接信息 和 文本域(到时候需要使用它进行在聊天室 输出【它是私有的】)
this.reader = reader;
this.textArea = textArea;
ClientThread = true;
}
2、重写run方法
public void run() {
String message = "";
while (ClientThread) {
try {
// 读取接受到的信息
message = reader.readLine();
// 根据传过来的信息,使用@进行分割( 例如:姓名@ALL(执行方案)@消息内容
if (message != null) {
StringTokenizer stringTokenizer = new StringTokenizer(message, "/@");
// 获取到命令 即:业务(执行方案)
String command = stringTokenizer.nextToken();
if (command.equals("CLOSE"))// 服务器已关闭命令
{
ClientThread = false;
textArea.append("系统公告:服务器已关闭!\r\n");
closeCon();// 被动的关闭连接
return;// 结束线程
}
//有用户上线更新在线列表
else if (command.equals("ADD")) {
String username = "";
String userIp = "";
// 判断是否为空并获取用户名,IP地址进行赋值
if ((username = stringTokenizer.nextToken()) != null
&& (userIp = stringTokenizer.nextToken()) != null) {
User user = new User(username, userIp);
onLineUsers.put(username, user);
listModel.addElement(username);
textArea.append("系统公告:" + username + "上 线!\r\n");
}
}
// 有用户下线更新在线列表
else if (command.equals("DELETE")) {
String username = stringTokenizer.nextToken();
User user = (User) onLineUsers.get(username);
textArea.append("系统公告:" + username + "下线!\r\n");
onLineUsers.remove(user);
listModel.removeElement(username);
}
// 加载在线用户列表
else if (command.equals("USERLIST")) {
int size = Integer.parseInt(stringTokenizer
.nextToken());
String username = null;
String userIp = null;
for (int i = 0; i < size; i++) {
username = stringTokenizer.nextToken();
userIp = stringTokenizer.nextToken();
User user = new User(username, userIp);
onLineUsers.put(username, user);
listModel.addElement(username);
}
} else {// 普通消息
textArea.append(message + "\r\n");
}
else {
continue;
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
(3)、客户端群发请求
可以封装一个send()方法,将信息发送到服务器
// !!!!!群发请求!!!!!!
writer.println(frame.getTitle() + "@" + "ALL" + "@" + message);
writer.flush();
样式: