Android 网络交互(三)之TCP实现和原理分析

也是好久没有写文章了,因为最近有点浮躁。今天终于想来写点东西了,今天就准备写,在网络交互下,和我们平常做应用比较息息相关的协议,我们一起来分析下探讨下原理和实现。在前面的文章,我们分析过TCP/IP协议簇模型,我们就知道UDP和TCP,是属于传输层,而HTTP是属于应用层。在TCP和UDP向应用层传输数据的过程中,Socket也是起着关键性的作用。通过下面的图,我们可以更清晰地看到他们之间的关系:


首先,我们先浅析下TCP:

TCP:Transmission Control Protocol 传输控制协议。TCP是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport layer)通信协议。

面向连接:通信前必须建立连接。

可靠地:TCP为了保证不发生丢包,就给每个字节一个序号,同时序号也保证了传送到接收端实体的包的按序接收。

TCP是全双工的,双方可以互发信息。

我们来看下TCP的首部:


源、目的端口号:占16比特。TCP协议通过使用"端口"来标识源端和目标端的应用进程。端口号可以使用0到65535之间的任何数字。

序列号:占32bit。用来标识从TCP源端向TCP目标端发送的数据字节流,它表示在这个报文段中的第一个数据字节。

确认号:占32比特。这个值表示一个准备接收的包的序列码,只有ACK标志为1时,确认号字段才有效。它包含目标端所期望收到源端的下一个数据字节。

头部:指出TCP负载(数据)的开始位置。没有任何选项字段的TCP头部长度为20字节;最多可以有60字节的TCP头部。

保留:占6bit,且都必须为0;

标志位:占6比特。包含:

URG(Urgent data):紧急指针(urgent pointer)有效。如果URG为1,表示携带紧急数据的包。

ACK (Acknowledgment field significant):确认序号有效。如果为1,则表示需要回应。

PSH(Push function):接收方应该尽快将这个报文段交给应用层。如果PSH为1,此封包所携带的数据会直接上传给上层应用程序而无需经过TCP处理。

RST(Reset):重建连接。如为1,表示要求重传。

SYN(Synchronize sequence number):建立连接。如为1,表示要求双方进行同步沟通。

FIN(Finish-No more data for sender):关闭连接。如果FIN为1,表示传送结束。

窗口大小段:占16比特。此字段用来进行流量控制。单位为字节数,这个值是本机期望一次接收的字节数,是可变的,会根据端的处理能力变。

校验和:占16位。对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证。

紧急指针:占16位。它是一个偏移量,指向后面是优先数据的字节,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,和序号字段中的值相加表示紧急数据最后一个字节的序号。在上面标志位介绍时,出现1的情况,这里就会指出紧急数据所在位置。

选项:占32位比特。可能包括"窗口扩大因子"、"时间戳"等选项。

TCP协议的三次握手:

TCP面向建立连接的时候,最鲜明的特点就是三次握手:


第一次握手:客户端发送一个SYN包(TCP数据段,其中同步标志位SYN=1,指明客户打算连接的服务器的端口,以及初始序号m,保存在包头的序列号(Sequence Number)字段里)。服务端由SYN=1知道,客户端要求建立连接,此时客户端进入SYN_SEND状态,等待服务器确认。

第二次握手:服务端收到SYN包后,返回确认包(ACK)应答。此段中SYN标志位被置为1,ACK标志位也被置为1,同时确认序号为m+1,随机生成的发送序号n,此时服务器进入SYN_RECV状态.

第三次握手:客户端收到后检查确认号是否正确,即第一次发送的序列号+1,以及ACK标志位是否为1,若正确,客户端会再发送确认号n+1,标志位ACK=1,主机B收到后校对确认值与ACK=1则连接建立成功。

完成三次握手,连接建立成功。

TCP四次挥手:


第一次挥手:某个应用进程首先调用close,我们称这一端执行主动关闭。客户端发送一个FIN分节(FIN包),表示数据发送完毕,客户端进入FIN_WAIT_1状态。

第二次挥手:接收到FIN的另一端执行被动关闭(passive close)。服务端收到FIN后,回送给客户端一个ACK,服务端进入CLOSE_WAIT状态。

第三次挥手:服务端发送一个FIN,用来关闭传输,服务端进入LAST_ACK状态。

第四次挥手:客户端收到FIN,进入TIME_WAIT状态,接着发送一个ACK给服务端,服务端进入CLOSED状态,完成四次挥手。

那么问题来了,你懂得,可是我们不去找蓝翔,自己来分析下,有人会问建立连接的时候是三次握手,可是关闭连接的时候却是四次挥手?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

还有几个常见的问题总结下:

1.为什么TIME_WAIT状态还需要等2*MSL秒之后才能返回CLOSED状态?

在不考虑网络实际情况下,可以直接CLOSED,这是理想状态,但是实际网络,有可能出现网络阻塞等常见问题,你无法保证你最后发送的ACK,对方可以收到,也就是说被动方在LAST_ACK状态下的Socket可能因为超时未收到ACK报文,而重发FIN报文。TIME_WIAT的设计就是为了重发可能丢失的ACK报文。

2.关闭TCP连接一定要四次挥手?

不一定,四次挥手是关闭TCP最安全的方法,但是当MSL数值过大,会导致有很多TIME_WAIT的TCP连接。这时我们可以设置SOCKET变量的SO_LINGER标志来避免SOCKET在CLOSE()之后,进入TIME_WAIT状态,这时会通过发送RST强制终止TCP连接。(TIME_WAIT往往对于我们是有利的)

多讲一下CLOSED_WAIT。被动方在收到主动方发送的FIN包,但未发出FIN包回给主动方,被动方进入CLOSED_WAIT状态,这样的原因可能是被动方在关闭之前还有很多数据没有处理完。如果服务端出现大量的CLOSED_WAIT状态的TCP,就会因为消耗太多资源,系统会不堪重负,崩溃。服务端需要检查代码,可以通过判断接收函数返回值是否错误,强行关闭Socket连接,或者通过修改一下TCP/IP的参数,来缩短这个时间:修改tcp_keepalive_*系列参数有助于解决这个问题。

下面我们看一下简单的TCP实现C/S端代码:

客户端代码:

try { 

//创建Socket对象,并绑定服务端的IP地址和端口号
            Socket s = new Socket(Serverhost, 8080); 

//   读取本地文件

   InputStream is = new FileInputStream("/sdcard/android/mm.jpg");
           OutputStream out = is.getOutputStream(); 
           byte[] buffer = new byte[4*1024];
           int temp = 0;
           //将inputstream数据读出,写入outputstream中

   while((temp = is.read(buffer))!=-1){

        out.write(buffer,0,temp);

   }

   out.flush();//向服务端发送数据
           s.close(); 
        } catch (UnknownHostException e) { 
            e.printStackTrace(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        }

TCP服务端代码:

try { 

//创建一个ServerSocket,并让其监听8080端口
          ServerSocket svs = new ServerSocket(8080); 

//调用ServerSocket的accept()方法,接受客户端所发送的请求
          Socket socket = svs.accept();

//从sokcet当中得到InputStream

  InputStream is = socket.getInputStream();

  byte[] buffer = new byte[4*1024];

  int temp = 0;

          //从InputStream中读取客户端发送的数据

          while((temp = is.read(buffer))!=-1){

System.out.print("hello");

}

            svs.close(); 
        } catch (UnknownHostException e) { 
            e.printStackTrace(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        }

本来这篇想写个TCP,UDP,HTTP合集的,不过篇幅有点长,UDP还是在下节再讲吧。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值