java仿QQ通信-(服务器端)
我们都使用QQ,在QQ列表里面我们可以任意选择好友聊天,把聊天的好友看成一个对象,我们的聊天活动就像是在两个对象之间建立了一条管道,它们之间可以互相发送消息数据。
完成不停对象间的数据传输需要我们使用 java Socket :百度百科
我们的QQ平台就像一台大的服务器,它不但要负责将来自用户的信息发送给正确的用户对象,还要管理着所有用户的信息,响应用户的操作。
下面我们来实现一个简单的服务器:
一个服务器首先要管理好自己的信息:服务器的端口,服务器ServerSocket对象。
由于启动服务器后,accept()方法等待连接的过程会阻塞。修改后将其放在独立线程中运行。
我们封装它的启动,关闭,状态监测方法。
ChatServer类(extends Thread):
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
//服务器类(放在独立的线程中)
public class ChatServer extends Thread{
int port;//服务器端口
ServerSocket sc;//服务器对象
//构造函数
public ChatServer(int port){
this.port=port;
}
//创建服务器
public void SetUpServer() throws IOException{
sc= new ServerSocket(port);
//开启服务器线程
this.start();
}
//重写run函数,注意要把accepct方法放到run函数中调用。
public void run(){
try {
//不断等待用户的连接
while(true){
//获得连接的用户对象
Socket client= sc.accept();
//建立线程对象处理该用户,避免阻塞下一个用户的连接
ServerThread st= new ServerThread (client);
//启动用户对象线程
st.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
ServerThread类(extends Thread):
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ServerThread extends Thread{
Socket client;//用户对象
InputStream ins;//聊天的输入输出流
OutputStream ous;
DataInputStream din;//包装后的输入输出流
DataOutputStream dou;
//构造函数(获得连接对象和输入输出流)
public ServerThread (Socket client) throws IOException{
this.client= client;
ins=client.getInputStream();
ous=client.getOutputStream();
din= new DataInputStream(ins);
dou= new DataOutputStream(ous);
}
//重写run方法
public void run(){
//处理该用户
ProcessClient();
}
//处理该用户
public void ProcessClient(){
//注册,得到注册昵称密码,响应返回分配的账号
//登录,得到登录的用户名,密码,核对是否正确并响应是否成功
//登陆失败,结束函数;
//登陆成功,与用户通信BeginChat();
//退出,closeMe()
}
//与用户通信
public void BeginChat(){
}
//关闭该用户
public void closeMe() throws IOException{
client.close();
}
}
从上面我们可以看到还缺少了很多的功能,比如:
1.如何管理用户的信息
2.服务端和用户端的消息发送,接受方法是可以封装的
1.我们需要一个管理用户信息类DaoTools:
(1)保存所有已经注册的用户信息
(2)可以注册新的用户
(3)可以删除用户
(4)可以处理用户的登录
我们还可以封装用户为一个类UserInfo:
(1)用户名,用户昵称,用户密码,用户客户端地址
(2)方法:
构造方法:以用户客户端地址为参数;
其他:修改用户昵称,密码,返回用户名,密码,地址
……
那么我们在DaoTools里面可以保存有一个map(用户名String与UserInfo的键值对),存有所有的已注册用户UserInfo
DaoTools:
package javaQQServer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class DaoTools {
private static Map<Integer,UserInfo> userDB= new HashMap<Integer,UserInfo>();//已注册用户集合
private static ArrayList<ServerThread> stList= new ArrayList<ServerThread>();//在线用户集合
private static int userAssignedNum=0;//账号分配流水号
//保护构造函数,不允许建立该对象
private DaoTools(){}
//核对登录信息
public static boolean checkLogin(ServerThread st){
UserInfo user=st.getUser();
if(!userDB.containsKey(user.JKnum )){
System.out.println(user.JKnum+"账号不存在");
return false;
}
UserInfo user2=userDB.get(user.JKnum);
if(user2.getPWD().equals(user.getPWD())){
//添加到在线用户队列
stList.add(st);
//下发用户的好友列表(待完善)
//下发用户信息
return true;
}
System.out.println("密码:"+user.getPWD()+"错误,正确的密码是:"+user2.getPWD());
return false;
}
//注册新用户,返回分配的账号
public static int Register(UserInfo user){
userAssignedNum++;
user.setJKnum(2020+userAssignedNum);//分配账号
userDB.put(user.getJKnum(), user);
System.out.println("用户"+user.getJKnum()+"注册成功,昵称:"+user.nickName+"密码:"+user.pwd);
return user.getJKnum();
}
//注销用户
public static void removeUser(int JKnum){
userDB.remove(JKnum);
System.out.println("用户"+JKnum+"注销成功");
}
}
UserInfo:
public class UserInfo {
String name;
String nickName;
String address;
String pwd;
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public String getnickName(){
return nickName;
}
public void setnickName(String nickName){
this.nickName=nickName;
}
public void setAddress(String address){
this.address=address;
}
public String getAddress(){
return address;
}
public String getPWD(){
return pwd;
}
public void setPWD(String pwd)
{
this.pwd=pwd;
}
}
至此,我们已经完成了用户信息的保存和相应的管理操作
下面我们实现服务端对接收来自用户的信息的处理
用户发送来的信息分为很多类,我们将这些信息用不同的类型编号表示:
每一条待接收的消息都应该有一个相同组成的头部,供接收方识别它的身份:
我们还可以在消息头加上消息发送者的账号:
一共13字节
我们将其包装为MSHead类:
public class MSHead {
private int totallen;
private byte type;
private int dest;
private int src;
public MSHead(int len,byte type, int dest,int src){
this.totallen=len;
this.type=type;
this.dest=dest;
this.src=src;
}
public int getLen(){
return totallen;
}
public byte getType(){
return type;
}
public int getDest(){
return dest;
}
public int getSender(){
return src;
}
}
其他类型的消息均继承于该类
下面是一些消息的消息体
注册消息:0x01
//注册消息
public class RegMsg extends MSHead {
private String nickname;
private String pwd;
public RegMsg(int len, byte type, int dest, int src,String nickname, String pwd) {
super(len, type, dest, src);
this.nickname=nickname;
this.pwd=pwd;
// TODO Auto-generated constructor stub
}
public String getNickname(){
return nickname;
}
public String getPwd(){
return pwd;
}
}
注册应答:0x11
package MSGType;
public class RegResMsg extends MSHead {
private byte state;
public RegResMsg(int len, byte type, int dest, int src,byte state) {
super(len, type, dest, src);
this.state=state;
}
public byte getState(){
return state;
}
}
登录消息:0x02
public class LoginMsg extends MSHead{
private String JKnum;//登录账号
private String pwd;
public LoginMsg(int len, byte type, int dest, int src,String JKnum, String pwd) {
super(len, type, dest, src);
this.JKnum=JKnum;
this.pwd=pwd;
}
public String getJKnum(){
return JKnum;
}
public String getPwd(){
return pwd;
}
}
登陆应答:0x12
package MSGType;
public class LoginResMsg extends MSHead{
private byte state;
public LoginResMsg(int len, byte type, int dest, int src, byte state) {
super(len, type, dest, src);
this.state=state;
}
public byte getState(){
return state;
}
}
聊天消息:0x06
package MSGType;
public class TextMsg extends MSHead {
private String msgContent;
public TextMsg(int len, byte type, int dest, int src,String msgContent) {
super(len, type, dest, src);
this.msgContent=msgContent;
}
public String getmsgContent(){
return msgContent;
}
}
文件消息:0x07
package MSGType;
public class FileMsg extends MSHead{
private String FileName;
private byte[] FileData;
public FileMsg(int len, byte type, int dest, int src, String FileName,byte[] FileData) {
super(len, type, dest, src);
this.FileName=FileName;
this.FileData=FileData;
}
public String getFileName(){
return FileName;
}
public byte[] getFileData(){
return FileData;
}
}
我们还需要包装一些常用处理消息的方法:
如使用很多的发送一定字节长度的字符串的方法,消息的打包和解包方法:
把它们封装在一个独立的工具类里面
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class MSGTools {
//构造方法私有,不允许构建该类的对象
private MSGTools(){}
//消息解包,返回MSHead对象
public static MSHead parseMsg(byte[]data) throws IOException{
ByteArrayInputStream bin= new ByteArrayInputStream(data);//字节数组输入流
DataInputStream din = new DataInputStream(bin);//包装输入流
int len= 4+data.length;
byte type=din.readByte();
int dest= din.readInt();
int src= din.readInt();
if(type==0x01){//注册请求消息
byte[]data2=new byte[10];
din.readFully(data2);//昵称
String nickname=new String(data2).trim();
din.readFully(data2);//密码
String pwd=new String(data2).trim();
System.out.println("MSGTools正在解包注册请求消息消息包,昵称:"+nickname+",密码:"+pwd);
return new RegMsg(len,type,dest,src,nickname,pwd);
}
else if(type==0x02){//登录请求消息
int JKnum=din.readInt();
byte[]data2= new byte[10];
din.readFully(data2);//密码
String pwd= new String(data2).trim();
System.out.println("MSGTools正在解包登录请求消息消息包,账号:"+JKnum+",密码:"+pwd);
return new LoginMsg(len,type,dest,src,JKnum,pwd);
}
else if(type==0x11){//注册应答消息
byte state=din.readByte();
System.out.println("MSGTools正在解包注册应答消息包,state:"+state+",JKnum:"+dest);
return new RegResMsg(len,type,dest,src,state);
}
else if(type==0x12){//登录应答消息
byte state= din.readByte();
System.out.println("MSGTools正在解包登录应答消息包,state:"+state);
return new LoginResMsg(len,type,dest,src,state);
}
else if(type==0x06){//普通文本消息
int l=len-13;
byte[]content=new byte[l];
din.readFully(content);
String scontent=new String(content);
System.out.println("MSGTools正在解包普通文本消息包msg:"+scontent);
return new TextMsg(len,type,dest,src,scontent);
}
else if(type==0x07){//文件消息
byte[]name= new byte[256];//文件名
byte[]FileData= new byte[len-13-256];//文件数据
din.readFully(name);
din.readFully(FileData);
String FileName= new String(name).trim();
System.out.println("MSGTools正在解包文件消息包,文件名:"+FileName+",文件长度:"+len);
return new FileMsg(len,type,dest,src,FileName,FileData);
}
else return null;//(待完善)
}
//消息打包,返回字节数组
public static byte[] packMsg(MSHead msg) throws IOException{
ByteArrayOutputStream bou= new ByteArrayOutputStream();//字节数组输出流
DataOutputStream dou= new DataOutputStream(bou);//包装输出流
writeHead(msg,dou);//写入消息头
byte type= msg.getType();
if(type==0x01){//注册请求消息
RegMsg rm= (RegMsg)msg;//强制转换
writeString(rm.getNickname(),10,dou);//昵称
writeString(rm.getPwd(),10,dou);//密码
System.out.println("MSGTools正在包装注册请求类型的消息包,昵称:"+rm.getNickname()+",密码:"+rm.getPwd());
return bou.toByteArray();
}
else if(type==0x02){//登录请求消息
LoginMsg lm=(LoginMsg)msg;
dou.writeInt(lm.getJKnum());//账号
writeString(lm.getPwd(),10,dou);//密码
System.out.println("MSGTools正在包装登录请求消息消息包,JKnum:"+lm.getJKnum()+",密码"+lm.getPwd());
return bou.toByteArray();
}
else if(type==0x11){//注册应答消息
RegResMsg rrm=(RegResMsg)msg;
dou.writeByte(rrm.getState());
System.out.println("MSGTools正在包装注册应答消息包,state:"+rrm.getState()+",JKnum:"+rrm.getDest());
return bou.toByteArray();
}
else if(type==0x12){//登录应答消
LoginResMsg lrm=(LoginResMsg)msg;
dou.writeByte(lrm.getState());
System.out.println("MSGTools正在包装登录应答消息包,state:"+lrm.getState());
return bou.toByteArray();
}
else if(type==0x06){//普通文本消息
TextMsg tm =(TextMsg)msg;
byte []data=tm.getmsgContent().getBytes();
dou.write(data);
dou.flush();
System.out.println("MSGTools正在包装普通文本消息包,msg:"+tm.getmsgContent());
return bou.toByteArray();
}
else if(type==0x07){//文件消息
FileMsg fm= (FileMsg)msg;
MSGTools.writeString(fm.getFileName(),256, dou);//文件名256字节
dou.write(fm.getFileData());
dou.flush();
System.out.println("MSGTools正在包装文件消息包,文件名:"+fm.getFileName()+",文件总长:"+fm.getLen());
return bou.toByteArray();
}
else return null;//(待完善)
}
//封装读消息头的方法
public static MSHead readMsgHead(DataInputStream din) throws IOException{
int len=din.readInt();//读总长
byte []data= new byte[len-4];//记住务必要-4!已经读取了一个整数了!
din.readFully(data);
MSHead msg= MSGTools.parseMsg(data);//消息解包
return msg;
}
//发送消息头
private static void writeHead(MSHead msg,DataOutputStream dou) throws IOException{
dou.writeInt(msg.getLen());//总长
dou.writeByte(msg.getType());//类型
dou.writeInt(msg.getDest());//目标
dou.writeInt(msg.getSender());//来源
}
//封装发送定长的字符串的方法
public static void writeString(String msg, int len, DataOutputStream dou) throws IOException{
byte []data= msg.getBytes();
dou.write(data);
while(len>data.length){
dou.write('\0');//这里千万不要用writeChar()方法,每调用一次会多输出一个空字符
len--;
}
}
}
至此我们可以进一步完善ServerThread内的processClient函数
//处理该用户
public void ProcessClient() throws IOException{
//读消息头
MSHead msg= MSGTools.readMsgHead(din);
if(msg.getType()==0x01){//注册消息
//注册,得到注册昵称密码,响应返回分配的账号
RegMsg rm=(RegMsg)msg;//强制转换
String nickname= rm.getNickname();//获得昵称
String pwd= rm.getPwd();//获得密码
//注册并获得JK号
UserInfo user= new UserInfo();
user.setnickName(nickname);
user.setPWD(pwd);
int JKnum=DaoTools.Register(user);
//发送给用户注册应答消息
byte type=0x11,state=0;
RegResMsg rrm= new RegResMsg(13+1,type,JKnum,0,state);
byte []data= MSGTools.packMsg(rrm);//打包消息
dou.write(data);
dou.flush();
}
else if(msg.getType()==0x02){//登陆消息
//登录,得到登录的用户名,密码,核对是否正确并响应是否成功
LoginMsg lm= (LoginMsg)msg;
int JKnum=lm.getJKnum();//账号
String pwd=lm.getPwd();//密码
UserInfo user= new UserInfo();
user.setJKnum(JKnum);
user.setPWD(pwd);
byte type=0x12,state;
if(!DaoTools.checkLogin(user)){
//登陆失败,结束函数;
state=1;
//发送登陆失败应答消息
LoginResMsg lrm= new LoginResMsg(13+1,type,0,0,state);
byte[]data=MSGTools.packMsg(lrm);
dou.write(data);
dou.flush();
return;
}
else{
state=0;
//登陆成功,发送登录成功消息,与用户通信
LoginResMsg lrm= new LoginResMsg(13+1,type,0,0,state);
byte[]data=MSGTools.packMsg(lrm);
dou.write(data);
dou.flush();
//开始通信
user= new UserInfo();
user.setJKnum(JKnum);
user.setPWD(pwd);
user.setAddress(client.getRemoteSocketAddress());
BeginChat();
}
}
下面完善服务器端的BeginChat()函数
//与用户通信
public void BeginChat() throws IOException{
while(true){
MSHead msg=MSGTools.readMsgHead(din);
if(msg.getType()==0x06){//聊天消息
TextMsg tm= (TextMsg)msg;
//将文本消息转发给指定用户
System.out.println("接收到来自账号"+tm.getSender()+"的消息:"+tm.getmsgContent());
}
else if(msg.getType()==0x07){//文件消息
FileMsg fm=(FileMsg)msg;
//将文件发送给指定客户
System.out.println("收到来自用户"+fm.getSender()+"的文件:"+fm.getFileName());
}
else{
//未知数据包
System.out.println("接收到未知数据包!");
}
}
}
从上面我们可以发现还需要实现服务器转发消息的功能:
①获得指定JK号的在线用户②将消息转发给指定用户
在DaoTools类里增加获得指定JK号的在线用户的方法:
//获得指定JK号的在线用户
public static ServerThread getOnlineUser(int JKnum){
UserInfo user;
for(int i=0;i<stList.size();i++){
user=stList.get(i).getUser();
if(user.getJKnum()==JKnum)
return stList.get(i);
}
return null;//该用户不在线或者不存在
}
在ServerThread里面增加发送打包好的数据给对应用户的方法,并修改BeginChat函数:
//发送打包好的数据给用户
public void sengMSGPack(byte []data) throws IOException{
dou.write(data);
dou.flush();
}
//与用户通信
public void BeginChat() throws IOException{
while(true){
MSHead msg=MSGTools.readMsgHead(din);
int dest=msg.getDest();//获得发送对象JKnum
ServerThread st=DaoTools.getOnlineUser(dest);//获得转发对象
if(st==null){
System.out.println("发送错误,该用户不存在或者 不在线");
//将系统报错消息回馈给该用户(待完善)
continue;
}
byte[]data=MSGTools.packMsg(msg);//打包消息
st.sengMSGPack(data);//将消息转发给指定用户
if(msg.getType()==0x06){//聊天消息
TextMsg tm= (TextMsg)msg;
System.out.println("接收到来自账号"+tm.getSender()+"的消息:"+tm.getmsgContent());
}
else if(msg.getType()==0x07){//文件消息
FileMsg fm=(FileMsg)msg;
//将文件发送给指定客户
System.out.println("收到来自用户"+fm.getSender()+"的文件:"+fm.getFileName());
}
else{
//未知数据包
System.out.println("接收到未知数据包!");
}
}
}
感谢各位的阅读,欢迎大佬们指出问题!
至此我们已经完成了从注册到登录,转发文本消息,转发文件消息的功能,在下一篇博客我们继续完善剩余的消息类型。😁
工程文件javaQServer