Java—聊天室的实现
在学习了IO流,多线程以及网络编程的知识之后,我们可以利用所学到的知识做一个小项目,这里我做了一个多人聊天室,实现了群聊和私聊功能,看完分享之后也可以自己去做一个练练手。
首先是整个项目的大体架构:首先要分为服务器端和客户端两个端口。如下图所示
客户端可以向服务器发送信息,并接受服务器返回的信息。而服务器实际上是作为一个中转站:在群聊模式时,将一个客户端的发送的信息转发至其他客户端。而在私聊时,服务器将信息发送到指定的客户端处,达到私聊的效果。
我们先依次附上服务器端与客户端的代码,再讲解实现的具体过程
服务器端(这里用本地主机作为服务器):
public class Server {
private List<MyChannel> all = new ArrayList<MyChannel>();
public static void main(String[] args) throws IOException {
new Server().start();
}
public void start() throws IOException {
ServerSocket server = new ServerSocket(7777);
while(true) {
Socket client = server.accept();
MyChannel channel = new MyChannel(client);
all.add(channel);
new Thread(channel).start(); //一条道路
}
}
/**
* 一个客户一条道路
* 建立服务器与客户端之间的数据通道
*
*/
private class MyChannel implements Runnable {
private DataInputStream dis;
private DataOutputStream dos;
private boolean flag = true;
private String name;
public MyChannel(Socket client) {
try {
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
flag = false;
try {
dis.close();
dos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
//接收客户端的信息
private String receive() {
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
flag = false;
e.printStackTrace();
all.remove(this);
try {
dis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return msg;
}
//向客户端发送信息
private void send(String msg) {
if (null == msg || msg.equals("")) {
return;
}
try {
dos.writeUTF(time());
dos.writeUTF(msg);
} catch (IOException e) {
flag = false;
e.printStackTrace();
all.remove(this); //移除自身
try {
dos.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
private void sendOthers(String msg) {
//判断是否是私聊
if (msg.contains("@") && msg.indexOf(":") > msg.indexOf("@")){
String spot = null;
String secreName = msg.substring(msg.indexOf("@") + 1, msg.indexOf(":"));
String secretMsg = msg.substring(msg.indexOf(":") + 1);
// System.out.println(secreName);
// System.out.println(secretMsg);
for (MyChannel other : all) {
if (secreName.equals(other.name)) {
other.send(name + "悄悄地对你说:" + secretMsg);
}
}
}else{
for (MyChannel other : all) {
if (other == this) {
continue;
}
other.send(msg);
}
}
}
private String time () {
Date now = new Date(System.currentTimeMillis());
String time = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss").format(now);
return time;
}
@Override
public void run () {
send("欢迎加入群聊");
name = receive();
sendOthers(name + "加入了群聊");
while (flag) {
sendOthers(receive());
}
}
}
}
现在我们来分析这段代码
- 1> 首先,Server类中包含一个ArrayList容器,其中保存的是MyChannel类元素,实际上每一个MyChannel类的对象就是一条 连接服务器与客户端的路径(其中数据以流的形式传输)
- 2> 在主函数中实例化了Server对象之后,调用了start()方法,我们看到方法体中首先为服务器端创建了ServerSocket对象,并指定了端口。紧接着就是一个死循环,接受连接到服务器的客户端,并将其信息添加至ArrayList容器中,并为其创建一条线程(线程就绪并开始运行)
- 3> 那么MyChannel类内部又是什么呢?我们画图来分析
- 4> 构造器:只有一个以Socket类的客户端对象为参数的构造器,在该构造器中,传入了客户端对象后,建立于客户端对象的数据通道。
- 5> receive()方法:从数据通道中读取从客户端发送来的信息(字符串)
- 6> time()方法:生成当前的具体时间,并以字符串形式返回
- 7> send(String msg)方法:将time()与msg信息依次发送到客户端
- 8> sendOthers(String msg):私聊部分(私聊形式:@客户端名:私聊信息):先对传入的msg进行分析,若字符串首字符为 ‘@’ 并且字符串中含有 ‘:’时,即判断这条信息是一条私聊信息,将私聊客户端名与私聊内容从msg中分离出来,并将其单独发送给指定的客户端(遍历容器并匹配客户端名)。群聊部分(直接发送内容):遍历整个容器,除了当前客户端,向其他所有客户端发送消息。
- 9> 线程体部分(run()方法 ):在一个客户端接入服务器后,首先向其发送一条”欢迎加入群聊”的信息,再将其加入聊天室的信息发送给其他聊天室内的用户,然后执行死循环 sendOthers(receive()) 方法
客户端(包含三个类):
- 客户端类:
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("请输入昵称");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String name = br.readLine();
Socket client = new Socket("localhost",7777);
//客户端发送
new Thread(new Send(client,name)).start();
//客户端接收
new Thread(new Receive(client)).start();
}
}
我们对客户端代码进行分析:
- Client 类中只有一个主方法,刚开始会要求你为客户端起名,紧接着创建Socket类实例,并与本地服务器的指定端口连接。
- 多线程的问题:我们不能规定客户端是先读取再发送还是先发送再读取,所以为两个功能分别建立一条线程,即读与写可以同时实现,而接受和发送信息的类就是下面要说的Receive类和Send类
- Receive类:
public class Receive implements Runnable{
//输入流
private DataInputStream dis;
//线程标识,判断线程运行状态
private boolean flag = true;
public Receive(Socket client){
try {
dis = new DataInputStream(client.getInputStream());
} catch (IOException e) {
e.printStackTrace();
flag = false;
try {
dis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
//接收数据
public String receive(){
String msg = "";
try {
msg = dis.readUTF();
} catch (IOException e) {
e.printStackTrace();
flag = false;
try {
dis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return msg;
}
@Override
public void run() {
while(flag){
System.out.println(receive());
}
}
}
- Receive类是负责接收从服务器发来的消息的类,主要有三个地方使用:1.刚与服务器连接后服务器发来的欢迎信息的接收。2.群聊信息的接收(时间+内容)。3.私聊信息的接收(时间+私聊内容)
- receive()方法,从输入流中读取信息。
- 线程体(run()方法):只有一个始终接收信息并打印到控制台的循环体
- Send 类:
public class Send implements Runnable{
//控制台输入流
private BufferedReader console;
//管道输出流
private DataOutputStream dos;
//控制线程
private boolean flag= true;
//聊天昵称
private String name;
public Send() {
console = new BufferedReader(new InputStreamReader(System.in));
}
public Send(Socket client,String name){
this();
this.name = name;
try {
dos = new DataOutputStream(client.getOutputStream());
} catch (IOException e) {
//e.printStackTrace();
flag = false;
}
}
//从控制台接收数据
private String getMsgFromConsole() {
try {
return console.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
//发送数据
public void send(String msg){
if (null != msg && !msg.equals("")){
try {
dos.writeUTF(name +": " + msg);
Date now = new Date(System.currentTimeMillis());
String time = new SimpleDateFormat("hh:mm:ss yyyy/MM/dd ").format(now);
System.out.println(time);
System.out.println(name + ":" + msg);
dos.flush();
} catch (IOException e) {
e.printStackTrace();
try {
flag = false;
dos.close();
console.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}else{
try {
dos.writeUTF(name);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
//线程体
send("");
while (flag){
send(getMsgFromConsole());
}
}
}
- Send类负责从控制台读入客户端所输入的信息,并将信息发送至服务器,由服务器判断是私聊信息还是群聊信息。
- 构造器:有一个无参构造器和一个参数为客户端对象和客户端名的构造器,建立对服务器的数据输出流,并保存客户端名
- 线程体(run()方法):先发送一个空字符串,send()方法会自动判断为发送客户端名至服务器,然后再循环体中始终执行发送从控制台读取的字符至服务器的方法
- send()方法:初次发送时,会将客户端名发送至服务器,在之后将从控制台读取信息并发送至服务器,同时在自己的控制台上打印自己发送消息的时间和消息的内容
调试结果
在IDEA 的控制台上运行服务器,将客户端类打jar包在本地Powershell上运行
第一个客户端接入:
服务器控制台信息:
多个客户端接入
服务器控制台信息:
群聊实现:
私聊实现: