最近在学Socket编程,为了巩固知识,简单实现了一个网络聊天室;目前只实现了个群聊功能,有时间继续更新和完善,下面附上代码截图,代码上都有详细的注释,如果有看不懂的地方,欢迎留言或私信我。
一、源代码地址:https://github.com/aa792978017/ChatRoom
二、本地多客户端调试效果图:(为了方便本地调试区分不同客户端,这里把用户名都设置为了“路人xxxx”,可以调整为用户名)
三、项目结构:
四、类代码分析:
1、ChatProtocol类:存放了一些公共的变量和方法。
/*
* Copyright 2019-2022 the original author or authors.
*/
public class ChatProtocol {
/** 服务端口号 */
public static final int PORT_NUM = 8080;
/** 消息类型为登录 */
public static final char CMD_LOGIN = 'A';
/** 消息类型为私发信息,暂未用上 */
public static final char CMD_MESG = 'B';
/** 消息类型为登出 */
public static final char CMD_QUIT = 'C';
/** 消息类型为广播(目前所有消息都为广播) */
public static final char CMD_BCAST = 'D';
/** 分隔符,用于分隔消息里的不同部分,识别各种信息*/
public static final int SEPARATOR = '|';
/**
* 判断消息体里面是否含有登录名
* @param message 消息
* @return 是否含有登录名
*/
public static boolean isValidLoginName(String message) {
return message != null && message.length() != 0;
}
}
2、ChatServer类:聊天室服务端实现类。
/*
* Copyright 2019-2022 the original author or authors.
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Java聊天服务器
*/
public class ChatServer {
/**
*
*/
protected final static String CHATMASTER_ID = "Server";
/**
* 任何handle和消息之间分隔串
*/
protected final static String SEP = ": ";
/**
* 服务器套接字
*/
protected ServerSocket serverSocket;
/**
* 连接到服务器的客户端列表
*/
protected List<ChatHandler> clients;
/**
* 调试状态,调整是否以调试的形式启动
*/
private static boolean DEBUG = false;
/**
* main方法,仅构造一个ChatServer,永远不返回
*/
public static void main(String[] args) throws IOException {
System.out.println("java.ChatServer 0.1 starting...");
// 启动时传入-debug,则以debug模式启动,会打印调试信息
if (args.length == 1 && args[0].equals("-debug")) {
DEBUG = true;
}
// 服务器启动后不会终止
ChatServer chatServer = new ChatServer();
chatServer.runServer();
// 如果终止了,说明出现了异常,程序停止了
System.out.println("**Error* java.ChatServer 0.1 quitting");
}
/**
* 构造并运行一个聊天服务
* @throws IOException
*/
public ChatServer() throws IOException {
clients = new ArrayList<>();
serverSocket = new ServerSocket(ChatProtocol.PORT_NUM);
System.out.println("Chat Server Listening on port " + ChatProtocol.PORT_NUM);
}
/**
* 运行服务器
*/
public void runServer() {
try {
// 死循环持续接收所有访问的socket
while (true) {
// 开启监听
Socket userSocket = serverSocket.accept();
// 输入连接到服务器的客户端主机名
String hostName = userSocket.getInetAddress().getHostName();
System.out.println("Accepted from " + hostName);
// 每个客户端的连接都开启一个线程来负责通信
ChatHandler client = new ChatHandler(userSocket, hostName);
// 给客户端返回登录消息
String welcomeMessage;
synchronized (clients) {
// 把处理用户连接信息的线程引用保存起来
clients.add(client);
// 构建欢迎信息
if (clients.size() == 1) {
welcomeMessage = "Welcome! you're the first one here";
} else {
welcomeMessage = "Welcome! you're the latest of " + clients.size() + " users.";
}
}
// 启动客户端线程来处理通信
client.start();
client.send(CHATMASTER_ID, welcomeMessage);
}
} catch (IOException ex) {
// 当前客户端处理报错,输出错误信息,但不抛出异常,服务器需要继续运行,服务其他客户端
log("IO Exception in runServer: " + ex.toString());
}
}
/**
* 日志打印
* @param logMessage 需要打印的信息
*/
protected void log(String logMessage) {
System.out.println(logMessage);
}
/**
* 每个线程处理一个用户对话
*/
protected class ChatHandler extends Thread {
/** 客户端套接字 */
protected Socket clientSocket;
/** 从套接字读取数据 */
protected BufferedReader is;
/** 从套接字上发送行数据 */
protected PrintWriter pw;
/** 客户端的IP */
protected String clientIp;
/** 用户句柄(名称)*/
protected String login;
/** 构造一个聊天程序 */
public ChatHandler(Socket clientSocket, String clientIp) throws IOException {
this.clientSocket = clientSocket;
this.clientIp = clientIp;
// TODO 正式使用时删掉下面这一行,这里为了本地运行多个客户端时,可以区分用户
this.clientIp = "路人"+ UUID.randomUUID().toString().substring(0,8);
is = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream(),"utf-8"));
pw = new PrintWriter(
new OutputStreamWriter(clientSocket.getOutputStream(),"utf-8"), true);
}
@Override
public void run() {
String line;
try {
/**
* 只要客户端保持连接,我们就应该一直处于这个循环
* 当循环结束时候,我们断开这个连接
*/
while ((line = is.readLine()) != null) {
// 消息的第一个字符是消息类型
char messageType = line.charAt(0);
line = line.substring(1);
switch (messageType) {
case ChatProtocol.CMD_LOGIN: // 登录消息类型:A + login(登录名)
// 登录信息内不包含登录名
if (!ChatProtocol.isValidLoginName(line)) {
// 回复登录消息,登录信息不合法
send(CHATMASTER_ID, "LOGIN " + line + " invalid");
// 日志记录
log("LOGIN INVALID from " + clientIp);
continue;
}
// 包含登录名
login = line;
broadcast(CHATMASTER_ID, login + " joins us, for a total of " +
clients.size() + " users");
break;
case ChatProtocol.CMD_MESG: // 私人消息类型:B + 接受用户名 + :+ message(消息内容)ps:私法消息在客户端上还没有实现
// 未登录,无法发送消息
if (login == null) {
send(CHATMASTER_ID, "please login first");
continue;
}
// 解析出接收信息的用户名,消息内容
int where = line.indexOf(ChatProtocol.SEPARATOR);
String recip = line.substring(0, where);
String message = line.substring(where + 1);
log("MESG: " + login + "-->" + recip + ": " + message);
// 查找接收消息的用户线程
ChatHandler client = lookup(recip);
if (client == null) {
// 找不到后,发送该用户未登陆
psend(CHATMASTER_ID, recip + "not logged in");
} else {
// 找到用户,把信息私人发送过去
client.psend(login, message);
}
break;
case ChatProtocol.CMD_QUIT: // 离线消息类型: C
broadcast(CHATMASTER_ID, "Goodbye to " + login + "@" + clientIp);
close();
return; // 这个时候,该ChatHandler线程结束
case ChatProtocol.CMD_BCAST: // 广播消息类型: D + message(消息内容)
if (login != null) {
// this.send(login + "@" + clientIp , line);
login = clientIp; // TODO 正式使用的时候去除这一行,用于本地多客户端调试
broadcast(login, line);
} else {
// 记录谁广播了消息,消息内容是什么
log("B<L FROM " + clientIp);
}
break;
default: // 消息类型无法识别
log("Unknown cmd " + messageType + " from" + login + "@" + clientIp);
}
}
} catch (IOException ex) {
log("IO Exception: " + ex.toString());
} finally {
// 客户端套接字结束(客户端断开连接,用户下线)
System.out.println(login + SEP + "All Done");
String message = "This should never appear";
synchronized (clients) {
// 移除离线的用户
clients.remove(this);
if (clients.size() == 0) {
System.out.println(CHATMASTER_ID + SEP + "I'm so lonely I could cry...");
} else if (clients.size() == 1) {
message = "Hey, you're talking to yourself again";
} else {
message = "There are now " + clients.size() + " users";
}
}
// 广播目前的聊天室状态
broadcast(CHATMASTER_ID, message);
}
}
/**
* 断开客户端连接
*/
protected void close() {
// 客户端socket本来为null
if (clientSocket == null) {
log("close when not open");
return;
}
try {
// 关闭连接的客户端socket
clientSocket.close();
clientSocket = null;
} catch (IOException ex) {
log("Failure during close to " + clientIp);
}
}
/**
* 某个用户发送消息
* @param sender 发送消息的用户
* @param message 消息内容
*/
public void send(String sender, String message) {
pw.println(sender + SEP + message);
}
/**
* 发送私人消息
* @param sender 接受消息的用户
* @param message 消息内容
*/
public void psend(String sender, String message) {
send("<*" + sender + "*>", message);
}
/**
* 向所有用户发送一条消息
* @param sender 发送者
* @param message 消息内容
*/
public void broadcast(String sender, String message) {
System.out.println("Boradcasting " + sender + SEP + message);
// 对client遍历,调用其send方法,进行广播
clients.forEach(client -> {
if (DEBUG) {
// 日志打印向某用户发送消息
System.out.println("Sending to " + client);
}
client.send(sender, message);
});
// 打印日志,完成了广播
if (DEBUG) {
System.out.println("Done broadcast");
}
}
/**
* 通过用户昵称,查找某用户
* @param nick 用户昵称
* @return 返回用户的处理线程
*/
protected ChatHandler lookup(String nick) {
// 同步,遍历查找
synchronized (clients) {
for (ChatHandler client: clients) {
if (client.login.equals(nick)) {
return client;
}
}
}
// 找不到返回null
return null;
}
/** ChatHandler的字符串形式 */
public String toString() {
return "ChatHandler[" + login +"]";
}
}
}
3、ChatClient类:聊天室客户端实现类。
/*
* Copyright 2019-2022 the original author or authors.
*/
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Font;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.UUID;
import javax.swing.JFrame;
/**
* 聊天客户端
*/
public class ChatClient extends JFrame {
private static final long serialVersionUID = -2270001423738681797L;
/** 这里获取系统用户名,如果获取失败,则通过UUID随机生成,取前八位 */
private static final String userName =
System.getProperty("user.name",
"路人"+ UUID.randomUUID().toString().substring(0,8));
/** logged-in-ness的状态 */
protected boolean loggedIn;
/** 界面主框架*/
protected JFrame windows;
/** 默认端口号*/
protected static final int PORT_NUM = ChatProtocol.PORT_NUM;
/** 实际端口号*/
protected int port;
/** 网络客户端套接字*/
protected Socket sock;
/** 用于从套接字读取数据(读取其他聊友发送的消息) */
protected BufferedReader is;
/** 用于在套接字上发送行数据(即用户发送消息到聊天室) */
protected PrintWriter pw;
/** 用于输入TextField tf */
protected TextField input;
/** 用于显示对话的TextArea(消息展示的界面) */
protected TextArea messageView;
/** 登陆按钮 */
protected Button loginButton;
/** 注销按钮 */
protected Button logoutButton;
/** 应用程序的标题 */
protected static String TITLE = "Chat Room Client";
/** 这里设置服务器的地址,默认为本地 */
protected String serverHost = "localhost";
/**
* 设置GUI
*/
public ChatClient() {
windows = this;
windows.setTitle(TITLE);
windows.setLayout(new BorderLayout());
port = PORT_NUM;
// GUI,消息展示界面样式
messageView = new TextArea(30, 80);
messageView.setEditable(false);
messageView.setFont(new Font("Monospaced", Font.PLAIN, 15));
windows.add(BorderLayout.NORTH, messageView);
// 创建一个板块
Panel panel = new Panel();
// 在板块上添加登陆按钮
panel.add(loginButton = new Button("Login"));
loginButton.setEnabled(true);
loginButton.requestFocus();
loginButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
login();
loginButton.setEnabled(false);
logoutButton.setEnabled(true);
input.requestFocus();
}
});
// 在板块上添加注销按钮
panel.add(logoutButton = new Button("Logout"));
logoutButton.setEnabled(false);
logoutButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
logout();
loginButton.setEnabled(true);
logoutButton.setEnabled(false);
loginButton.requestFocus();
}
});
// 消息输入框
panel.add(new Label("Message here..."));
input = new TextField(40);
input.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 判断有无登录,登录后才能发送消息
if (loggedIn) {
// 以广播的方式发送出去,所有人可见
pw.println(ChatProtocol.CMD_BCAST + input.getText());
// 发送后,发消息输入框置空
input.setText("");
}
}
});
// 把消息输入框加入板块
panel.add(input);
// 把板块加入主界面的最下方
windows.add(BorderLayout.SOUTH, panel);
windows.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
windows.pack();
}
/**
* 登陆到聊天室
*/
public void login() {
showStatus("准备登录!");
// 如果已经的登录,则返回
if (loggedIn) {
return;
}
// 没有登录,开始尝试连接到聊天室服务器
try {
// 聊天室服务器地址使用了默认的localhost(127.0.0.1)地址
sock = new Socket(serverHost, port);
TITLE += userName;
is = new BufferedReader(
new InputStreamReader(sock.getInputStream(),"utf-8"));
pw = new PrintWriter(
new OutputStreamWriter(sock.getOutputStream(),"utf-8"),true);
showStatus("获取到聊天服务器的socket");
// 现在假登录,不需要输入密码
pw.println(ChatProtocol.CMD_LOGIN + userName);
loggedIn = true;
} catch (IOException ex) {
showStatus("获取不到服务器的socket " + serverHost + "/" + port + ": " + ex.toString());
windows.add(new Label("获取socket失败: " + ex.toString()));
return;
}
//构建和启动reader: 读取服务器的消息到消息展示区
new Thread(new Runnable() {
@Override
public void run() {
String line;
try {
// 只要登录并且服务器有消息可读
while (loggedIn && ((line = is.readLine()) != null)) {
// 每读取一行消息,换行
messageView.append(line + "\n");
}
} catch (IOException ex) {
showStatus("与其他客户端连接中断!\n" + ex.toString());
return;
}
}
}).start();
}
/** 登出聊天室 */
public void logout() {
// 如果已经登出了,则直接返回
if(!loggedIn) {
return;
}
// 修改登录状态,释放socket资源
loggedIn = false;
try {
if (sock != null) {
sock.close();
}
} catch (Exception ex) {
// 处理异常
System.out.println("聊天室关闭异常: " + ex.toString());
}
}
/**
* 控制输出状态,便于调试
* @param message 状态信息
*/
public void showStatus(String message) {
System.out.println(message);
}
/** main方法 允许客户端作为一个应用程序*/
public static void main(String[] args) {
ChatClient room = new ChatClient();
room.pack();
room.setVisible(true);
}
}
来源:CSDN
作者:一心憧憬
链接:https://blog.csdn.net/aa792978017/article/details/103464719