Android与物联网设备通信-UDP&TCP协议

有很多小伙伴私聊我说更新太慢,夺命吹吹吹。一周一次,还不能满足你们吗?

好,那我不写了。╭(╯^╰)╮!!!

额,尽量保持完成审核后就交货发布啦。实际上前面的章节都是周末完成等到周三才发的。

上一节我们细说了网络模型分层。知道了七层分法实际现在只有五层了。今天我们展开来学习传输层UDP和TCP的协议。因为这两个协议关联性比较大,且篇幅不高。故合并层一个小节学习。

章节

目录

  • UDP
  • TCP

UDP (User Datagram Protocol)

它是一种高速但数据不可靠的协议,为什么说它不可靠呢?是因为与之对应的TCP是非常可靠和稳定的。我们先看一下UDP的报文结构再讲解传递数据过程的原理。

UDP报文

image.png

可以看到很简单,没有什么东西需要特别说明的,根据前几节的知识就可以推断出来。UDP的效率高并非在报文上,而是它的传输机制就是只管往外发,对方能不能接收到(是否存在),并不关心。

你一定还记得网络不通的时候ping一下,这种小秀的骚操作的把?没有错它的底层就是依靠UDP协议直接实现的,ping的过程就是客户端组装UDP报文,和DNS服务器接收解析UDP并响应客户端的过程。当ping不通的时候就会出现超时,即DNS服务器不回复客户端。

所以UDP的传输可理解成来数据了,就不管三七二一就是一个走你,拜拜了您勒。也不去校验到底有没有收到。

正常情况下一条报文会被路由、交换机经过一层层转发,最后到该接收的位置。他们依靠上一节提到的数据链路层和网络层来寻找主机。UDP协议只关心端口。这种最典型的UDP称为单播UDP,除此之外还有组播广播。关于组播和广播,我们后续的章节讲到做查找设备时会详细讲解,并做一个小demo。

TCP(Transmission Control Protocol)

相对UDP来说TCP协议就要稳定很多了。同样我们先看报文结构,再学习传输原理。

TCP报文

image.png

我们重点看几个字段说明:

  • 序号: 表示发送端当前包位于整组数据的第几个字节,也叫流水号。用于确保数据组的稳定性。

  • ack号: 也叫确认号,表示接收方收到了多少个字节,表示期待下一组数据的字节序号。

  • 数据偏移: 一般不用,但是出现在TCP头里如果可选字段增加后,就要在数据偏移中指明偏移的位置,最多可以将20字节的头扩展到60个字节。

  • 控制位: 6个标志位URG ACK PSH RST SYN FIN每一个表示一个控制功能,也就是告诉对方当前的包是做什么用的标识。

  • 窗口: 接收端告诉发送端自身的缓存大小的。避免过大的数据包导致接收端接受不过来而丢失数据。

控制位详解

  • URG: 紧急指针标志是否有效。

  • ACK: 确认序号是否有效。

  • PSH: 刷新缓存标志1有效,0忽略,要求把数据尽快给应用,而不要放在缓存里。(java里的flush方法)

  • RST: 异常标志,表示强制断开连接,在异常的情况下。

  • SYN: 连接过程同步序号标志。

  • FIN: finish标志,用于释放连接1表示关闭连接。

咋一看你会发现TCP的报文比UDP复杂了好多,一大堆东西。我的老伙计,别担心。那么底层的事情又不要你处理。你只需要知道它们在干嘛。并且值得说的是在Java上做socket编程时,你根本就感知不到底层字段的状态。到上层在处理数据的时候仅仅只有一个输入流和输出流了。这样极大的方便了应用层的简便开发。

TCP协议分析

