TCP协议是一种作用在传输层的面向连接的可靠的数据传输控制协议;
UDP协议是一种如用在传输层的无连接的不可靠的数据传输控制协议;
TCP协议报文由源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位
SYN表示建立连接,FIN表示关闭连接;ACK表示响应;PSH表示有 DATA数据传输;RST表示连接重置;URG表示紧急指针字段有效。
SYN、FIN、ACK位:其中ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应。如果只是单个的一个SYN,它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。
RST位:RST一般是在FIN之后才会出现为1的情况,表示的是连接重置。一般地,当出现FIN包或RST包时,我们便认为客户端与服务器端断开了连接;而当出现SYN和SYN+ACK包时,我们认为客户端与服务器建立了一个连接。
PSH位(Push):PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。TCP的连接建立和连接关闭,都是通过请求-响应的模式完成的。当两个应用进程进行交互式的通信中,有时在一端的应用程序希望在键入一个命令后立即收到对应的响应。在这种情况下,TCP就可以使用推送操作。通常的数据中都会带有PSH,但URG只在紧急数据才设置,也称“带外数据”。
URG位(URGent):当URG=1时,表示紧急指针字段有效。他告诉系统次报文段有紧急指针,应该尽快的处理(相当于高优先级的数据),而不要按照原来的排序序列来传送。若不使用紧急指针,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有数据段被处理完毕后这两个字符才能被交付到接收方的网应用进程。URG是一个正偏移,与TCP首部中序号字段的值相加表示紧急数据后面的字节,即紧急指针是指向紧急数据最后一个字节的下一字节。
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-11,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK11, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是11,
建立连接(三次握手)的过程:
看下两次握手会产生什么影响就可以理解为什么要三次握手了。ClientA发送给ServerA 第一次发送SYN申请建立连接,但是因为网络原因阻塞ServerA没收到迟迟没有回复,于是第二次发送SYN申请建立连接,这次ServerA收到了并且回应了ACK给ClientA,在两次握手情景下此时连接建立,互相发送完请求和相应后正常关闭连接。但就在此时ClientA第一次发送的SYN数据包网络顺畅了到达了ServerA,但是ClientA已关闭甚至关机,但是由于两次握手ServerA回复ACK后建立连接,此时此连接会长期存在,会造成服务器资源浪费。
看下三次挥手会产生什么影响就可以理解为什么要四次挥手了,ClientA发送FIN给ServerA申请关闭连接,如果是三次挥手ServerA接到后就必须回复ACK+FIN 回复ClientA的关闭请求以及向ClientA发起关闭请求,然后ClientA回复ServerA的关闭请求发送ACK。在三次挥手的此次流程里有一个问题就是serverA必须同时回复ACK+FIN,在很多时候client申请关闭连接时ServerA还在处理之前请求或者为响应完数据,此时serverA不应该发送FIN,应该先发送ACK等数据发送结束后再发送FIN申请关闭。所以需要四次挥手才能满足以上场景。
下面再在套接字层面上解释一下四次挥手干了什么:
首先在Linux系统中一切皆文件,套接字也是一种特殊的文件为了建立网络通讯。欲建立连接的两个进程各自有一个socket来标识,即socket肯定时成对出现的,socket的包括读缓存区和写缓存区两部分。如下图,client的读缓存区对应server的写缓存区,client的写缓存区对应server的读缓存区。
在client发送FIN时就是告诉server“我要准备关闭啦,不会再给你发消息了,但是还能接收你的消息,你没发完的赶紧发”,server接收到以后回应ACK,此时client的写缓存区就关闭了,server的读缓存区关闭,此时称为半关闭状态。但是server还有消息没发完,等server发完消息以后,发送FIN给client“我消息发送完了,我也准备关闭了”,client收到以后回给server ACK,然后server的写缓存区和client的读缓存区关闭,连接真正关闭,释放资源。
在UDP协议中如果客户端发送消息很快,服务器处理的比较慢会导致服务器的读缓存区满掉,后面的消息就回丢失,在TCP协议中利用“滑动窗口”这一机制解决了这个问题。
从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的
抛出一个疑问为什么主动发起的一方在发送完最后的ACK之后还要再等2ML时间后才能真正关闭?
答:当Client最后的ACK由于网络原因未到Server时,Server还会重新发送FIN申请关闭,此时如果Client已经关闭则Server会一直卡在LAST_ACK状态无法关闭,所以Client延时关闭就是为了防止最后ACK消失的情况。
要想理解分片问题首先需要科普几个名词:分片、分段、MTU、MSS
MTU:以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的。这个1500字节被称为链路层的MTU(最大传输单元).
分片:当传输层传入网络层的数据过大,加上网络层IP前缀后大于1500是,为了满足MTU限制,会在网络层对数据进行分片,使每一片都小于MTU,在接收端的网络层在进行重新组合,这样做的弊端是当其中一片数据包丢失时,导致无法组合,在UDP协议中此时如果上层(应用层)没有重传机制则只能丢弃全部数据,即使有重传机制也要将整个数据包进行重传(重传机制应用层肯定要为数据包做编号,应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),所以不管上层是什么协议尽量要避免网络层触发数据分片。
MSS:MSS就是TCP数据包每次能够传输的最大数据分段。TCP协议在连接建立阶段进行三次握手的时候会确认此次连接的MSS值,默认取通讯双方MSS的的最小值作为此次连接的MSS最大值即TCP数据包每次能够传输的最大数据分段。
分段:分段是TCP的一个特性,当应用层传输的数据过大超过MSS时,TCP在传输层会进行分段,保证每个分段都小于MSS,TCP的每个分段都带有序号,在接收端的传输层可以进行重新排序组合,相比与网络层的分片组合,如果有一个分段消失,结合TCP的“确认和重传机制
”可以支持单个分段的重传而不用全部重传。
看完上面四个名词的解释,大家应该也明白网络分片带来的影响不会出现的TCP协议中,因为MSS的值会小于MTU,所以如果传输层使用的是TCP协议到达网络层后都满足MTU,不用进行分片,所以在使用TCP协议时,无需在应用层(用户程序)去控制每次发送数据包的大小。
如果想对BIO/NIO/多路复用等IO模型有深入了解请看我上篇文章从系统调用分析IO模型演变,一篇讲懂BIO、NIO、多路复用(selector,poll,epoll)原理,保证一发入魂。
服务器
package com.lago;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class SelectorServer {
public static void main(String[] args) throws Exception {
// 创建Socket服务器,绑定端口8080(调用socekt()函数时第二个参数为SOCK_STREAM)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 创建多路复用器selector :调用epoll_create函数创建eventpoll
Selector selector = Selector.open();
// 为socket服务器添加selector的引用:调用epoll_ctl将eventpoll添加至socket的等待队列中,并制定感兴趣的事件为有客户端连接时。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
// 调用epoll_wait,如果eventpoll中的rdlist(readList)不为空不阻塞,如果为空阻塞,并且将main函数java进程
// 添加至eventpoll的等待队列(进程挂起,释放cpu)
int select = selector.select();
// 如果有客户端连接服务器、或者收到客户端发来的消息,网卡会向cpu发送中断信号,触发中断程序唤醒java进程并且将有状态改变socket
// 对象添加至rdlist。所以能走到这一步说明rdlist里必然有socket,这里获取eventpoll中rdlist里的socket集合。
// rdlist中的socket处理完后不会清空,再有新的socket收到消息时会继续追加到rdlist中,为了防止socketA使用后
// 还始终在rdlist中,rdlist在遍历处理完后程序显示清空一下。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isAcceptable()){
// 触发事件为有客户端连接,说明这个socket为服务端ServerSocketChannel
// 如果为服务端接收到新的连接,则获取新接入的客户端并且将客户端也添加selector的引用
ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel();
// 获取新接入的客户端
SocketChannel client = serverSocket.accept();
// 为客户端也添加selector的引用:调用epoll_ctl将eventpoll添加至新接入客户端的等待队列中,并制定感兴趣的事件为接收到数据时。
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
System.out.println("已经有客户端"+client.socket().getInetAddress().getHostAddress()+":"+client.socket().getPort());
}else if(selectionKey.isReadable()){
// 触发事件为接收到数据,说明这个socket为客户端SocketChannel
SocketChannel socketClient= (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffers=ByteBuffer.allocateDirect(200);
// 获取客户端传入的消息
long read = socketClient.read(byteBuffers);
byteBuffers.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffers).toString();
byteBuffers.clear();
System.out.println("接收到客户端"+socketClient.socket().getInetAddress().getHostAddress()+":"+socketClient.socket().getPort()+":"+receiveData);
}
}
// 清空已经rdlist里已经经过处理的socket引用
selectionKeys.clear();
}
}
}
客户端
public class Client01 {
public static void main(String[] args) throws Exception {
// 创建客户端设置服务器ip端口
Socket socket = new Socket("192.168.2.170", 8080);
Scanner scanner=new Scanner(System.in);
while (true){
String content = scanner.nextLine();
socket.getOutputStream().write(content.getBytes());
}
}
}
常规总结肯定是,TCP协议是一种作用在传输层的面向连接的可靠的数据传输控制协议;但是这里的总结是结合上面所讲来分析这就话的加粗部分体现在哪儿。
UDP的报文相对于TCP来看简单了很多,因为UDP要做的事情也比TCP简单的多,甚至都不需要建立连接,在发送端只负责将应用层要发送的数据包丢给网络层,在接收端只需要将网络层中接收的数据包丢给应用层,不保证数据是否丢失。
由于UDP无需三次握手创建连接,也无需四次挥手断开连接,所以UDP的通讯时序图没什么意思,每条交互都是双方互发数据,没什么特别含义,这里用socket的系统调用图来展现UDP的交互细节,同时附一个TCP版的方便比较:
根据上图比较可看成,UDP客户端缺少了connect(),服务端缺少了accept(),在TCP的总结里也有提到connect函数触发了三次握手创建连接,这里也论证了UDP无需面向连接这一理论。
注:在TCP的分片问题里介绍了MTU、MSS、分片、分段四个词的含义,忘记的同学可以往上翻翻。
UDP比起TCP就简单暴力的多,应用层给我什么数据我就一次性都给到网络层,并没有MSS等限制,所以在应用层一次想要发送大量数据时就会触发网络层的数据分片,然而UDP的接收端也不会考虑网络层重组的分片是否完整直接将数据给到应用层,所以应用层最好控制每次发送数据包的大小,保证到达网络层后数据包小于MTU不会触发网络分片(应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),除此之外需要为每个数据包设计编号,并且包含超时重发机制。
PS:UDP常说收到消息时顺序有可能错乱,并不是由分片引起的,分片是指发送端发送一段报文数据过大,被网络层进行分片处理,这里分片处理后到达接收端的网络层肯定可以按顺序排好序的;UDP乱序情景是发送端发送多段符合MTU的报文时,到达接收端后由于每个段报文走的路由可能不同可能导致多段数据到达顺序错乱,所以如果对数据展示顺序有严格要求时应用层最要有排序机制。同时也是因为UDP应用层需要考虑很多机制所以不可能到达一个报文展示一个报文,所以也推荐有缓存机制,接收端消息模块 处理完一组报文后放入缓存,接收端展示模块去读缓存中正确排序且完整的数据进行展示。
服务器
PS:此服务器为简单版UDP服务器,仅仅添加了个消息收到确认机制,并未添加超时重发、排序等机制。
public class SelectorUDPServer {
public static void main(String[] args)throws Exception {
// 创建UDP服务器,绑定端口,设置为非阻塞(调用socekt()函数时第二个参数为SOCK_DGRAM)
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress("192.168.2.170",8080));
datagramChannel.configureBlocking(false);
// 创建多路复用器:调用epoll_create创建eventpoll
Selector selector = Selector.open();
// 为socket服务器添加selector的引用:调用epoll_ctl将eventpoll添加至socket的等待队列中,并制定感兴趣的事件为有客户端连接时。
datagramChannel.register(selector, SelectionKey.OP_READ);
while (true){
// 调用epoll_wait,如果eventpoll中的rdlist(readList)不为空不阻塞,如果为空阻塞,并且将main函数java进程
// 添加至eventpoll的等待队列(进程挂起,释放cpu)
int select = selector.select();
if(select>0){
// 如果有客户端连接服务器、或者收到客户端发来的消息,网卡会向cpu发送中断信号,触发中断程序唤醒java进程并且将有状态改变socket
// 对象添加至rdlist。所以能走到这一步说明rdlist里必然有socket,这里获取eventpoll中rdlist里的socket集合。
// rdlist中的socket处理完后不会清空,再有新的socket收到消息时会继续追加到rdlist中,为了防止socketA使用后
// 还始终在rdlist中,rdlist在遍历处理完后程序显示清空一下。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isReadable()){
// 触发事件为接收到数据,说明这个socket为客户端SocketChannel
DatagramChannel channel= (DatagramChannel) selectionKey.channel();
// 获取客户端传入的消息
InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
byteBuffer.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("服务器接收到客户端"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
// 给客户端回复消息已收到
channel.send(ByteBuffer.wrap(("以接收到消息**"+receiveData+"**").getBytes()),receive);
byteBuffer.clear();
}
}
// 清空已经rdlist里已经经过处理的socket引用
selectionKeys.clear();
}
}
}
}
客户端
PS:客户端与服务器基本类似故未增加过度注释
public class UDPClient {
public static void main(String[] args) throws Exception {
DatagramChannel client=DatagramChannel.open();
// 只是在javaAPI中为了流程统一调用了connect方法,其实并未调用系统函数connect
client.connect(new InetSocketAddress("192.168.2.170",8080));
Selector selector=Selector.open();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
while (true){
Scanner scanner=new Scanner(System.in);
String content = scanner.nextLine();
client.write(ByteBuffer.wrap(content.getBytes()));
int select = selector.select();
if(select>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);
for (SelectionKey selectionKey : selectionKeys) {
if(selectionKey.isReadable()){
DatagramChannel channel= (DatagramChannel) selectionKey.channel();
InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
byteBuffer.flip();
String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("客户端接收到服务器"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
byteBuffer.clear();
}
}
selectionKeys.clear();
}
}
}
}
同TCP一样这里的总结的主要内容就是解析“UDP协议是一种如用在传输层的无连接的不可靠的数据传输控制协议”中加粗字的含义体现在哪里。