自己实现RMI(四)socket通信方案之UDP通信

        虽然说UDP通信不可靠且没有连接状态,但是高效的传输效率还是非常诱人的。鉴于本人的项目环境是千兆以太网,考虑采用UDP通信来传输对象的序列化后的状态。原因有:以太网内传输UDP包基本上不会丢包,在程序设计时候可以不考虑该因素。此外,连接状态以及包乱序的问题,可以很方便的解决。下面具体讲述鄙人的简单方案。

        基本原理是服务器端监听某端口,当有客户端发送请求,则新建线程为客户服务,线程中另外监听一个端口,客户端与线程以后的数据交换通过线程监听的端口完成。例如服务器监听9000端口,客户端想要连接服务器端,则先向9000端口发送一个消息给服务器,服务器收到消息后新建线程,线程监听另外一个端口9001,服务器将线程监听的端口号发送给客户端,客户端存储该端口号,以后的数据通信都通过9001端口号完成。在数据交换过程中,不管服务器端还是客户端,收到一个包,必须给对方一个ack包以确认收到包,用这种简单的机制保证数据包可靠达到且没有乱序。

        该方案中,客户端与服务器端的数据交换过程中,客户端实际上是与某线程进行数据交换,而线程与客户端是一一对应的,所以线程收发数据包不会受其他客户端的影响,这样,接收方数据包的去重复和乱序等问题可以大大的简化,再加上确认机制,不会出现发送方发送过快而出现接收方溢出问题。

        由于UDP协议中数据以数据包的形式传输,而数据包的大小有限制,一般在局域网可以设置为60k的数据包大小。但是考虑到更大数据的传输能力,需要数据包的切割和组装。下面介绍一个简单的UDP包管理器PackageManager。

自定义包格式

        为了方便调试以数据包验证,设计一下自定义数据包格式: uninqID | packageID | gMsgID | data

其中,uninqID 是数据包的唯一id,packageID 是数据包的包id,即该包在一次发送的消息中所有数据包中所处的位置id,0表示自己是最后一块数据包。gMsgID 是标识消息的id。data是具体的消息数据。整个自定义数据包的大小不能超过60k,自定义数据包通过UDP发送。

        假如有110k的数据需要发送,先将数据分割为两部分,第一块为 0 | 1 | 1 | (60k - 3*sizeof(int)),第二块为1 | 0 | 1 | 剩下部分。

        第一块的大小为60k,第二块中数据域为110k剩下的部分。第二块为所发送的数据的最后一块,所以包id为0,两块的消息id都为1。包管理器的实现如下:

class PackageManager
{
	DatagramSocket socket = null;
	ByteBuffer sendBuffer = ByteBuffer.allocate(config.bufferSize+100);
	DatagramPacket sendPacket = new DatagramPacket(sendBuffer.array(),sendBuffer.capacity());
	ByteBuffer recvBuffer = ByteBuffer.allocate(config.bufferSize+100);
	DatagramPacket recvPacket = new DatagramPacket(recvBuffer.array(),recvBuffer.capacity());
	SocketAddress destAddress=null;
	boolean isClient=false;
	final int HEAD_SIZE=Integer.SIZE/Byte.SIZE*3;//消息头
	boolean recvedInSended=false;//发送过程中接受到消息
	
	byte[] dataBuffer = new byte[config.maxStringLeng];//10M数据空间
	int uninqID=1;
	int gMsgID=0;
	
	public PackageManager(DatagramSocket s)
	{
		socket = s;
	}
	public void setSocket(DatagramSocket s)
	{
		socket = s;
	}
	public void setClient()//客户端的包管理器
	{
		isClient=true;
		setTimeout(true);
	}
	
