基于UDP编写协议实现可靠传输

面向无连接的UDP

UDP是一种面向无连接的传输协议,可以理解为对某个地址寄一封信,只要双方地址填对,信就可以成功送出去,但对方能不能收到这份信,那就不一定了,可能信在传输的过程中丢了,也有可能接收方没有去查收它。总之,UDP不可靠。那么什么是可靠的呢?TCP,TCP是一种面向连接的通信协议,它在建立连接前会通过三次握手来确认双方都可以正确接收信息,面向连接的通信方式可以理解为打电话的过程,只有双方都在线才能通话。那么既然TCP可以面向连接实现通信,为什么不直接使用TCP而放弃UDP呢?UDP自然有它的优点。UDP可以不用实时地建立连接,在没有数据的时候不会占用资源,而TCP需要保持一种稳定地连接状态,UDP明显比TCP更加灵活高效一些,但是它会丢包,为了解决丢包等问题,就需要人为编写协议来约束这个过程了。

我们的协议设计

为了更好地解决UDP的不可靠性带来的问题,又充分利用它的优点,实现一种可以被我们代码灵活控制的传输,我和小伙伴一起设计了一份协议。这份协议主要针对文件的传输,确保文件传输过程的可靠性,在之后可以继续扩展到传输别的内容上。
文件传输的三个过程
①三次握手(双方确认连接)
②传文件信息(文件的名称,字节数等信息)
③传文件内容(读取本地文件后得到的字节数组)
在这里插入图片描述
为了满足文件传输过程的标准化,我们设计了一种数据包的格式,它可以应用于三个传输过程,分别用不同的类型号来区别,同时它有可以灵活地用于其他类型数据的传输,具有可扩展性。
数据包的结构设计
在这里插入图片描述
数据包各部分的作用:
1、¥和@标识符:数据包的头部和尾部分别有一个“¥”和“@”符号,它可以用来表示数据包的开头和结尾。当传输的数据量比较大的时候,路由器会对数据包进行分解,使得其符合窗口的大小,最后在接收端得到的可能就是零零散散的数据包了这时标识符就起了作用,它可以判断这个包是否完整,因为包的总长是确定的1024,那么一个被拆分的数据包的前半段就可以通过长度去匹配丢失的下半段,接收方找到被拆开的包的两个部分后就可以把它们合并起来了。
2、类型号:因为我们的通信过程包括三个部分,虽然使用相同的数据包模板,但其中数据的存储形式与读取的需求是不同的,接收方需要根据不同的类型号来判断这个数据包是三次握手的数据包还是文件信息内容的数据包。类型号的设计思路使得在同一对端口传输不同类型数据包成为了可能。
3、包编号:一个文件往往需要拆解为大量的数据包来进行传输,包编号就是数据段的一个身份凭证,接收方可以根据编号来恢复文件内容,发送方也可以根据包编号来判断重传。包编号的我们用到了4个字节,是考虑到文件比较大的时候包的数量会很多,4个字节可以承载这些数量。
4、预留:预留部分是为了增加代码的扩展性而设计的,在这个过程中,我们发现需要为服务器和客户端建立一个文件的序列号,这个序列号是在服务器分配文件任务的时候创建的,在多个用户同时向服务器传输文件的时候,服务器可以通过文件序列号来区分,这个文件序列号就规定在了预留1,。后续的预留字节可以根据需要来安排。
数据包的类设计
数据包对象包含的信息有编号,被重传的次数,目标地址等信息(就像是一个快递包裹)。

/**
 * 数据包类
 * @author mayifan
 *
 */
class Packet{
   	
	public int number;//包的编号
	public int reSendTimes;//重传次数
	public byte[] data;//存放的数据
	public long lastTime;//最后一次发送的时间
	public String destIp;//目标IP
	public int destPort;//目标端口号		
	public Packet(int number,byte[] data,String destIp,int destPort){
   
        this.number=number;
        this.data=data;
        this.destIp=destIp;
        this.destPort=destPort;
        this.reSendTimes=0;
        this.lastTime=System.currentTimeMillis();
	}	
}

三次握手,建立可靠连接

三次握手图示
在这里插入图片描述
三次握手的数据包结构:数据包需要标明类型号0,表示是三次握手过程;预留1处明确文件在服务器端的序号(在第一次接受服务器反馈时获取,以后每次都要带上);内容部分的最后两个字节,分别存放seq和ack,seq是序列号,而ack表示应答号。
三次握手的目的:为了让服务器和客户端都知道对方可以接收到自己的信息,为了后续的传输提供安全可靠的前提。
三次握手的过程:客户端向服务器发送自己的序列号,假设是1,服务器接收到客户端是序列号后把客户端的序列号加一得到2作为应答号发送,同时发送自身的序列号5;客户端在收到后再把服务器的序列号5加1得到6作为应答号发还给服务器。这样双方就可以确保对方能够收到自己的消息,再加上重传机制就没问题了。
字节的赋值方式:这里字节的赋值思路和大家简单提一下,因为Java中的数是有符号的,因此类似buffer[1]=0的形式是把int数值转为字节中八位二进制对应的值,单字节的赋值范围是(-128~127),如果等号右侧的值在范围外,那么是会有错误提示的;如果写一个int变量,需要强转为byte类型。总之,需要考虑符号。代码中涉及到的不同位数的字节数组和int值相互转化的方法都写在Tools工具类中。
客户端生成三次握手数据包的代码
输入seq和ack的值就可以返回数据包的字节数组了,在这个方法里有字节数组的生成过程(为各个字节赋值)。

    /**
     * 生成包裹对应的字节数组
     * 输入序列号和应答号
     * @return
     */
	public byte[] getConnectString(int first,int second){
   		
        byte buffer[] =new byte[1024];//定义一个空的字节数组        	
    	buffer[0]='$';//文件头¥
    	buffer[1]=0;//类型0
        for(int i=2;i<6;i++){
      //包编号
        	buffer[i]=0;
        }
    	buffer[6]=(byte)fileNumber;//预留1
    	buffer[7]=0;//预留的剩余两个字节
    	buffer[8]=0;
    	for(int i=10;i<1021;i++){
   //内容1
    		buffer[i]=0;
    	}
    	buffer[1021]=(byte)first;//自身的序列号1,应答号部分为0;第一个是seq位,第二个位是ack位   
    	buffer[1022]=(byte)second;; 
    	buffer[1023]='@'; //结尾部分,一个字节  
    	return buffer;
	}

