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

来源:互联网 发布:hifi耳机推荐 知乎 编辑:程序博客网 时间:2024/04/28 06:23

        虽然说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 blocke.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);//包唯一idsendBuffer.putInt(flag);//包idsendBuffer.putInt(gMsgID);//消息idsendBuffer.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 blocke.printStackTrace();}}public synchronized String recvString(){if(socket.isClosed())return "";int len=0;try {len = recvFullData();} catch (Exception e) {// TODO Auto-generated catch blocke.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 blocke.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 blocke.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(){@Overridepublic 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()+"");}}





        

原创粉丝点击