java仿QQ通信项目实现一

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

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hnu哈哈

请接受直女的么么哒????

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值