赞
踩
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", "开始
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。