	/**
	 * 设置超时
	 * @param flag
	 */
	public void setTimeout(boolean flag)
	{
		if(socket.isClosed())
			return;
		try {
			if(flag==true)
				socket.setSoTimeout(10000);
			else {
				socket.setSoTimeout(0);
			}
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}	

	boolean isEqual(SocketAddress a,SocketAddress b)
	{				
		if(((InetSocketAddress)a).equals((InetSocketAddress)b))				
			return true;
		return false;
	}
	public void sendByteArray(byte[] data,int len) throws Exception
	{
		int begin = 0;
		int flag = 0;
		int tempLen=0;	
		if(socket.isClosed())
			return;
		
		if(isClient)
			gMsgID++;
		//Log.log("send ID:"+gMsgID);
		while(begin < len)
		{		
			if(begin+config.bufferSize<len)
			{
				flag ++;
				tempLen = config.bufferSize;
			}
			else {
				flag = 0;//结束标志
				tempLen=len-begin;
			}
			sendBuffer.clear();
			sendBuffer.putInt(uninqID);//包唯一id
			sendBuffer.putInt(flag);//包id
			sendBuffer.putInt(gMsgID);//消息id
			sendBuffer.put(data,begin,tempLen);
			send();		
			
			while(true)
			{			
				setTimeout(true);
				recv();			

				if(!isEqual(sendPacket.getSocketAddress(),recvPacket.getSocketAddress()))
				{
					Log.log("bad recv socket address with in ack!");
				}				
				int id=recvBuffer.getInt();
				
				if(id > 0)//是正常的数据包
				{	
					sendBuffer.clear();					
					sendBuffer.putInt(-id);	//应答包					
					send();	
					Log.log("kill a  msg:id="+id);
					if(isClient == false)
					{
						recvedInSended = true;
						return;//迅速结束当前任务
					}					
				}
				else {
					id=-id;
					if(id==uninqID)
					{
						recvedInSended = false;
						break;
					}
					else {
						Log.log("got bad message id.id="+id+" sendedID="+uninqID);
						throw new Exception("bad udp id");
					}
					
				}
			}
			uninqID++;			
			
			begin +=tempLen;
		}
	}
	
	public synchronized void sendString(String s)
	{		
		if(socket.isClosed())
			return;
		//Log.log("send:"+s);
		try {
			sendByteArray(s.getBytes(),s.length());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	public synchronized String recvString()
	{
		if(socket.isClosed())
			return "";
		int len=0;
		try {
			len = recvFullData();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(len<=0)
			return null;
		String ret=new String(dataBuffer,0,len);
		//Log.log("recv:"+ret);
		return ret;
	}
	public byte[] getDataBuffer()
	{
		return dataBuffer;
	}
	
	public void setDestAddress(SocketAddress addr)
	{
		destAddress = addr;
	}
	
	void send()
	{		
		if(socket.isClosed())
			return;
		sendPacket.setData(sendBuffer.array(),0,sendBuffer.position());
		sendPacket.setLength(sendBuffer.position());		
		sendPacket.setSocketAddress(destAddress);
		try {
			if(socket.isClosed())
				return;
			socket.send(sendPacket);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}//回复应答包
	}
	void recv()
	{
		if(socket.isClosed())
			return;
		//接收确认包
		recvBuffer.clear();
		recvPacket.setLength(recvBuffer.capacity());		
		try {			
			socket.receive(recvPacket);
			SocketAddress currentAddress=recvPacket.getSocketAddress();
			if(destAddress == null)
			{
				destAddress = currentAddress;
			}
			else if(!isEqual(destAddress,currentAddress)){
				Log.log("recv add is diff from destAddress dest="+destAddress+" recvAddress="+currentAddress);
				destAddress = currentAddress;
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public int recvFullData() throws Exception
	{
		int flag=0;	
		int temp;
		int dataOffset=0;
		if(isClient==false)
		{
			setTimeout(false);
		}
		if(socket.isClosed())
			return 0;
		//Log.log("recv begin");
		while(true)
		{
			if(recvedInSended==false)
			{
				recv();			
				int pID=recvBuffer.getInt();
				if(pID < 0)
				{
					Log.log("got a ack!");
					continue;//ack包
				}
				
				sendBuffer.clear();
				sendBuffer.putInt(-pID);
				send();//发送ack包
			}
			
			temp = recvBuffer.getInt();		//flag标志
			int mID=recvBuffer.getInt();//消息标志
			//Log.log("recv ID:"+mID);
			if(isClient==false)//服务器端
			{
				if(gMsgID>1 && gMsgID >=mID && !Server.isMaster())
				{
					throw new Exception("got a same msg! msgID="+gMsgID+" currentID="+mID);					
				}
				gMsgID=mID;//保存消息id				
			}
			else if(mID!=gMsgID)
			{				
				throw new Exception("msgID="+gMsgID+",currentId="+mID);				
			}
			if(++flag != temp && temp>0)
			{								
				throw new Exception("get bad flag! flag="+flag+" temp="+temp);				
			}
			//Log.log("temp="+temp);
			int headLen = HEAD_SIZE;
			int dataLen = recvPacket.getLength()-headLen;
			System.arraycopy(recvBuffer.array(),headLen,dataBuffer,dataOffset,dataLen);
			dataOffset += dataLen;
			if(temp==0)
				break;			
		}
		//Log.log("recv end:"+dataOffset);
		return dataOffset;
	}
}
客户端请求连接过程

        客户端在跟服务器数据交互之前,有一个”连接“的过程。实现如下:

socket=new DatagramSocket();					
								
InetSocketAddress address=new InetSocketAddress(ip,port);	
socket.connect(address);
pManager = new PackageManager(socket);
pManager.setDestAddress(address);
if(socket.isConnected())
{				
	pManager.setClient();
	pManager.sendString("connect");					
	String portString = pManager.recvString();					
				
	SocketAddress address=new InetSocketAddress(ip,Integer.parseInt(portString));
					
	socket.disconnect();
	pManager.setDestAddress(address);
	Log.log("connect to server succes!");
	return;		
}
与服务器连接之后,就可以进行正常的数据交互了。

服务器端处理连接请求 

	DatagramSocket tempDatagramSocket=null;
	public void start()
	{
		while(runningFlag)
		{				
			String tString = pManager.recvString();		
			if(tString==null)
			{
				runningFlag=false;//接收失败
				continue;
			}
						
			tempDatagramSocket = getDatagramSocket(ss.getLocalPort());				
			
			Timer workThread=new Timer();//用定时器代替线程
			workThread.schedule(new TimerTask()
			//Thread taskThread = new Thread(new Runnable()
			{					
				@Override
				public void run() {	
					this.cancel();
					DatagramSocket subSocket = tempDatagramSocket;
					PackageManager manager = new PackageManager(subSocket);
					while(runningFlag)
					{
						String string =manager.recvString();											
						String retString = function.run(string);  //处理客户端请求
						if(retString != null)
						{
							manager.sendString(retString);	//发送回应消息				
						}
					}
				}
			},0,1);
			//taskThread.start();
			
			//回应新连接的端口号
			pManager.sendString(tempDatagramSocket.getLocalPort()+"");
		}
	}





        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值