要了解该项目,得先有相应的Thread和Socket基础知识。
我们要实现局域网聊天室的群聊系统和实时更新当前在线人数和用户名展示还有私聊系统,但是私聊系统在本文先不做。
服务器要接收3种类型的信息,1是客户的用户名登录信息,2是群聊信息,3是私聊信息
通信系统的客户端
这里我们用javax.swing.*编写
确定需求
对于这种界面,我们完全可以让ai编写。所以我们得先确定需求:
1.一个登录界面,为了简便,只需要用户在输入框(JTextField)输入用户名,然后点击登录按钮(Button)后打开聊天室
2.一个聊天室窗口,该窗口包括3个部分,①聊天区域(JTextArea) ②在线用户展现区域(JScrollPane) ③聊天内容输入框(JTextField)和发送按钮(JButton)
就按这需求和豆包说,得到登录界面和聊天室窗口的ui:
entry_UI.java
communication_UI.java
注意,我们只是要ai给我们生成界面UI,里面的逻辑监听器都由我们自己实现
登录界面完善逻辑
现在我们先看登录界面的逻辑完善
为登录按钮添加事件处理:
// 用匿名函数添加登录按钮事件处理
loginButton.addActionListener(e -> {
String nickname = nameField.getText();//返回用户输入的用户名
if(!nickname.isEmpty()){
try {
login(nickname);//自写的登录函数,用于创建客户端管道传给服务器
//进入聊天室
new communication_UI(nickname,socket);//聊天室的构造函数,需要将用户名和管道送过去,注意,这里的管道在login()函数里创建,这里能用是因为把socket设置为entry_UI的成员变量
frame.dispose();
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
自写的login()函数
void login(String name) throws Exception {
//1.创建Socket管道请求与服务端的socket链接
socket = new Socket(Const.SERVER_IP,Const.SERVER_PORT);//这里参数是我设置的常量,Const.SERVER_IP是127.0.0.1 是服务器的 IP 地址也是本机的默认地址,Const.SERVER_PORT是6666是服务器将要注册的端口
//2.立即发送消息类型1 和自己的名称发给
DataOutputStream dos =
new DataOutputStream(socket.getOutputStream());
dos.writeInt(1);//发送1给服务器说明是用户名登录信息
dos.writeUTF(name);
dos.flush();//因为特殊数据输出流是通过把消息存到缓冲区来节省性能的,所以要用flush函数刷新,以确保信息正确发送
}
ok,本项目的entry_UI.java就编写完了,将用户输入的用户名传给服务器,将用户名和该管道传给communication_UI
接下来是communicaton_UI.java
package UI;
import javax.swing.*;
import java.awt.*;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class communication_UI extends JFrame{
private JTextArea chatArea;// 聊天区域
private JButton sendButton; // 发送按钮
private Socket socket;
private String nickname;
public JList<String> onLineUsers = new JList<>();// 在线用户列表
public communication_UI() {
initialize();
this.setVisible(true);
}
public communication_UI(String nickname, Socket socket){//有参构造器
this();// 调用自己的无参构造
this.nickname = nickname;
this.socket = socket;
this.setTitle(nickname + "的聊天室");
//立即把客户端的这个socket管道交给一个独立的线程专门负责读取管道从服务端收到在线人数更新数据或者群聊数据
new ClientReader_Thread(socket, this).start();//自写线程
}
private void initialize() {
// 创建主窗口
this.setTitle("聊天窗口");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置关闭操作
this.setSize(800, 600);
this.setLocationRelativeTo(null); // 居中显示
// 创建主面板,使用BorderLayout布局
JPanel mainPanel = new JPanel(new BorderLayout());
// 创建中央聊天区域
chatArea = new JTextArea();
chatArea.setEditable(false);
JScrollPane chatScrollPane = new JScrollPane(chatArea);
mainPanel.add(chatScrollPane, BorderLayout.CENTER);
// 创建右侧在线用户区域
Font font = new Font("SimKai", Font.PLAIN, 14);
onLineUsers.setFont(font);
onLineUsers.setFixedCellWidth(120);
onLineUsers.setVisibleRowCount(13);
JScrollPane userListScrollPane = new JScrollPane(onLineUsers);
userListScrollPane.setBorder(BorderFactory.createEmptyBorder());
userListScrollPane.setPreferredSize(new Dimension(120, 500));
mainPanel.add(userListScrollPane, BorderLayout.EAST);
// 创建底部面板,使用BorderLayout布局
JPanel bottomPanel = new JPanel(new BorderLayout());
// 创建消息输入框
// 消息输入框
JTextField messageField = new JTextField();
bottomPanel.add(messageField, BorderLayout.CENTER);
// 创建发送按钮
sendButton = new JButton("发送");
bottomPanel.add(sendButton, BorderLayout.EAST);
//给发送按钮绑定点击事件
sendButton.addActionListener(e -> {
String msg = messageField.getText();
messageField.setText("");
//发送消息给服务器
sendMsgToServer(msg);
});
// 将底部面板添加到主面板
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
// 设置主面板到窗口
this.setContentPane(mainPanel);
// 设置窗口可见
// this.setVisible(true);
}
private void sendMsgToServer(String msg) {
//从数据中得到一个特殊数据输出流
DataOutputStream dos = null;
try {
dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(2);
dos.writeUTF(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 在事件调度线程中创建和显示GUI
// SwingUtilities.invokeLater(communication_UI::new);
new communication_UI();
}
public void updateOnLineUsers(String[] onLineName) {
// 在事件调度线程中更新界面(避免界面不同步)
onLineUsers.setListData(onLineName);
}
//更新群聊消息到聊天框
public void setMsgToWin(String msg) {
chatArea.append(msg);
}
}
局域网内通信的服务器
首先要有一个Map集合存储用户的端口和用户名,因为每一个端口都有一个用户名,用户名可以重复,但是端口不会重复,所以用Map<Sorcket,String>集合。
然后注册服务器端口,接收客户端链接后交给线程来进行后续工作
server.java
package server;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class server {
public static final Map<Socket, String> onLine_Users = new HashMap<>();
public static void main(String[] args) {
//建立一个服务端系统
System.out.println("服务端启动");
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(Const.PORT);//这里的Const.PORT=6666
//主线程负责接受客户端的链接请求
while (true) {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();//接收客户端链接并且返回链接服务端和客户端的管道
System.out.println("一个客户端连接了");
new server_Thread(socket).start();//把管道交给线程对象处理
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
server_Thread.java
package server;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
public class server_Thread extends Thread{
private Socket socket;
public server_Thread(Socket socket){//有参构造器
this.socket = socket;
}
@Override
public void run() {
try{
DataInputStream dis = new DataInputStream(socket.getInputStream());//特殊数据输入流
while(true){
int type = dis.readInt();//读取数据类型
switch(type){
case 1:
//处理登录请求
String username = dis.readUTF();
server.onLine_Users.put(socket,username);
//更新所有客户端的在线人数列表
updateClientList();//更新在线用户列表
break;
case 2:
//客户端发来群聊消息,接收消息内容,再把消息发给所有在线用户
String msg = dis.readUTF();
sendMsgToAll(msg);
break;
case 3:
//客户端发来私聊消息,接收消息内容,再把消息发给指定用户
break;
default:
//处理其他请求
}
}
}
catch(Exception e){
System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
//客户端下线了,从在线用户列表中移除
server.onLine_Users.remove(socket);
updateClientList();//更新在线人数列表
}
}
private void updateClientList(){
//更新全部客户端的在线人数列表
//拿到全部在线客户端的用户名称,把这些名称全转发给在线socket管道
Collection<String> onLineUser = server.onLine_Users.values();//返回集合的迭代器
for (Socket socket : server.onLine_Users.keySet()){
try {
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());//获取每一个客户端管道的特殊数据输出流
dos.writeInt(1);
dos.writeInt(onLineUser.size());
for (String username : onLineUser) {//把所有用户名都推给每一个客户端
dos.writeUTF(username);
}
dos.flush();
}
catch(Exception e){
e.printStackTrace();
}
}
}
void sendMsgToAll(String msg){
//拼装好所有消息发给所有客户端
StringBuilder sb = new StringBuilder();
String name = server.onLine_Users.get(socket);
//获取当前时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
String nowStr = dtf.format(now);
String msgResult = sb.append(name).append(" ").append(nowStr).append("\n")
.append(msg).append("\n").toString();
for (Socket socket : server.onLine_Users.keySet()){
try {
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(2);
dos.writeUTF(msgResult);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}