在上一篇总结中,我基本完成了一个简易的聊天工具。可是那个聊天工具的太过死板,必须严格遵循一定的顺序,收发消息。比如服务器是按照什么顺序写消息的,那么客户端就必须得按什么顺序读消息。这样以来的话当一个通信的工程过于繁杂的时候难免会出错。这就需要我们制定一个通信协议来规定这个通信过程的规则!如此一来,就不用顾虑读写的顺序,而只用在乎读写的消息类型!同时,制定协议以后一条消息中可能包含多个内容,这也方便我们做更多的扩展!
首先让我来大体分析一下交互的流程, 交互流程描述如下:
1.客户机与服务器建立tcp/ip连结后,发送的第一条消息,只能是登陆/注意请求消息
2.某客户机登陆成功,服务器对其发送在线用户列表,并对在线用户发送有人上线的消息;
3.服务器接到客户机发送的聊天消息后,将这条消息发送给指定的客户机用户
4.某个用户下线后,对所有客户机发送下线的消息;
在规定了这样的一个流程以后,我再来制定一个XMPP协议,也就是一个通信的规则!我写的XMPP是基于XML标签语言进行扩展的,具体分为以下8类:
1.登录请求 :
<m><type>login</type><name>用户名</name><pwd>密码</pwd></m>
2.登录应答:
<m><type>loginResp</type><state>登录结果</state></m> //0:成功,1:用户名错误 ,2:密码错误
3.注册消息:
<m><type>reg</type><name>用户</name><pwd>密码</pwd></m>
4.注册应答消息
<m><type>regResp</type><state>注册结果</state></m> //0表示成功
5.聊天消息:
<m><type>chat</type><sender>发送者名字</sender><reciever>接受者名字</reciever><msg>聊天内容</msg></m>
6.在线用户表信息:
<m><type>budyList</type><user>用户1,用户2,...</user></m> //在线用户以逗号分离
7.上线消息:
<m><type>onLine</type><user>用户</user></m> //上线者名字
8.下线消息:
<m><type>offLine</type><user>用户</user></m> //下线者名字
在分析完以上的流程和规则之后,要做的只是把这样的一个流程和规则加到之前写过的那个以文本和换行来执行通信的群聊工具中去就可以了!唯一不同的是,此时发送的都是XMPP风格的消息。服务器和客户端,通过解析这个XMPP串来确定要发送的消息类型和消息内容既可!以下我分析几个主要的代码,一个是通信辅助类ChatTools类,一个是服务器执行通信的线程类ServerThread类,一个是客户端收发消息的ClientConn类
1.服务器通信辅助类ChatTools类:这个类主要执行当某客户机登陆成功后,服务器对其发送在线用户列表,并对在线用户发送有人上线的消息。同时执行一些转发消息的功能。该类我把一个个客户机的用户名和其对应的线程装入一个码表中,每次发送消息时,我遍历这个码表找到要发送的客户机,然后严格遵守XMPP协议发送既可!具体代码如下:
/**
* xmpp服务器:连结对象管理和消息转发工具类
*/
public class ChatTools {
// key为处理对象代表的用户名,vlaue为对应的处理线程对象
private static Map<String, ProcessThread> pts = new HashMap();
/**
* 当一个处理线程对象启动后,要将其加入到服务器的队列中
*
* @param userName
* :客户名字
* @param pt
* :一个处理线程对象
*/
public static void addPT(String userName, ProcessThread pt) {
pts.put(userName, pt);// 将处理线程对象放入服务器中
Set set = pts.keySet();// 1.取得在线用户列表
Iterator<String> it = set.iterator();
String names = "";
String onlineMsg = "<m><type>onLine</type>";
String onLineContent = onlineMsg + "<user>" + userName + "</user></m>";
// String
// onLineContent="<m><type>online</type><user>"+userName+"</user></m>";
while (it.hasNext()) {
String nextName = it.next();
if (!nextName.equals(userName)) {
// 2.给其它用户发送已上线的消息
pts.get(nextName).sendMsg(onLineContent);
// 拼成"用户名,用户名,..."格式串
names += "," + nextName;
}
}
String head = "<m><type>budyList</type>";
String content = "<user>" + names + "</user>";
String buddyListMsg = head + content + "</m>";
// String buddyListMsg ="<m><type>budyList</type><user>"+names+"</user></m>";
// 给当前用户发送在线好友列表
pt.sendMsg(buddyListMsg);
}
/**
* 将一条消息发给某个用户对象
*
* @param userName
* :发送者名字
* @param msg
* :消息内容
* @param destUserName
* :接收用户名字
*/
public static void castMsg(String userName, String msg, String destUserName) {
for (int i = 0; i < pts.size(); i++) {
ProcessThread pt = pts.get(destUserName);// 取出队列中的一个线程对象
try {// 将消息发给接受者名字所代表的处理对象
if (null != pt && pt.getUserName().equals(destUserName)) {
pt.sendMsg(msg);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 当一个用户下线时,从队列中移除其所对应的处理对象
public static void removeUser(String userName) {
ProcessThread pt = pts.remove(userName);
Set set = pts.keySet();// 取得在线用户列表
Iterator<String> it = set.iterator();
String names = "";
String offLineMsg = "<m><type>offLine</type><user>" + userName
+ "</user></m>";
while (it.hasNext()) {
String nextName = it.next();
if (!nextName.equals(userName)) {
pts.get(nextName).sendMsg(offLineMsg);
names += "," + nextName;
}
}
}
}
2.服务器线程ServerThread类:这个类中主要解析客户端发来的消息类型和消息内容,首先读取发送的第一条消息,只能是登陆/注意请求消息,登录成功后再调用ChatTools中的方法按照XMPP风格的协议执行转发给客户端等功能。具体代码如下:
/**
* 每一个对象对应于一个客户端
*
* @author Administrator
*
*/
public class ProcessThread extends Thread{
private java.net.Socket client;
private java.io.OutputStream ous; // 输出流对象
private java.io.InputStream ins; // 输入流对象
private String userName; // 客户的用户名
private boolean connOK = false; // 是否连接成功
public ProcessThread(Socket client) throws Exception {
this.client = client;
ous = client.getOutputStream();
ins = client.getInputStream();
connOK = true;
}
public void run() {
try {
processClient(this.client);
} catch (Exception e) {
e.printStackTrace();
// 从队列中移除这个用户的处理对象,退出聊天
ChatTools.removeUser(this.userName);
}
}
public String getUserName() {
return this.userName;
}
/**
* 给当前的客户机发送一条消息
*
* @param msg
* :要发送的消息对象
*/
public void sendMsg(String msg) {
try {
ous.write(msg.getBytes());
} catch (Exception e) {
connOK = false;
}
}
private void processClient(Socket client) throws Exception {
if(readFirstMsg()){//登录成功
ChatTools.addPT(this.userName,this);
while(connOK){
String msg=readString();
String type=getXMLValue("type",msg);//解析出消息类型
if(type.equals("chat")){
String destUserName=getXMLValue("reciever",msg);
ChatTools.castMsg(this.userName, msg,destUserName);
}
else{//其它消息,暂不处理
System.out.println("unknow Msg type"+type);
}
}
}
}
//读取客户机发来的第一条消息 注册或者登录
//如果登陆成功,返回true
private boolean readFirstMsg()throws Exception{
//读取客户机发来的第一条消息,登录或注册
String msg = readString();//1是注册,2是登录
String type=getXMLValue("type",msg);
if(type.equals("reg")){//注册请求
userName = getXMLValue("name",msg);//解析用户名
String pwd = getXMLValue("pwd",msg);//解析密码
int state = -1; //预设注册状态
if(ServerDao.saveUser(userName, pwd)){
//调用存储模块保存用户名密码,如果保存成功,则认为注册成功
state=0;
}
//给客户机发送注册应答消息
String resp="<m><type>regResp</type><state>"+state+"</state></m>";
sendMsg(resp);
this.client.close();
}
if(type.equals("login")){//登录请求
userName = getXMLValue("name",msg); //解析用户名
String pwd = getXMLValue("pwd",msg);//解析密码
int state=-1;
if(ServerDao.hasUser(userName, pwd)){
state=0;
}
String resp="<m><type>loginResp</type><state>"+state+"</state></m>";
sendMsg(resp);
if(state==-1){//如果登录失败
this.client.close();
}else{
return true;
}
}
return false;
}
/**
* 从一条xmlMsg消息串中提取flagName标记的值,
* @param flagName:要提取的标记的名字
* @param xmlMsg:要解析的xml消息字符串
* @return:提取到flagName标记对应的值
* @throws:如果解析失败,则是xml消息格式不符协议规范,抛出异常
*/
private String getXMLValue(String flagName, String xmlMsg) throws Exception {
try{
//1.标记头出现的位置
int start = xmlMsg.indexOf("<"+flagName+">");
start+=flagName.length()+2;
//2.结束符出现的位置
int end = xmlMsg.indexOf("</"+flagName+">");
//3.截取标记所代表的消息的值
String value = xmlMsg.substring(start,end).trim();
return value;
}catch(Exception e){
throw new Exception("解析"+flagName+"失败:"+xmlMsg);
}
}
/**
* 从输入流中读取一条xml消息,以</msg>结尾即是
* @return:从流中读取的一条xml消息
*/
private String readString() throws Exception{
String msg="";
int i = ins.read();//从输入流中读取对象
StringBuffer stb = new StringBuffer();//创建字符串缓冲区
boolean end = false;
while(!end){
char c = (char)i;
stb.append(c);
msg=stb.toString().trim(); //去除消息尾的空格
if(msg.endsWith("</m>")){
break;
}
i=ins.read();//继续读字节
}
msg = new String(msg.getBytes("ISO-8859-1"),"GBK").trim();
return msg;
}
}
然后在建立一个服务器,等待客户端来连接,这个类已写过好多次这里就不再重复了~如此一来一个基于XMPP风格的服务器就建成了!
3.客户端收发消息的ClientConn类:这个类负责验证登录注册信息,同时发送XML风格的消息给服务器,并解析服务器发来的XML串!其实也就是在原来的基础上把读取一行和一个换行符改为了读取一条XML串并解析它而已。这个类要实现多台客户机同时交互,所以要继承线程的方法!具体代码如下:
/**
* xmpp客户机通信模块:接收消息,提供发送接口
* @author 蓝杰 www.NetJava.cn
*/
public class ClientConn extends Thread{
private javax.swing.JTextArea jta_recive; //从界面传来显示接受消息的组件
private JComboBox jcb_users; //界面上JCombox用到的用户名列表
private OutputStream ous;
private InputStream ins;
private boolean connOK=false;
/**
* 给接收对象传入一个TextArea对象,以在界面上显示消息
* @param jta_recive:界面上显示消息组件
* @param jcb_users:界面上的用户名列表
*/
public void setDisplay(JTextArea jta_recive,JComboBox jcb_users){
this.jta_recive=jta_recive;
this.jcb_users=jcb_users;
}
/**
* 与指定IP指定端口的服务器建立连结
* @param serverIP:服务器的IP地址
* @param port:服务器的端口号
* @return:是否连结成功
*/
public boolean conn2Server(String serverIP,int port){
try{
Socket sc = new Socket(serverIP,port);//连接上服务器
ins=sc.getInputStream();
ous=sc.getOutputStream();
return connOK=true;
}catch(Exception e){return connOK=false;}
}
/**
* 客户机发送登陆请求消息的调用
* @param name:用户名
* @param pwd:密码
* @return:是否登陆成功
* @throws Exception
*/
public boolean login(String name,String pwd){
try{
String login="<m><type>login</type><name>"+name+"</name><pwd>"+pwd+"</pwd></m>";//1.拼接登陆消息xml串
ous.write(login.getBytes());//发送登陆请求xml消息
String xml = readString();//读取登录应答
String state = getXMLValue("state",xml);
return state.equals("0");
}catch(Exception e){
return false;
}
}
/**
* 客户机发送注册请求消息的调用
* @param name:用户名
* @param pwd:密码
* @return:是否注册成功
* @throws Exception
*/
public boolean reg(String name,String pwd){
try{
String reg="<m><type>reg</type><name>"+name+"</name><pwd>"+pwd+"</pwd></m>";
ous.write(reg.getBytes());//发送登陆请求xml消息
String xml = readString();//读取登录应答
String state = getXMLValue("state",xml);
return state.equals("0");
}catch(Exception e){return false;}
}
/**
* 向服务器发送对某一用户的文本聊天消息
* @param sender
* @param reciver
* @param msg
*/
public void sendTextChat(String sender,String reciver,String msg){
try{
String textChatXML = "<m><type>chat</type><sender>" +sender+
"</sender><reciever>" + reciver+
"</reciever><msg>" +msg+
"</msg></m>";
ous.write(textChatXML.getBytes());
}catch(Exception e){e.printStackTrace();}
}
//线程运行后,连接服务器,接收服务器发来的消息
public void run(){
try{
while(connOK){
String xmlMsg = readString(); //接收一条消息
String type = getXMLValue("type",xmlMsg);
//登录成功后,接收到的消息类型可能有 聊天。用户表,上线,下线
if(type.equals("chat")){//如果服务器发来的是聊天消息
String sender = getXMLValue("sender",xmlMsg); //取得发送者名字
String msg = getXMLValue("msg",xmlMsg); //取得消息内容
jta_recive.append(sender+"说"+msg+"\r\n"); //显示到界面
}
if(type.equals("budyList")){//服务器发来的在线用户表信息
//取得在线用户表,多个在线用户名,用,分开
String users=getXMLValue("user",xmlMsg);
//解析以,分割的在线用户名,加入到界面列表中
java.util.StringTokenizer stk = new StringTokenizer(users,",");
while(stk.hasMoreTokens()){
String uName=stk.nextToken();
jcb_users.addItem(uName+" ");//加到界面的用户列表中去
}
}
if(type.equals("onLine")){//服务器发来的用户上线消息
String uName=getXMLValue("user",xmlMsg);
jta_recive.append(uName+"上线啦\r\n");
jcb_users.addItem(uName);//加到界面的用户列表中
}
if(type.equals("offLine")){
String uName = getXMLValue("user",xmlMsg);
jta_recive.append(uName+"下线啦!\r\n");
int count = jcb_users.getItemCount();
for(int i=0;i<count;i++){
String it = (String)jcb_users.getItemAt(i);
it=it.trim();
if(it.equals(uName)){
jcb_users.removeItemAt(i);
break;
}
}
}
}
}catch(Exception e){connOK=false;}
}
/**
* 从一条xmlMsg消息串中提取flagName标记的值,
* @param flagName:要提取的标记的名字
* @param xmlMsg:要解析的xml消息字符串
* @return:提取到flagName标记对应的值
* @throws:如果解析失败,则是xml消息格式不符协议规范,抛出异常
*/
private String getXMLValue(String flagName, String xmlMsg) throws Exception {
try{
//1.标记头出现的位置
int start = xmlMsg.indexOf("<"+flagName+">");
start+=flagName.length()+2;
//2.结束符出现的位置
int end = xmlMsg.indexOf("</"+flagName+">");
//3.截取标记所代表的消息的值
String value = xmlMsg.substring(start,end).trim();
return value;
}catch(Exception e){
throw new Exception("解析"+flagName+"失败:"+xmlMsg);
}
}
/**
* 从输入流中读取一条xml消息,以</msg>结尾即是
* @return:从流中读取的一条xml消息
*/
private String readString() throws Exception{
String msg="";
int i = ins.read();//从输入流中读取对象
StringBuffer stb = new StringBuffer();//创建字符串缓冲区
boolean end = false;
while(!end){
char c = (char)i;
stb.append(c);
msg=stb.toString().trim(); //去除消息尾的空格
if(msg.endsWith("</m>")){
break;
}
i=ins.read();//继续读字节
}
msg = new String(msg.getBytes("ISO-8859-1"),"GBK").trim();
return msg;
}
}
以上一个客户机的主要实现流程也就做好了!
XMPP协议风格通信的最大好处就是减少了很多死板的通信流程,不用严格按照一定的顺序收发消息,而可以按照消息的类型收发消息。这大大增加了程序的灵活性!同时一个XMPP协议的串中可以包含多层信息!也大大减少了代码量方便我们更好的扩展!不过上述代码在很多地方还有漏洞,比如当一个客户端下线时,程序会报错,执行了一个SOCKET的异常!这就需要我在以后的代码中继续改进才是!