简单聊天室——服务器
什么是简单聊天室?什么又是服务器?相对应的,什么又是客户端?
在刚接触通信时,这是我最想了解的三个问题。好吧,现在让我一一解释:
简单聊天室,我们可以将之看成QQ中的群聊天,实现群聊,单对单聊天,还有在线好友列表,新增了上下线通知;
服务器&客户端,服务器将之比作移动公司,而我们的手机就是客户端。我们打电话,就是向服务器申请通话请求,服务器在根据请求作出应答,即连接另一部手机,使之进行通话连接。
在计算机中,我们可以根据IP和端口创建服务器:
1. 获得IP
在命令提示符中,输入ipconfig,从中获得IPv4即可。下列都用本地IP localhost。
2. 创建端口
端口一般是从0~2^16之间,前1024个端口为常用端口,有特殊用途,比如80端口,故只用后面的一些端口。
下列中皆选取9999端口。
3. 创建服务器(等待客户端接入)
/**
* 创建一个服务器
*
* @param port:端口号
* @throws IOException
*/
public void setUpServer(int port) throws IOException {
// 创建一个服务器,port为端口号
server = new ServerSocket(port);
System.out.println("服务器创建成功!" + port);
runing = true;
while (runing) {
// 等待一个客户端
Socket client = server.accept();
// 创建一个客户端线程
ClientThread ct = new ClientThread(client);
ct.start();
}
}
蓝色部分便是创建一个服务器,只传入端口号即可。
红色部分<SPAN style="COLOR: #ff0000">,在计算机中,其实一个端口只能连接上一个进程,换句话讲,一个服务器只能连接一个客户端,这显然不是我们想要的,故将客户端设置为线程,用一个近似于死循环,等待一个个客户端的接入,将Socket对象传入到自定义的客户端线程类中即可操作该客户端,其中一个客户端对应一个客户端线程,当没有客户端接入时,服务器进入阻塞。
只要在主函数中调用setUpServer(port)方法,一个简单的服务器&客户端就完成了,只不过他还不能干任何事。
下面讲讲服务器&客户端都需要干些什么:
一. 服务器类:
1. ClientThread类 对应客户端的线程,每一个客户端接入就会创建一个该线程,而将线程放入队列中,就是一个用户线程队列,其中定义了方法(参数省略):
⑴. getOwerUser() 取得该线程的用户对象(UserInfor类的对象)
⑵. closeMe() 关闭该线程
⑶. setMessage() 发送给客户端(用到输出流out.write())
⑷. readString() 自定义的对客户端发来的信息的读取(用到输入流ins.read())
⑸. dealMsg() 处理从客户端发来的信息(有关对通信协议xmpp的处理)(完成了登陆应答,注册应答等等)
public void run() {
try {
ThreadMain();
} catch (IOException e) {
e.printStackTrace();
}
}
public void ThreadMain() throws IOException {
// 创建输入输出流
out = client.getOutputStream();
ins = client.getInputStream();
// 如果收到bye则断开客户端与服务器的链接
do {
// 一行一行的读取客户机发来的消息
readString(ins);
} while (!isClose);
}
该类中的主要方法,红色部分<SPAN style="COLOR: #ff0000">,是一个近似的死循环,他的作用是不停的从Socket对象client中读取,如果没有则阻塞,这与客户端连接服务器相似。
2. UserInfor类 用户对象的类,在简单聊天室中,只有用户名和密码两个属性,设置&获取 四个方法。
3. ServerTools类 服务器的工具类,定义了静态的方法,将每个ClientThread的对象放入到一个队列中,即有对应的方法addThread(),remove(),clear(),getNum()(队列中元素个数)等方法,其中将所有线程对象放在一个队列中,就可以根据需求对每一个线程进行操作,实现群发信息。
4. DaoTools类 不同于 ServerTools类 主要是储存用户的名单(大致一个小型的库),判断用户是否注册,是否已登陆,账号密码正确性,显示在线用户等。用到了哈希表key=用户名,value=密码。
5. ServerUI类&ServerListener类 界面的类和监听器的类。
二. 客户端类
客户端和服务器是不同的两个程序,即有两个主函数,而连接客户端与服务器的纽带就是IP和端口。
1. ClientUI类 客户端的界面类。
2. ClientTX类 客户端的通信类。
⑴. con2Server() 创建一个客户端连接上服务器。
/**
* 1.是否连接上服务器
*/
public boolean con2Server() {
try {
// 创建一个到服务器端的Socket对象
Socket client = new Socket(serverIp, port);
// 得到输入输出流对象
ins = client.getInputStream();
ous = client.getOutputStream();
System.out.println("服务器连接成功!\n");
return true;
} catch (Exception ef) {
ef.printStackTrace();
}
return false;
}
红色部分<SPAN style="COLOR: #ff0000">,在客户端中穿件Socket对象不同于在服务器中创建,这里需要传入IP地址和端口,注意serverIp在这里是一个String,要输入比如"192.168.1.150"或者"localhost"本地IP;
⑵. loginServer(String name,String pwd) 发送登陆的用户名和密码给服务器,服务器验证后传回消息。
⑶. chatOnline() 主要的聊天方法。
/**
* 线程中读取服务器发来的消息
*/
public void run() {
chatOnline();
}
/**
* 3.开始聊天 (1).获取信息,写在jta_main上
*/
public void chatOnline() {
String connent;
try {
do {
// 获得对方的话
connent = readString(ins);
jta_main.setText(jta_main.getText() + connent + "\n");
} while (!isClose);
} catch (IOException e) {
e.printStackTrace();
}
}
红色部分<SPAN style="COLOR: #ff0000">,不断读取流中的信息,然后反映到JTextArea的对象上。
⑷. readString() 自定义的对客户端发来的信息的读取(用到输入流ins.read())
⑸. dealMsg() 处理从服务器发来的信息(有关对通信协议xmpp的处理)(完成了登陆结果,注册结果等等)
三. xmpp
根据xmpp自定义了一套传输的协议。
1. 登陆请求 "<login><name>" + name + "</name><pwd>" + pwd + "</pwd></login><end>"
2. 登陆应答 "<loginResp>No Name</loginResp><end>"
3. 注册请求 "<reg><name>" + yhm + "</name><pwd>" + mm + "</pwd></reg><end>"
4. 注册应答 "<regResp>" + isRegister + "</regResp><end>"(isRegister是DaoTools中的一个静态方法
isRegister()的返回值,boolean)
5. 信息发送 "<msg><sender>" + uName + "</sender><reciver>" + " 大家 " + "</reciver><body>" +
content_next + "</body></msg><end>"
6. 下线请求 "<offline>" + uName + "</offline><end>"
7. readString(InputStream ins)
/**
* 自定义的读取字符串的方法 从输入流上读取字节,转为一个字符串,以<end>分割
*
* @param ins:输入流对象
* @return:读取到的字符串
* @throws IOException
*/
private void readString(InputStream ins) throws IOException {
StringBuffer sb = new StringBuffer();
// 将字节转化成一句话
while (!sb.toString().endsWith("<end>")) {
int in = ins.read();
if (in == 8)
// 退格键
sb.deleteCharAt(sb.length() - 1);
else
// 将字符转化为字符串
sb.append((char) in);
}
// 中文解析
input2 = new String(sb.toString().getBytes("ISO-8859-1"), "GBK").trim();
dealMsg(input2);
}
8. dealMsg(String msg)——服务器
/**
* 处理信息
*
* @param msg:传入的内容
* @throws IOException
*/
public void dealMsg(String msg) throws IOException {
// 登陆应答
if (msg.startsWith("<login>")) {
start = msg.indexOf("<name>") + 6;
end = msg.indexOf("</name>");
username = msg.substring(start, end);
start = msg.indexOf("<pwd>") + 5;
end = msg.indexOf("</pwd>");
userpwd = msg.substring(start, end);
user = new UserInfor(username, userpwd);
// 看用户是否已登录
boolean isLogin = DaoTools.isLogin(user);
// 调用数据库模块,验证用户是否存在
boolean loginState = DaoTools.checkLogin(user);
if (!loginState) {
// 不存在这个用户帐号则重新输入
setMessage("<loginResp>No Name</loginResp><end>");
} else if (isLogin) {
// 已登陆,则
setMessage("<loginResp>Logining</loginResp><end>");
} else {
setMessage("<loginResp>OK</loginResp><end>");
}
// 添加线程
if (!isLogin && loginState) {
// 将线程添加到队列中
ServerTools.addThread(this); // 认证成功:将这个对象加入服务器队列
ServerUI.frash();
ServerTools.castMsg(user.getName(), " 大家 ", "我悄悄地来了.");
}
}
// 消息内容
else if (msg.startsWith("<msg>")) {
// 发信人的名字
start = msg.indexOf("<sender>") + 8;
end = msg.indexOf("</sender>");
sender = msg.substring(start, end);
// 收件人
start = msg.indexOf("<reciver>") + 9;
end = msg.indexOf("</reciver>");
reciver = msg.substring(start, end);
// 内容
start = msg.indexOf("<body>") + 6;
end = msg.indexOf("</body>");
String output = msg.substring(start, end);
ServerTools.castMsg(sender, reciver, output);
}
// 注册应答
else if (msg.startsWith("<reg>")) {
start = msg.indexOf("<name>") + 6;
end = msg.indexOf("</name>");
username = msg.substring(start, end);
start = msg.indexOf("<pwd>") + 5;
end = msg.indexOf("</pwd>");
userpwd = msg.substring(start, end);
boolean isRegister = DaoTools.isRegister(username, userpwd);
setMessage("<regResp>" + isRegister + "</regResp><end>");
}
// 下线应答
else if (msg.startsWith("<offline>")) {
start = msg.indexOf("<offline>") + 9;
end = msg.indexOf("</offline>");
username = msg.substring(start, end);
// 清空队列
isClose = true;
ServerTools.castMsg(username, " 大家 ", "我悄悄地走了.");
client.close();
ServerTools.remove(this);
ServerUI.frash();
}
}
9. dealMsg(String msg)——客户端
/**
* 处理信息
*
* @param msg:传入的内容
*/
private String dealMsg(String msg) {
String output = null;
// 登陆结果
if (msg.startsWith("<loginResp>")) {
start = 11;
end = msg.indexOf("</loginResp>");
output = msg.substring(start, end);
}
// 消息内容
else if (msg.startsWith("<msg>")) {
// 发信人的名字
start = msg.indexOf("<sender>") + 8;
end = msg.indexOf("</sender>");
sender = msg.substring(start, end);
// 收件人的名字
start = msg.indexOf("<reciver>") + 9;
end = msg.indexOf("</reciver>");
reciver = msg.substring(start, end);
// 内容
start = msg.indexOf("<body>") + 6;
end = msg.indexOf("</body>");
output = sender + "对" + reciver + "说: " + msg.substring(start, end);
}
// 注册信息
else if (msg.startsWith("<regResp>")) {
start = msg.indexOf("<regResp>") + 9;
end = msg.indexOf("</regResp>");
content = msg.substring(start, end);
if (content.equals("true"))
javax.swing.JOptionPane.showMessageDialog(null, "注册成功!");
else
javax.swing.JOptionPane.showMessageDialog(null, "用户名已注册!");
}
return output;
}
四. 问题总结
1. 对于 客户端接入 与 注册登陆 先后顺序问题。在登陆界面中,我们需要将登陆信息发送的服务器,在注册界面中,我们也需要发送注册信息。注册 和 登陆 在操作时,不分先后,这就要求我们不能将 客户端的接入只写在登陆界面或注册界面,当然也不能两个都写,可以写在界面生成之前,如果服务器未开启,则不显示登陆界面。否则 会报空指针异常(Socket对象client为空,ins,out输入输出流对象也为空)。
2. 在 服务器界面 上显示在线用户,要用到JTable,JTable要时时更新。但是当客户端非正常关闭时,会导致内存泄露,则会出现用户下线,但服务器界面上用户还在线。解决这一问题的方法:可以用windowListener()中的windowClosing()方法重写关闭方法。
3. 关于 用户列表 的读取写入问题。在 注册时 我们需要将用户的信息写入到一个文件中,用户名可以是中文,在打开服务器时,要读取文件中的信息,这样用户在注册一次后就可以一直用这个账号登陆,而不用每次开启服务器就要注册一遍。
我在写入时用的是writeUTF()的方法,读取时用readUTF()的方法,如果用write(byte[] b)或者writeByte(byte[] b),read(byte[] b)或者readFully(byte[] b)等方法,会报空指针,原因不懂,希望有人能帮我解决一下。