功能
水了一发控制台上的聊天室,能够发送公共消息和私密消息,效果如下
思路
用户端初始时,首先输入将要在聊天室中显示的用户名,然后创建与服务器的 TCP 连接,新建两个线程用以发送与接收消息。在发送线程初始化时,将刚刚输入的用户名发送到服务器,之后就开始无限循环工作了。
用户可以发送两种类型的消息,公共消息和私密消息。公共消息没有格式,私密消息的格式为 @<接收人>:<内容>。
服务器端用 ServerSocket 创建套接字,阻塞式等待新的连接。当有用户试图连接服务器时,服务器新建一个 Channel 线程,来收发该用户的消息,并将该 Channel 加入用户表中,以转发消息。
服务器收到用户发送的公共消息时,会将其转发给所有其他在线用户,通过遍历用户表来实现;至于私密消息,则遍历用户表,一一比对用户表中的用户名与私密消息的目标用户名,将消息转发给匹配的用户。服务器还能发送系统消息,以提醒用户上下线等。
消息的实现是通过一个 Message 类,非常粗糙,只有通过一个 Type 来区分公共消息、私密消息、系统消息,而不是继承(水啊水)。重写了 toString 方法,以更方便的展示消息。
代码
服务器端
package NET_CHATROOM;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;
public class Server {
// 适合多线程的数据结构
private static CopyOnWriteArrayList<Channel> clientList = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("-----SERVER-----");
// 指定端口,使用ServerSocket创建服务器套接字
ServerSocket server = new ServerSocket(8888);
while (true) {
// 阻塞式等待TCP连接
Socket client = server.accept();
System.out.println("建立TCP连接");
// 创建新线程处理业务
Channel channel = new Channel(client);
// 将当前连接加入用户表中
clientList.add(channel);
// 开启线程
new Thread(channel).start();
}
}
static class Channel implements Runnable {
private Socket socket;
private ObjectInputStream ois;
private ObjectOutputStream oos;
private String username;
private boolean isOnline;
Channel(Socket socket) throws IOException, ClassNotFoundException {
this.socket = socket;
this.ois = new ObjectInputStream(this.socket.getInputStream());
this.oos = new ObjectOutputStream(this.socket.getOutputStream());
// 得到用户名
this.username = receive().getUsername();
this.isOnline = true;
// 发送欢迎消息和提醒上线消息
send(new Message(username).genSysMsg(Message.SysMsgType.WELCOME));
sendToOthers(new Message(username).genSysMsg(Message.SysMsgType.ONLINE));
}
private Message receive() throws IOException, ClassNotFoundException {
return (Message) ois.readObject();
}
private void send(Message msg) throws IOException {
oos.writeObject(msg);
oos.flush();
}
private void send(Message msg, String toUsername) throws IOException {
for (Channel channel: clientList) {
if (channel.username.equals(toUsername)) {
channel.send(msg);
break;
}
}
}
private void sendToOthers(Message msg) throws IOException {
for (Channel client: clientList) {
if (client != this) {
client.send(msg);
}
}
}
private void release() throws IOException {
Utils.close(socket, ois, oos);
}
@Override
public void run() {
while (isOnline) {
try {
// 接收消息,然后按消息类型发送
Message msg = receive();
if (msg.getType() == Message.Type.PRI) {
send(msg, msg.getToUsername());
} else {
sendToOthers(msg);
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
// 释放资源,发送下线提醒
try {
isOnline = false;
sendToOthers(new Message(username).genSysMsg(Message.SysMsgType.OFFLINE));
release();
clientList.remove(this);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
}
用户端
package NET_CHATROOM;
import javax.rmi.CORBA.Util;
import java.io.*;
import java.net.Socket;
import java.util.Date;
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("-----CLIENT-----");
// 初始化用户名
System.out.print("请输入你的用户名:");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String username = reader.readLine();
Socket server = new Socket("localhost", 8888);
// 开启接收与发送两个线程
new Thread(new Send(server, username)).start();
new Thread(new Receive(server)).start();
}
}
class Send implements Runnable {
private boolean isOnline;
private String username;
private Socket socket;
private ObjectOutputStream oos;
private BufferedReader reader;
Send(Socket socket, String username) throws IOException {
this.isOnline = true;
this.username = username;
this.socket = socket;
this.oos = new ObjectOutputStream(socket.getOutputStream());
this.reader = new BufferedReader(new InputStreamReader(System.in));
send("初始化用户名");
}
private void send(String content) throws IOException {
// 根据发送的内容判断消息类型
if (content.charAt(0) == '@') {
// 私密消息格式 @<接收人>:<内容>
String toUsername = content.split(":")[0].substring(1);
oos.writeObject(new Message(toUsername, username, content, new Date(), Message.Type.PRI));
} else {
oos.writeObject(new Message(username, content, new Date(), Message.Type.PUB));
}
oos.flush();
}
private void release() throws IOException {
Utils.close(socket, oos, reader);
}
@Override
public void run() {
while (isOnline) {
try {
send(reader.readLine());
} catch (IOException e) {
e.printStackTrace();
isOnline = false;
try {
release();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
class Receive implements Runnable {
private boolean isOnline;
private Socket socket;
private ObjectInputStream ois;
Receive(Socket socket) throws IOException {
this.isOnline = true;
this.socket = socket;
this.ois = new ObjectInputStream(socket.getInputStream());
}
private Message receive() throws IOException, ClassNotFoundException {
return (Message) ois.readObject();
}
private void release() throws IOException {
Utils.close(socket, ois);
}
@Override
public void run() {
while (isOnline) {
try {
Message msg = receive();
if (msg != null) {
System.out.println(msg.toString());
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
isOnline = false;
try {
release();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
消息类
package NET_CHATROOM;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Message implements Serializable {
enum Type {PUB, PRI, SYS}
enum SysMsgType {WELCOME, ONLINE, OFFLINE}
private String toUsername;
private String username;
private String content;
private Date date;
private Type type;
Message(String username) {
this.username = username;
}
Message(String username, String content, Type type) {
this.username = username;
this.content = content;
this.type = type;
}
Message(String username, String content, Date date) {
this.username = username;
this.content = content;
this.date = date;
}
Message(String username, String content, Date date, Type type) {
this.username = username;
this.content = content;
this.date = date;
this.type = type;
}
Message(String toUsername, String username, String content, Date date, Type type) {
this.toUsername = toUsername;
this.username = username;
this.content = content;
this.date = date;
this.type = type;
}
// 生成系统消息
Message genSysMsg(SysMsgType smt) {
switch (smt) {
case WELCOME:
return new Message("【系统消息】", username + "欢迎您", Type.SYS);
case ONLINE:
return new Message("【系统消息】", username + "上线了", Type.SYS);
case OFFLINE:
return new Message("【系统消息】", username + "下线了", Type.SYS);
default:
return null;
}
}
@Override
public String toString() {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
switch (type) {
case PUB:
return String.format("%s %s\n\t%s", username, df.format(date), content);
case PRI:
return String.format("【私密消息】%s %s\n\t%s", username, df.format(date), content);
case SYS:
return String.format("%s %s", username, content);
default:
return null;
}
}
public String getToUsername() {
return toUsername;
}
public void setToUsername(String toUsername) {
this.toUsername = toUsername;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public static void main(String[] args) {
Message msg = new Message("sender", "hello", new Date(), Type.PUB);
System.out.println(msg.toString());
}
}
工具类
package NET_CHATROOM;
import java.io.Closeable;
import java.io.IOException;
public class Utils {
public static void close(Closeable... closeables) throws IOException {
for (Closeable closeable: closeables) {
if (closeable != null) {
closeable.close();
}
}
}
}