实现多人聊天——简单群聊服务器的实现
作为一个现代人,我们对当前众多的聊天通信平台并不陌生,facebook,qq,微信等都是大部分人每天都会接触的。那你有想过构建一个自己打造的聊天室,按照自己喜欢的模式,然后和朋友一起使用吗?下面就讲下聊天室的前身——群聊服务器的实现,之后你就可以在其基础上设计独属的聊天室了。
要实现群聊服务器,首先要先了解下面的几个问题?
1.Socket 与ServerSocket区别!?
2.程序中的阻塞机制是怎样的?怎样解决“阻塞”现象?
3.群聊服务器至少需要几个类才能实现?各个类应该实现的功能?
接下来将结合代码,逐步带大家了解这些问题的答案,认识体会实现过程的思路与期间遇到的种种问题。
最先,当然就是服务器的创建。建立绑定在指定端口的服务器,然后调用服务器的接受方法,进入阻塞状态。这相当于你开了家咖啡厅,然后等待客人的光顾。不过比较遗憾的是,当前的咖啡厅只能迎接一个客人!!
ServerSocket server=new ServerSocket(port);//建立绑定在指定端口的服务器"+port);
while(true)
{
Socket client=server.accept();//调用服务器接受方法,进入阻塞状态
System.out.println("连接上"+client.getRemoteSocketAddress());
..........}
看着这几行代码,就涉及到我们上面提到的前两个问题:
对于第一个问题,ServerSocket是对应服务器,而Socket是对应客户端;ServerSocket用于绑定端点,提供服务,并接受连接请求。Socket就是普通的连接套接字,在建立网络连接时使用的;在连接成功时,应用程序两端都会产生一个Socket实例,就相当于两头连通了一根电话线,能完成所需的会话。因此,真正进行通信的是服务器端的Socket与客户端的Socket,在ServerSocket 调用accept方法之后,它就“默默退出舞台“,由accept方法产生的Socket主权。
那么程序中的阻塞机制又是怎么回事呢?事实上,当ServerSocket调用accept方法时,会产生服务端的Socket实例,在没有用户连接上绑定的端口号时,此Socket会一直等待连接,相当于”阻塞“。当一个用户连接端口号后,服务端的Socket和用户的Socket就会连接上。若有其他用户连接端口号,会出现无法连接服务器的现象,直到之前连接的用户与服务器断开连接后才能轮到下一位,简而言之就是”一对一“;这也即”阻塞“现象。那怎么破呢???答案就是我们熟悉的线程!一个线程处理一个用户的Socket,这样多线程处理多用户,就能完美解决”僧多粥少“的困境。
因此我们要创建一个线程类,负责处理与用户的连接。当新的用户连接上端口号,就会创建一个线程,传入一个连接。
/*
* 线程类,实现一个线程绑定一个处理对象
*/
public class ServerThread extends Thread{
private Socket client;
private OutputStream ous;
private UserInfo user;
//构造函数
public ServerThread(Socket cs)
{
this.client=cs;
}
//取得线程对象所处理的用户对象
public UserInfo getRelatedUser()
{
return this.user;
}
//处理连接对象的方法,传送服务器与客户端之间的字符串实现聊天功能
private void Process(Socket client) throws IOException
{
InputStream ins=client.getInputStream();
ous=client.getOutputStream();
//将输入流封ins装为可以读取一行字符串,也就是以\r\n结尾的字符串
BufferedReader bfr=new BufferedReader(new InputStreamReader(ins));
//获取到用户输入的用户名与密码
readmsg("请输入你的用户名");
String username=bfr.readLine();
readmsg("请输入你的密码");
String pwd=bfr.readLine();
//创建UserInfo对象,将它用所给信息实体化,以待与数据库中信息比较核实
user=new UserInfo();
user.setUsername(username);
user.setCode(pwd);
//调用数据库模块,验证用户信息是否存在
boolean loginstate=DaoTool.UserLogin(user);
if(!loginstate)
{
readmsg("您输入的用户不存在,请重新输入");
this.closeUp();
return;
}
ChatTool.addClient(this);//调用管理处理类中的方法
String s="你好,可以开始与服务器正式通话";
this.readmsg(s);
//String input=ReadString(ins);//调用读取字符串的方法,读取输入流中的字符串
String input=bfr.readLine();//一行行读取用户输入的信息
while(!input.equals("bye"))
{
System.out.println(this.user.getUsername()+"说:"+input);
//将每条信息传到其他客户上
ChatTool.sendMsg(this.user, input);
//s="服务器收到:"+input+"\r\n";
//this.readmsg(s);
//input=ReadString(ins);//接受下一次通话信息
input=bfr.readLine();
}
ChatTool.clearClient(this);//用户下线,调用管理类的方法
s="通话结束,欢迎再次连接";
this.readmsg(s);
this.closeUp();
}
//读取信息的方法,传入前信息不用加\r\n
public void readmsg(String str)
{
str+="\r\n";
byte[] data=str.getBytes();
try {
ous.write(data);
ous.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//读取字符串的方法
private String ReadString(InputStream ins) throws IOException
{
StringBuffer stb=new StringBuffer();//创建字符串缓冲区
char c=0;
//读取字符串,当按下空格键时表示一个字符串输入完成
while(c!=13)
{
int ch=ins.read();
c=(char)ch;//强制转换,将c转换成字符型
stb.append(c);//将读取的字符添加到字符串缓冲区上
}
String str=stb.toString().trim();//读取字符串缓冲区中的完整字符串,去掉空格
return str;
}
public void run()
{
//在线程中调用连接处理方法
try {
Process(this.client);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//处理方法结束后,线程就退出
}
//关闭线程处理对象
public void closeUp()
{
try {
client.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
其中有几个地方要解释下,归纳如下:
1、输出要调用flush函数:flush函数是把缓冲区的数据强行输出。在读写时,数据是先被读到内存当中,后再写到文件里;故当数据读完并不意味着数据写完,若此时调用close方法关闭IO流,遗留在内存缓冲区上的数据就会流失。所以要在close前先flush一下,正如买的饮料要喝完才丢掉瓶子!
2.发送字符串时,首先调用字符串的 getBytes()方法,得到组成这个字符串的字节数组,发送出的实际上是这个字节数组。读取字符串的时候,有两种方法。第一种,因为系统本身是一个字节一个字节的读取,我们
可以创建一个字符串缓冲区,每次把读的字节转换成字符形式再放进去,当读到回车再把缓冲区的数据输出来,如此便能读取字符串了。第二种,就是使用了系统提供的 BufferedReader API,包装了从 Socket
上得到的输入流对象,调用其已有的 readLine()方法读取一行字符串,如此在读取中文时不会乱码。
对于群聊服务器,其实最初应该有一个验证环节。用户输入用户名和密码,然后系统核实后才能连接上。所以还需要有三个类:用户信息类、数据访问类、连接处理类。
1)用户信息类,就是定义用户属性,如用户名、密码、登陆时间、地址等等。
2)数据访问类,即是对用户数据进行增、删、查、改,即 CRUD 操作。这样的一个类命名时,通常以 Dao 作为前缀( Data Access Object)。这里暂时只提供确认用户存在的方法,其他方法大家可以自行添加。
/*
* 数据对象访问类
*/
public class DaoTool {
//用户信息数据库,储存所有用户信息
private static Map<String,UserInfo> UserDP=new HashMap();
//确认用户存在的方法,核对用户名是否与用户信息数据库中的匹配
public static boolean UserLogin(UserInfo ui)
{
if(UserDP.containsKey(ui.getUsername()))
{
return true;
}
System.out.println("您输入的用户不存在,请重新输入!");
return false;
}
//静态块,在类自动加载之前先存入10个用户信息
static
{
for(int i=0;i<10;i++)
{
UserInfo user=new UserInfo();
user.setUsername("user"+i);
user.setCode("pwd"+i);
UserDP.put(user.getUsername(), user);//存入用户信息数据库
}
}
}
这里出现了静态块,它里面的内容会在类加载之前就先执行。java中的类加载机制分为预先加载和依需求加载。java运行所需的基本类是预先加载,而我们平常用的new关键字就是进行依需求加载;如定义一个类实例时,Student stu=new Student();此时JRE才真正把Student类加载进来。
3)用户连接类,用于管理连接处理的线程对象。
//连接处理类,管理连接处理线程对象
public class ChatTool {
private static List <ServerThread>stList=new ArrayList();//创建队列,存储所有的连接处理线程
private ChatTool(){};//设置构造器私有,其他类无法生成该类对象,但可以调用其方法,适用于工具类
/*
* 当新的用户连接上服务器时,会同时产生一个连接处理类线程对象,把这个线程添加到队列中
*/
public static void addClient(ServerThread st) throws IOException
{
stList.add(st);//添加线程到队列上
sendMsg(st.getRelatedUser(),"我上线了,大家好!!目前在线人数:"+stList.size());
}
/*
* 当用户退出连接时,对应的线程对象也从队列中移除
*/
public static void clearClient(ServerThread st)
{
stList.remove(st);//从队列中移除线程
sendMsg(st.getRelatedUser(),st.getRelatedUser().getUsername()+"下线了,目前在线人数:"+stList.size());
}
/*
* 发送信息给每个在线的用户
*/
public static void sendMsg(UserInfo user,String msg)
{
msg=user.getUsername()+"说"+msg;
for(int i=0;i<stList.size();i++)
{
ServerThread st=stList.get(i); //从队列中获取到所有用户
st.readmsg(msg);//将信息输出给每个用户
}
}
}
这里大家应该会注意到构造器的私有设定!当构造器私有时,它只能被包含它的类自身所访问,而无法在类的外部调用,故而可以阻止对象的生成。这种构造器私有的用法一般是针对工具类的,如字符串的验证、枚举转换之类的,可以认为是作为静态接口被外部调用。我们不需要实例它们,只是需要类名调用到它们里面的方法即可。
通过这5个类,我们就能实现简单的群聊服务器了!