虽然说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()+"");
}
}