我们知道TCP是面向连接的,它严格的把控住了每一次传输的数据的稳定性。那么它是如何做到的呢?干巴巴的说太过于抽象,现在我们实际动手写一下代码(kotlin)搭配利刃wireshark分析一下。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(args: Array<String>) {
val ss= ServerSocket(22222);
while (true){
val accept = ss.accept()
val dataInputStream = DataInputStream(accept.getInputStream())
val dataOutputStream = DataOutputStream(accept.getOutputStream())
val s = dataInputStream.readUTF()
dataOutputStream.writeUTF("hello Server")
dataInputStream.close()
dataOutputStream.close()
accept.close()
println(s)
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val socket = Socket("192.168.0.5", 22222);
val dataOutputStream = DataOutputStream(socket.getOutputStream())
val dataInputStream = DataInputStream(socket.getInputStream())
dataOutputStream.writeUTF("hello")
val s = dataInputStream.readUTF()
dataInputStream.close()
dataOutputStream.close()
socket.close()
println(s)
}

值得注意的是wireshark无法直接抓到TCP在本地的回路包,但是我们可以使用命令行配置一下就可以了,不然抓不到本机的回路包。

  • 给路由表添加一条
    route add 本机IP mask 255.255.255.255 路由IP metric 1

  • 抓完删除route delete 本机IP

为了方便演示,我这里使用了两块网卡。所以往路由里添加了两次回路IP。

这里首先我们开启服务端,进入了循环监听状态,随后开启了客户端连接上后发送一个hello,服务端收到后回复了一个hello Server。再没有做更多的事情了。我们来看一下通过wireshark抓到的包。

image.png

  • 客户端: 192.168.0.4端口:55705(操作系统协议栈分配)
  • 服务端: 192.168.0.5 端口:22222(我们定义的)

1.启动服务端后,操作系统将开始对22222端口的SYN包进行监听。

2.客户端启动,并尝试连接服务端,由操作系统协议栈随机分配一个发送端口。

握手:

3.客户端组装一组SYN报文发送至服务端22222号端,标记客户端seq0(完成第一次握手)

4.服务端收到SYN包,并组装一组SYN、ACK报文,内容是告诉客户端我确认了你的SYN包,并期待你的下一组数据从1开始,标记服务端seq0(完成第二次握手)

5.客户端收到服务端返回的SYN、ACK包报文,,标记客户端seq1,并组装一组ACK报文,内容是告诉服务端期望收到的下一组ack1开始。(完成第三次握手)

发包:

6.客户端组装了一组PSH、ACK包,并带上ack=1,内容为hello,发送个服务端。

7.服务端收到了内容为hello的包,确认后将ack加上收到的内容的长度。组装一组PSH、ACK并带上ack=8发送给客户端。

挥手:

8.客户端收到服务端的消息,更新自己的seq=8,并组装一组FIN、ACK包,并带上ack=15确认号,尝试请求断开连接。(客户端第一次主动挥手)

9.服务端收到客户端的FIN包,更新自己的seq=15,并组装一组FIN、ACK包,并带上ack=8发送给客户端。(服务端确认,开始第二次挥手。)

10.客户端收到服务端的ack包并检测seq是否一致,组装一组ACK包发送给服务端。(第三次挥手)

11.服务端收到客户端的ack并检测seq是否一致,组装一组ACK包发送给客户端。(第四次挥手)

上面描述的这个过程从字面上去看并没有太大意义,我希望你真正打开wireshark和代码,调试着玩玩。

回顾过程:

握手: 客户端和服务端会先进行三次握手,握手的时候会告诉对方自身的窗体大小和ack确认号。这个过包的功能是从ACK、SYN来达到认识的。

传输数据: 在接受对方的数据时,在下一组包里带上ACK标记告知对方我已经收到了多少内容,期待下一组内容的起始位置。

挥手: 相互确认包是否发送完整,再双方确认了ACK后。才真正挥手完毕。

丢包或包校验不对情况: 操作系统协议栈会根据ACKSEQ号对控制位为ACK的包进行校验,如果对不上则要求重发上一组包。


到此UDP&TCP的协议我们就学习完了。这里没有去演示UDP包的抓取过程,因为如果TCP都会了UDP就不是什么难题。再一个没有展示wireshark的具体使用过程,因为网上实在有很多优秀的教程了。

即便如此,我敢打赌的是如果读者是第一次接触TCP/IP绝对会因为很多概念和发来发去的字段搞得头晕或者一知半解。因为我的能力还不足以让你看看文章就完全懂了。(原话出自凯哥 ^o^

听我一句劝,这个时候不要放弃,成热打铁赶快打开分析工具,照着我前面的代码跑起来。再拿出纸笔写写画一画。深挖一下真正的去感受它们的巧妙之处。你会感觉这个设计是非常棒的。

话说大家看得真的有收获吗?如果觉得不错,又想奖励我一下戳这里打赏(听说打赏有吹更效果噢)。

随缘打赏!