鉴于之前有不少同学在跟我要客户端的代码,我近期整理了一下,把整个工程都传到github上了。地址:https://github.co/Alexlingl/Chatroom
里面有比较详细的工程运行教程,这篇博客则主要对工程的代码实现进行介绍,没有通信知识基础的同学,在看这篇博客之前可以先看下我通信板块的另外几篇博客:
《JAVA通信(一)——输入数据到客户端》
《JAVA通信(二)——实现客户机和服务器通信》
《JAVA通信(三)——实现多人聊天》
前面我们已经了解了通信技术的基本原理,也通过多线程实现了一个服务器同时与多个客户机通信的程序。今天我们来实现一个简单的聊天室。也就是当有客户机给服务器发消息时,这个消息必须同时被发送到其他的客户机。(注意:并不是直接让客户机之间进行连接)
一、待实现的聊天室构想
1.我们先来看一下QQ群是怎样运作的。
首先,用户需要通过验证加入到某一个群;加入之后,每个用户都会有自己的一个聊天室界面,这个界面中实时更新所有群成员发送的消息。
2.整体框架图
3.服务器和单一客户机交互图
A.用户信息正确
B.用户信息错误
二、代码架构
按照我们前面的分析,感觉只需要构建服务器和客户机这两个类就可以实现这个聊天室了。但是这样一来就会造成这两个类中包含了过多的方法,有悖于面向对象的“单一职责原则”。(《面向对象的三大特征和六大基本原则》)不利于我们后期对这个程序进行修改扩展。因此这里我们对这两个类进行了更加仔细的职责划分。总共分成以下五个类。
ChatServer类:服务器类,也是主类,里面包含服务器的创建方法setUpServer(int port)和主函数入口main。当程序开始运行时,它会把相应的端口port设置为服务器。并让其始终处于待连接状态。每当有客户机连接上来时,就实例化一个线程类(ServerThread)对象,并启动一个线程去处理。(也就相当于我们为每个用户提供了一个独立的线程)。
ServerThread类:客户端类。它是一个线程类。里面实现了线程的启动方法run()和客户机服务器的通信处理方法processSocket()。当然在通信之前我们必须要先验证这个用户信息是否正确。这个验证方法我们在DaoTool类中实现。这里直接调用它的验证方法即可
DaoTool:用户信息验证类。里面实现了用户信息的验证方法checkLogin()。并且它还储存了一个模拟的用户信息库userDB。
UserInfo:用户信息类。里面保存了每一个用户的信息,包括用户名和密码。定义了获取用户名和密码的方法。
ChatTools:聊天室类。负责保存当前登录的每一个用户,并且当某一个客户机给服务器发了消息,它需要立即把这条消息转发给其他客户机。
细分后的构图如下:
三、具体的代码实现
package communicatetest4;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ChatServer {
//主函数入口
public static void main(String[] args) throws IOException {
//实例化一个服务器类的对象
ChatServer cs=new ChatServer();
//调用方法,为指定端口创建服务器
cs.setUpServer(9000);
}
private void setUpServer(int port) throws IOException {
// TODO Auto-generated method stub
ServerSocket server=new ServerSocket(port);
//打印出当期创建的服务器端口号
System.out.println("服务器创建成功!端口号:"+port);
while(true) {
//等待连接进入
Socket socket=server.accept();
System.out.println("进入了一个客户机连接:"+socket.getRemoteSocketAddress().toString());
//启动一个线程去处理这个对象
ServerThread st=new ServerThread(socket);
st.start();
}
}
}
package communicatetest4;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
/*
* 每当有客户机和服务器连接时,都要定义一个接受对象来进行数据的传输
* 从服务器的角度看,这个类就是客户端
*/
public class ServerThread extends Thread{
private Socket client;//线程中的处理对象
private OutputStream ous;//输出流对象
private UserInfo user;//用户信息对象
public ServerThread(Socket client) {
this.client=client;
}
public UserInfo getOwerUser() {
return this.user;
}
public void run() {
try {
processSocket();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//在显示屏中打印信息,例如"用户名"、"密码"等等
public void sendMsg2Me(String msg) throws IOException {
msg+="\r\n";
ous.write(msg.getBytes());
ous.flush();
}
private void processSocket() throws IOException {
// TODO Auto-generated method stub
InputStream ins=client.getInputStream();
ous=client.getOutputStream();
BufferedReader brd=new BufferedReader(new InputStreamReader(ins));
sendMsg2Me("欢迎你来聊天,请输入你的用户名:");
String userName=brd.readLine();
sendMsg2Me("请输入密码:");
String pwd=brd.readLine();
user=new UserInfo();
user.setName(userName);
user.setPassword(pwd);
//调用数据库,验证用户是否存在
boolean loginState=DaoTools.checkLogin(user);
if(!loginState) {
//如果不存在这个账号则关闭
this.closeMe();
return;
}
ChatTools.addClient(this);//认证成功,把这个用户加入服务器队列
String input=brd.readLine();
while(!input.equals("bye")) {
System.out.println("服务器读到的是:"+input);
ChatTools.castMsg(this.user, input);
input=brd.readLine();
}
ChatTools.castMsg(this.user, "bye");
this.closeMe();
}
//关闭当前客户机与服务器的连接。
public void closeMe() throws IOException {
client.close();
}
}
package communicatetest4;
import java.io.IOException;
import java.util.ArrayList;
/*
* 定义一个管理类,相当于一个中介,处理线程,转发消息
* 这个只提供方法调用,不需要实例化对象,因此都是静态方法
*/
public class ChatTools {
//保存线程处理的对象
private static ArrayList stList=new ArrayList();
//不需要实例化类,因此构造器为私有
private ChatTools() {}
//将一个客户对应的线程处理对象加入到队列中
public static void addClient(ServerThread st) throws IOException {
stList.add(st);//将这个线程处理对象加入到队列中
castMsg(st.getOwerUser(),"我上线了!目前人数:"+stList.size());
}
//发送消息给其他用户
public static void castMsg(UserInfo sender,String msg) throws IOException {
msg=sender.getName()+"说:"+msg;//加上说的对象
for(int i=0;i
package communicatetest4;
import java.util.HashMap;
import java.util.Map;
//定义一个处理用户登录信息的类
public class DaoTools {
//内存用户信息数据库
private static MapuserDB=new HashMap();
//静态块:模拟生成内存中的用户数据,用户名为1~10
//当程序启动时这段代码会自动执行向userDB放入数据
static {
for(int i=1;i<=10;i++) {
UserInfo user=new UserInfo();
user.setName("user"+i);
user.setPassword("psw"+i);
userDB.put(user.getName(), user);
}
}
public static boolean checkLogin(UserInfo user) {
//在只验证用户名是否存在
if(userDB.containsKey(user.getName())) {
return true;
}
System.out.println(user.getName()+"用户验证失败!");
return false;
}
}
package communicatetest4;
//定义一个用户信息的类
public class UserInfo {
private String name;//用户名
private String password;//密码
private String loignTime;//登录时间
private String address;//客户机端口名
public String getName() {
return name;
}
public void setName(String name) {
// TODO Auto-generated method stub
this.name=name;
}
public void setPassword(String psw) {
this.password=password;
}
}
四、程序实现图
五、小总结
1.服务器和客户机的连接。其实在验证用户名和密码之前,客户机就已经和服务器连接上了。如果没有连接,我们是无法把用户名和密码送给服务器进行验证的。只是当我们验证完用户信息后,如果发现这个用户名不存在或者密码不正确时,再去断开客户机和服务器的连接。
2.ChatTools里面都是静态属性和静态方法,因为我们不需要实例化这个类的对象,我们只想要调用它的方法,对静态方法不了解的同学可以看一下我的另一篇博客(《静态方法和非静态方法的区别JAVA》)
3、这种基于BIO模型所写出来的服务器性能比较有限,最大并发数大约在2000到2300左右个客户端。后期我进一步提升了服务器的性能,详情可见博客(《C10k破局(一)——线程池和消息队列实现高并发服务器》)。当然使用线程池只是在BIO模型的基础上做了一定的优化,真想要要大幅度地提升服务器性能就只能使用NIO或者AIO模型进行重构,有兴趣的小伙伴可以看下我的另一篇博客(《基于netty NIO开发的聊天室》)