在实现的TCP服务器和客户端中,发送双方确定的应用层协议就是以换行符作为每条消息的结尾。也就是说在发送时按照换行符进行编码,接收时按换行符进行解码。在开发应用程序时,一个大的工作就是进行协议的确定。常见的应用层协议有HTTP,FTP…
主要是一种组织数据的格式。在XML文件中,每一个标签都是成对出现的,闭合标签带一个/。如果一个标签中含有子标签,那么这个标签就可以表示一个对象。如果一个标签包含多个相同的子标签,那么这个标签就表示集合。
缺点:结构复杂、不美观、冗余字符太多,在网络中传输比较耗费带宽。
1.用{}表示一个对象;
2.用[]表示一个集合;
3.属性用”key“:”value“。如果value是整型可以不加引号;
4.多个属性用逗号隔开,最后一个属性不加逗号。
JSON格式的优点是可读性好、美观、扩展性强,缺点是引入了额外的字符,占用带宽较大。
HTTP协议将在后面的学习中重点介绍。
核心的协议
UDP:无连接,不可靠传输, 面向数据报,全双工,大小受限。
TCP:有连接,可靠传输,面向字节流,全双工,大小不限。
1.无连接:UDP传输的过程类似于发短信。知道对端的IP和端口号就直接进行传输,不需要建立连接。
2.不可靠传输:没有任何安全机制,发送端发送数据报以后,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
3.面向数据报:应用层交给UDP的多长的报文,UDP原样发送,既不会拆分也不会合并。
用UDP传输100个字节的数据:如果发送端一次发送100个字节,那么接收端也必须一次接收100个字节;而不能循环接收10次,每次接收10个字节。
4.缓冲区
• UDP只有接收缓冲区,没有发送缓冲区;
• UDP没有真正意义上的 发送缓冲区。发的数据会直接交给内核由内核将数据传给网络层协议进行后续的传输动作;
• UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃。
• UDP的socket既能读,也能写,这个概念叫做全双工。
5.大小受限:
UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K (包含UDP首部)。
UDP是传输层协议,传输层协议是通过操作系统负责实现的,操作系统管理着进程,每个进程开放一个端口号。16位最大可以表示65535, 说明端口号的范围就是0~65535。16位UDP长度也就是65535个byte,约等于64KB。UDP里的校验和是一个CRC冗余校验。即就是通过对数据(byte数组)中的每个byte累加得到的值。在解析UDP报文时,先16位表示源端口,再截16位标识目的端口号…。最终截取到数据长度是由UDP的长度决定的。
byte累加示例:
public class Demo_CRC {
public static void main(String[] args) throws UnsupportedEncodingException {
// 定义两个字符串
// String str = "你好世界";
String str = "你好啊,一会去吃火锅吧!!!";
String abc = "how are you.";
// 转换成byte数组
byte[] bytes = str.getBytes("UTF-8");
System.out.println(Arrays.toString(bytes));
System.out.println(bytes.length);
// 循环累加每个byte的值,得到CRC结果
int crc = 0;
for (int i = 0; i < bytes.length; i++) {
crc += bytes[i];
}
System.out.println("str crc = " + crc);
// 转换成byte数组
bytes = abc.getBytes("UTF-8");
System.out.println(bytes.length);
System.out.println(Arrays.toString(bytes));
// 循环累加每个byte的值,得到CRC结果
crc = 0;
for (int i = 0; i < bytes.length; i++) {
crc += bytes[i];
}
System.out.println("acb crc = " + crc);
}
}
1有连接:TCP传输的过程类似于打电各方面
2可靠传输:通过TCP自身的多种机制来保证可靠传输,3- 12条
3.面向字节流:内容是以字节的方式进行发送与接收
4.缓冲区:TCP有接收缓冲区,也有发送缓冲区。全双工。
5.大小不受限。
16位源端口和目的端口和UDP中的一样们都是用来确定进程的。
4位首部长度:1111 = 15。首部总共可以有15*4byte=60个byte。选项之前一种有4 *5=20个字节,所以选项最多有40个字节。
数据就是应用层发来的载荷。
六位标志位:URG: 紧急指针是否有效。ACK: 确认号是否有效。PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。RST: 对方要求重新建立连接:我们把携带RST标识的称为复位报文段。SYN: 请求建立连接:我们把携带SYN标识的称为同步报文段。FIN: 通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段。
16位校验和:CRC校验。
选项就是自定义信息。
16位紧急指针:标识哪部分数据是紧急数据,暂时不关注。
序号、确认序号以及窗口大小会在后面介绍。
在与人聊天的过程中,一发一收的过程就是确认应答。由于网络的原因,可能出现收发信息乱序的问题。为了解决这个问题,TCP将每个字节的数据都编了号。即为序列号。
这个序号保存在上面提到的32位序号和32位确认序号中。对于发送和接收数据,TCP提供了SYN(发送)和ACK(应答)来标记。ACK携带了确认序号,就是要告诉发送者,我已经接收到哪了,下一次你要从哪开始发。当发送一个请求时,将SYN标志位置为1, 应答时将ACK标志位置为1。
消息在网络中传输的过程中,会经过操作系统、网卡、交换机、路由器以及其他网络设备。每个设备都有自己的负载能力,如果超出了范围,当前数据包就可能阻塞或被丢弃。
1.发送方丢包
等待了一会儿发现还没有收到ACK,那么就在规定事时间之后重新发送之前的数据。
2.响应超时
主机B接收到了数据,并发送了ACK应答,主机A只是没有接收到应答。这种情况会有一个重复接收问题。此时主机B在自己的缓冲区中通过32位确认序列号来过滤掉重复数据。并直接给出ACK的应答。
那么,如果超时的时间如何确定?
• 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
• 但是这个时间的长短,随着网络环境的不同,是有差异的。
• 如果超时时间设的太长,会影响整体的重传效率; 如果超时时间设的太短,有可能会频繁发送重复的包;
•TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。 Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待2500ms 后再进行重传。如果仍然得不到应答,等待 4500ms 进行重传。依次类推,以指数形式递增。
• 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
主机之间做为发送方和接收方在网络通信时,必须要确认双方收发数据的能力,其中涉及到建立连接与断开连接的协商过程。每天高铁的在开第一趟车之前,都会空跑一趟,对于网络通信来说,就是检查收发双方的能力。
通过两次SYN和ACK的过程就可以保证双方网络都没有问题。在这个基础上就可以进行正常的数据发送与接收。TCP本身对效率做了优化,将SYN+ACK合并为一次操作,这就是三次握手。
不能通过两次握手来确认双方的收发能力,因为没有完整验证。四次是可以的,将SYN+ACK拆开即可。
三次握手还有一个重要功能就是协商序列号从哪开始。
端口状态
通过netstat-an命令查看端口:
第一个ACK是操作系统实现的TCP协议的应答,第二个FIN是应用程序级别的。这两个操作之间是有时间差的,大概率不会合并在一起返回,所以描述为四次挥手。第二FIN丢包了如何处理?如果丢包会触发超时重传。
服务端:
[CLOSED -> LISTEN]
服务器端调用listen后进入LISTEN状态,等待客户端连接;
[LISTEN -> SYN_RCVD]
一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
[SYN_RCVD -> ESTABLISHED]
服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了。
[ESTABLISHED -> CLOSE_WAIT]
当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK]
进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)。如果系统中出现大量的CLOSE_WAIT状态,可能是程序没有调用close()方法。
[LAST_ACK -> CLOSED]
服务器收到了对FIN的ACK,彻底关闭连接。Close之后会等待系统回收资源。
客户端:
[CLOSED -> SYN_SENT]
客户端调用connect,发送同步报文段;
[SYN_SENT -> ESTABLISHED]
connect调用成功,则进入ESTABLISHED状态,开始读写数据;
[ESTABLISHED -> FIN_WAIT_1]
客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2]
客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段;[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK;
[TIME_WAIT -> CLOSED]
客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态。
数据通过一发一收的过程可以保证正常通信,但效率不高。既然这样一发一收的方式性能较低,那么我们一次发送多条数据,如图:
1.图示
• 滑动窗口本身是一种数据结构,用来维护窗口的大小以及已近发送和正在发送的数据。
• 白色框内的数据是等待确认ACK的数据段。
• 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。
• 发送前四个段的时候,不需要等待任何ACK,直接发送;
• 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
• 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
• 窗口越大,则网络的吞吐率就越高;
2.可以预见的丢包问题
ACK响应丢了:
即便是中间丢了某一个ACK,但是最后一个ACK应答中的确认序号表示在此之前的数据包都已经收到了。
SYN请求丢了:
在接收数据的过程中,如果发现32位序号缺失了一部分,那么就会一直发ACK来向发送方索要缺失部分的数据。这时接收到的其他数据会被缓存起来,等缺失的数据补充完整之后再拼装到数后面。
3.滑动窗口的效率
效率的高低取决于窗口的大小;
窗口越大效率越高;
窗口越小效率越低;
假设窗口无穷大,此时发送方就完全不需要等待ACK,此时效率就和UDP一样。
上面提到了滑动窗口的效率,那滑动窗口到底取多大合适?流量控制主要确认滑动窗口的大小,通过发送方与接收方动态协商来确认。比如做好饭了,问一下你能吃多少,我就给你盛多少饭。
1.发送与接收缓冲区
每个程序在启动时都会去申请系统资源,发送与接收缓冲区就是申请来的资源,也就是内存中的一片区域用来存放BYTE数据流。ACK把缓冲区中的剩余空间大小填充到窗口大小协议字段(16位窗口大小)。通过接收方来反制发送方对于窗口大小的限制,发送方不能为了提高效率而无限制的扩大窗口大小。已使用空间与剩余空间的大小是动态变化的,每次接收方从缓冲区中读到数据之后,剩余空间就会变大。
2.具体过程
• 发送方向接收方发送数据;
• 接收方在接收到数据后,会把数据存在接收方的缓冲区(内存中开辟的一个空间);
• 接收方的应用程序通过socketapi(lnputStream)从缓冲区里读取数据,读取一点缓冲区的数据就少一点,相当于取走数据接收方在做ACK应答时就会把这个缓冲区剩余空间的大小一并发给发送方;
• 剩余空间的大小就相当于可以继续处理数据的大小接收端将自己可以接收的缓冲区大小放入 TCP 首部中的“窗口大小字段,通过ACK通知发送端;
• 窗口大小字段越大,说明网络的吞吐量越高。接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
• 发送端接受到这个窗口之后,就会减慢自己的发送速度如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
如果接收方的处理能力较低,可能会出现缓冲区装满的情况。此时发送方会每隔一段时间发送一个窗口探测请求,没有真实数据,询问接收方还能接收多少。
3.实际窗口大小
TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息。16位数字最大表示65535,那么TCP窗口最大就是65535字节么?实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M位。
网络中数据传输过程是非常复杂的,其中可能会经过很多的交换机、路由器等网络设备。每一个网络设备出现问题都会对传输造成影响。
TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
此处引入一个概念称为拥塞窗口。
1.发送开始的时候,定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;
2.接下来每一次发送数据,窗口大小以指数扩大 2 4 8 16;
3.当达到初始阈值的时候,不再以指数扩大,而是线性的方式增长,每次加1;
4.当窗口到达某个值时,出现了大量丢包的现象,也就是说频繁的出现超时重传,就说明网络出现了堵塞;
5.拥塞窗口的大小直接回到最小值1,新的拥塞窗口阈值也会被调整为当前拥塞窗口的一半;
6.重复1-5步。
每次发送数据包的时候,将拥塞窗口和接收端缓冲区大小做比较,取较小的值作为实际发送的窗口。
少量的丢包,仅仅是触发超时重传;大量的丢包,就认为网络拥塞;当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
在收发过程中,接收方不停的在处理数据,接收方缓冲区的未使用部分在不断加大。通过延迟应答可以把最新的缓冲区未使用大小返回给发送方,从而提高窗口大小,提高网络收发效率。
1.间隔应答:间隔数一般取2。即就是不一定每次都应答,接收两次请求应答一次。比如2 4 6 8 应答。但如果整个发送过程只有3次就发完了,延迟应就没有办法返回ACK。
2.时间限制:超过最大延迟时间就应答一次。系统有一个默认值,一般取200ms,可以修改。
正常当接收方收到一个SYN请求时,系统内核会马上应答一个ACK。真正的响应是由应用程序做的,与ACK的时机不同,存在一定的时间差。由于延迟应答的存在,可能存在发送SYN报文和ACK报文同时发送的情况,那么系统就会把两个报文合二为一。这种机制称为稍带应答。
注意:虽然有稍带应答的机制存在,但并不是100%发生的,这个是由系统内核进行处理的。
由于接收方会将发送方发来的数据放在放在接收方缓冲区中,而接收方缓冲区是一个BYTE数组,不能有效区分消息的边界,这种现象称为粘包问题。
解决粘包问题:
1.在消息的末尾加上特殊的分隔符来标识消息的结束;在使用的时候按特殊字符截取缓冲区内容即可。
2.使用一个专门用来描述消息体长度的字段,来标识消息体的具体长度。
在读取消息之前,先把4byte的表示消息体长度的字段内容读出来,值为42;
继续在缓冲区里读取42个字节,这42个字节就表示消息的内容;
再读4byte表示下一个消息的长度,…,反复执行即可。
JSON用大括号来包裹消息,那么就可以理解为他是使用大括号作为特数字符来表示消息结尾的。
HTTP,应用层协议,即使用了分隔符也使用了表示消息长度的字段来解决粘包问题。
1.程序崩溃:操作系统会感知到,可以做相应的处理。操作系统会回收进程的资源,其中释放包括文件描述符,相当于调用了对应的socket的close,之后触发FIN操作,进而开始进入四次挥手,和普通的四次挥手没有区别。
2.正常关机:通过开始菜单或执行关机命令,系统会强制结束所有进程,回收资源,与程序崩溃执行的流程类似。
3.主机掉电:操作系统不会做出任何反应。
接收方掉电:发送方并不知道接收方挂了,继续发送数据发送数据后收不到ACK应答,触发超时重传多次重传都没有收到ACK应答,会尝试进行连接重置 (RST标识位)连接重置也失败,只能放弃连接。
发送方掉电:一般出现在长连接中,服务器与客户端会维护一个心跳包(客户端每隔1秒给服务器发送一个数据包,证明自己存活如果服务器一直收不到这个心跳包,比如过了10秒之后还没有收到,就判定为客户端挂了,自行断开连接客户端网络恢复之后再次进行重连即可。
4.网线断开:与主机掉电的情况相同,只不过是主机都是正常工作的。