三次握手的方法
这是三次握手的方法,其中的sendMessage方法是发送数据包(字节数组)的方法,这里不展开。这里包括了发数据包,收数据包,读取信息,发回应包的过程。

    /**
     * 三次握手,客户端和服务器建立连接
     */
    public void threeHandShake(FileMessage fileMessage){
    
    	try{
   
	    	Log.v("MainActivity", "开始一次握手");
	    	//一次握手		    	
	        byte[] connect=getConnectString(fileMessage.getSerialNumber(),0);//获取第一次握手的字节数组
		    sendMethod.sendMessage(connect, fileMessage.getDestIp(), fileMessage.getDestPort());//发10,第一次握手
		    reSendThread.addPacket(new Packet(0, connect, fileMessage.getDestIp(), fileMessage.getDestPort()));//把它放到重发的线程中		    
		    Log.v("MainActivity", "开始
  • 12
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
UDP协议中,由于其不可靠性,可能存在数据包丢失或乱序等问题。因此,为了实现可靠的数据传输,需要在UDP的基础上进行一些改进。下面是一份基于UDP协议可靠数据传输的Java代码实现。 首先,定义一个Packet类,用于封装数据包: ```java public class Packet implements Serializable { private int seqNum; //序列号 private byte[] data; //数据 private long timeStamp; //时间戳 private boolean ack; //是否是ACK包 public Packet(int seqNum, byte[] data, long timeStamp, boolean ack) { this.seqNum = seqNum; this.data = data; this.timeStamp = timeStamp; this.ack = ack; } public int getSeqNum() { return seqNum; } public byte[] getData() { return data; } public long getTimeStamp() { return timeStamp; } public boolean isAck() { return ack; } } ``` 然后,定义一个UDPClient类,用于发送数据包和接收ACK包: ```java public class UDPClient { private DatagramSocket socket; private InetAddress address; private int port; public UDPClient(String ipAddress, int port) throws UnknownHostException, SocketException { this.socket = new DatagramSocket(); this.address = InetAddress.getByName(ipAddress); this.port = port; } public void send(byte[] data) throws IOException { int seqNum = 0; int windowSize = 4; long timeout = 1000; //超时时间为1秒 int base = 0; int nextSeqNum = 0; List<Packet> packets = new ArrayList<>(); while (seqNum < data.length) { byte[] buffer = new byte[Math.min(data.length - seqNum, 1024)]; System.arraycopy(data, seqNum, buffer, 0, buffer.length); Packet packet = new Packet(seqNum, buffer, System.currentTimeMillis(), false); packets.add(packet); seqNum += buffer.length; } while (base < packets.size()) { while (nextSeqNum < base + windowSize && nextSeqNum < packets.size()) { Packet packet = packets.get(nextSeqNum); sendPacket(packet, socket, address, port); nextSeqNum++; } boolean timeoutOccurred = false; byte[] receiveData = new byte[1024]; DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); try { socket.setSoTimeout((int) timeout); socket.receive(receivePacket); Packet ackPacket = (Packet) deserialize(receivePacket.getData()); if (ackPacket.isAck()) { base = Math.max(base, ackPacket.getSeqNum() + 1); } } catch (SocketTimeoutException e) { timeoutOccurred = true; } if (timeoutOccurred) { for (int i = base; i < nextSeqNum; i++) { Packet packet = packets.get(i); sendPacket(packet, socket, address, port); } } } } private void sendPacket(Packet packet, DatagramSocket socket, InetAddress address, int port) throws IOException { byte[] sendData = serialize(packet); DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port); socket.send(sendPacket); } private byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.flush(); return baos.toByteArray(); } private Object deserialize(byte[] data) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } } ``` 在send方法中,首先将数据分成若干个Packet对象。然后,采用滑动窗口的方式发送Packet对象,每发送一个Packet对象,就将nextSeqNum加1。接着,等待ACK包的到来。如果ACK包到来,则将base设置为ACK包的序列号加1。如果超时,则重传从base到nextSeqNum-1的Packet对象。直到所有的Packet对象都被发送并且ACK包都被接收到为止。 最后,可以编写一个测试用例: ```java public class TestUDPClient { public static void main(String[] args) throws IOException { String ipAddress = "127.0.0.1"; int port = 9999; UDPClient client = new UDPClient(ipAddress, port); byte[] data = "Hello, world!".getBytes(); client.send(data); } } ``` 这里,我们将数据设置为“Hello, world!”,然后调用client.send方法将数据发送出去。在实际应用中,可以在UDPClient类中添加回调接口来处理接收到的数据。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值