Android 网络详解(一) TCP与UDP

来源:互联网 发布:lol全网络大区 编辑:程序博客网 时间:2024/06/10 23:09

Android中的网络连接比较丰富,所有网络连接都需要异步完成。


首先,介绍一下TCP连接。


TCP传输的数据格式如下图,

源端口和目的端口各占2字节,端口号加上IP地址,共同构成socket。互相通信的进程使用一对socket,包括协议、源IP、源端口、目的IP、目的端口,这五个元素唯一确定一个TCP连接。

顺序号为传输的字节流中第一个字节的序号,它与窗口大小,唯一确认了这个字节流。

数据即为所传输的数据。


在OSI模型中,TCP协议处在第四层,传输层,它把网络层IP协议传过来的无序数据包排好序,为应用层的FTP,HTTP等协议提供服务。


TCP协议属于面向连接的传输协议,它在传输之前先与对话方建立好连接,再传输字节流。这与它的兄弟UDP,截然不同。UDP属于不面向连接的传输协议,发送的数据格式为数据报。关于UDP,我们待会再议。

TCP建立连接需要进行三次握手,客户端向服务端发送一个序号为x的同步信号,服务端接收后回复一个序号为x+1的确认信号,同时发送一个序号为y的同步信号,客户端接收后,再回复一个序号为y+1的确认信号,经过三次你一来,我一往,连接便建成了,接下来就可以愉快的发送字节流了。

TCP断开连接需要进行四次挥手,客户端在数据发送完毕后,向服务端发送一个序号为m的结束信号,服务端接收后回复一个序号为m+1的确认信号,然后服务端继续将没发送的数据全部发出去,之后发送一个序号为n的结束信号,客户端接收后,回复一个序号为n+1的确认信号,这样,两人就可以毫无顾虑地,愉快地分手了。


对比两次互动,可以发现,挥手时多了一次互动,主要是因为服务端也有数据要发送了,有了一定主动权,不像握手时,服务端完全是被撩的。


TCP协议在传输数据的同时,还要进行拥塞控制和流量控制,确保数据传输的安全性。流量控制使用的是,滑动窗口协议,下面给一张图示,

图片来自百度。

从上图可以看到,TCP为传输的每个字节都编上序号,每次传输一定数量的字节,收到接收方反馈回来的确认信号后,再继续发送。

每次传输的字节数,便是滑动窗口的大小。在TCP的接收端与发送端,都会建立一个缓冲区,需要发送的数据在缓冲区内排队,每次发送完滑动窗口内的数据后,发送方会等待接收方反馈回来的信号。信号中的既包括接收方对接收到的数据的确认,也包括接收方对滑动窗口大小的调整要求。滑动窗口向前滑动,继续发送下面的数据,此时滑动窗口的大小取决于接收方与滑动窗口大小的调整要求,如下面这张图,就包含了窗口大小的调节,

图片来自百度。

起初滑动窗口大小为4096,发送方一次发送了大小为4096的数据,接收方发现滑动窗口太大了,要求把滑动窗口的大小调整为2048,下一次,发送方便一次性只发送大小为2048的数据了。

但遗憾的是,上面的图片相对理想化了些,每次发送的数据,都被完全接收到了,但事实上,发送的数据难免遇到丢失的情况,遇到这种情况该怎么办呢?来看下面这张图,

图片来自百度

可以看到,原来的滑动窗口为[32,51],窗口的大小为20,发送方发送了大小为20的数据后,如果接收方全部接收到了,便会发送一个序号为52的确认信号,由于中间数据的丢失,接收方只能收到序号[32,35]区间的连续数据,所以接收方发回了序号为36的确认信号。接收方就会把滑动窗口的左边界移到序号为36的数据处,滑动窗口的右边界自然由滑动窗口的大小来决定了。此时,[36,51]区间的数据便成为已发送却未接收到确认信号的数据了。那么,这段数据该怎么办呢?答案很简单,超时重传。超过一定时间后,若没有确认信号再发回,滑动窗口就会继续发送窗口内的数据,原来由于丢失而未被确认的发送过的信号会再被发送一次。滑动窗口左边界的意义,就是边界前的所有数据均被接收到了。这样,就可以保证所有数据都被有效地接收了。


所谓流量控制,就是不会让发送方一次性发送的数据太多,接收方接收不来,从而产生数据的丢失。在滑动窗口协议中,我们可以通过让接收方来控制滑动窗口的大小,达到了TCP协议的流量控制的目的。


但在网络传输中,不仅会产生流量问题,还会发生拥塞问题,网络路径的带宽有限,数据量不会造成流量问题,却有可能制造拥塞问题,这时,便有了拥塞控制。

在滑动窗口中,我们如何实现拥塞控制呢?

流量控制的掌控方在接收方,拥塞控制的掌控方,便在发送方。

发送方除了有滑动窗口,还设置了拥塞窗口门限。窗口的大小变化如下图


图片来自百度


起初,窗口的大小从1开始指数增长,当达到拥塞窗口门限,初始值为16,便开始线性增长,逐渐增大到24,网络开始出现拥塞,此时,窗口的大小回到1,拥塞窗口门限变为出现拥塞时的窗口大小的一半,然后重复上一轮的变化过程。三个阶段总结起来就是:指数增长,线性增加,乘法减小。

那么,如何判断是否出现网络拥塞了呢?只要发送了数据后,没有收到确认,就认为网络出现了拥塞,当然没有收到回复也有可能是其他原因,例如数据丢失,但此处统一按照网络拥塞来处理。

上面的拥塞判断方法显得有些过于笨拙了,有没有方法可以将数据丢失导致的未收到确认和网络拥塞导致的未收到确认分开来呢?看下图,

图片来自百度

上图说明的是快重传算法。当接收方收到失序报文后,立刻发送重传信号,发送端收到连续三个重传信号时,说明网络还很正常,重传信号还可以接收到,未收到确认信号的原因是由于数据丢失了,此时拥塞门限依然变为此时窗口大小的一般,但窗口的大小不再从1开始变化,而是直接从拥塞门限开始线性增加,这样,就将数据丢失与网络拥塞导致未收到确认信号的两种情况区分开来,算法的效率获得了明显的提升。这种算法成为TCP Reno版本。

通过拥塞控制的分析,窗口的大小由接收方的要求和拥塞的要求两方面联合控制,取两个值中较小的一个,便达到了流量控制与拥塞控制两个目的。

ok,TCP协议差不多介绍清楚了,下面讲一讲TCP在Android中的封装。

TCP协议通信封装在Android的socket类中,下面看代码,

        try {            ServerSocket server = new ServerSocket(PORT);            mExecutorService = Executors.newCachedThreadPool();  //create a thread pool            System.out.println("服务器已启动...");            Socket client = null;            while(true) {                client = server.accept();                //把客户端放入客户端集合中                mList.add(client);                mExecutorService.execute(new Service(client)); //start a new thread to handle the connection            }        }catch (Exception e) {            e.printStackTrace();        }

上面是服务端的代码。服务端使用ServerSocket(PORT)建立网络连接,然后使用ServerSocket的accept方法等待客户端的连接,客户端获取到服务端的IP地址和PORT端口号后,就可以与服务端建立socket连接了。
        try {            socket = new Socket(HOST, PORT);            in = new BufferedReader(new InputStreamReader(socket                    .getInputStream()));            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(                    socket.getOutputStream())), true);        } catch (IOException ex) {            ex.printStackTrace();            ShowDialog("login exception" + ex.getMessage());        }
上面是客户端的代码,客户端直接调用new Socket(HOST, PORT)方法,输入服务端的IP地址和端口号,便与服务端建立了socket连接。

socket连接建立完成之后,便可以愉快地使用流来传输数据了。在传输过程中,还要注意连接断开引发的IOExecption。

文章开头已经提到过了,作为TCP的兄弟,UDP是无连接传输,对于发送出去的数据报,沿着网络进行路由,很容易就会丢失,是不安全的连接,但这并不代表它没有用处。许多即时通讯使用的就是UDP协议,例如QQ。由于其不需要,发送的数据中不需要那么多的冗余信息,因而相同的带宽,UDP传输的数据量要比TCP多一些。

下面直接看UDP在服务器端与Android端的实现。

        try {            DatagramSocket socket = new DatagramSocket(100);            byte[] data = new byte[1024];            DatagramPacket packet = new DatagramPacket(data, data.length);            socket.receive(packet);        } catch (IOException e) {            e.printStackTrace();        }


服务器端使用DatagramSocket(PORT)方法输入端口号建立UDP连接,使用DatagramPacket方法构造数据报,然后调用DatagramSocket的receive方法来接收传来的数据报。注意,receive方法会产生阻塞。

try {            InetAddress address = InetAddress.getByName("localhost");            DatagramSocket socket = new DatagramSocket();            byte[] data = "需要发送的数据".getBytes();            DatagramPacket packet = new DatagramPacket(data, data.length, address, PORT);            socket.send(packet);        } catch (IOException e) {            e.printStackTrace();        }
Android端在构造DatagramPacket时直接在里面添加了接收端的InetAddress和PORT端口号,然后使用DatagramSocket的send方法发送数据报。同样的,客户端也可以接收数据报,服务端也可以发送数据报,只需要分别构造DatagramPacket,然后使用DatagramSocket的接收和发送方法即可。

好了,说到这里,TCP和UDP协议都已经说完了,下一篇,我们来说一说建立在TCP协议之上的,在应用层中的HTTP协议。

0 0