TCP是什么php
首先看一下OSI七层模型:前端
而后数据从应用层发下来,会在每一层都加上头部信息进行封装,而后再发送到数据接收端,这个基本的流程中每一个数据都会通过数据的封装和解封的过程,流程以下图所示:java
在OSI七层模型中,每一层的做用和对应的协议以下图所示:linux
说回TCP,简单说TCP(Transmission Control Protocol)即传输控制协议,是一种面向链接的、可靠的、基于ip的传输层协议。缓存
TCP协议头部格式服务器
要学习TCP协议,首先得知道TCP协议头部的格式,我在网上找了一张以为画得比较好的TCP协议头部格式的图片:网络
这张图把TCP协议头部格式的每部分都描述得比较清楚:socket
从图上咱们能够看到,TCP头部的固定大小为20个字节,不过因为有可选字段,实际上TCP头部的大小有可能超过20字节。tcp
TCP三次握手函数
TCP三次握手是TCP一个比较重点的内容,来学习一下。
TCP三次握手其实就是TCP链接创建的过程,三次握手的目的是同步链接双方的序列号和确认号并交换TCP窗口大小信息。下面是TCP三次握手的流程图:
画得很清晰,惋惜不是我画的。整个流程为:
为何在第3步中客户端还要再进行一次确认呢?这主要是为了防止已经失效的链接请求报文段忽然又传回到服务端而产生错误的场景:
所谓"已失效的链接请求报文段"是这样产生的。正常来讲,客户端发出链接请求,但由于链接请求报文丢失而未收到确认。因而客户端再次发出一次链接请求,后来收到了确认,创建了链接。数据传输完毕后,释放了链接,客户端一共发送了两个链接请求报文段,其中第一个丢失,第二个到达了服务端,没有"已失效的链接请求报文段"。 如今假定一种异常状况,即客户端发出的第一个链接请求报文段并无丢失,只是在某些网络节点长时间滞留了,以致于延误到链接释放之后的某个时间点才到达服务端。原本这个链接请求已经失效了,可是服务端收到此失效的链接请求报文段后,就误认为这是客户端又发出了一次新的链接请求。因而服务端又向客户端发出请求报文段,赞成创建链接。假定不采用三次握手,那么只要服务端发出确认,链接就创建了。
因为如今客户端并无发出链接创建的请求,所以不会理会服务端的确认,也不会向服务端发送数据,可是服务端却觉得新的传输链接已经创建了,并一直等待客户端发来数据,这样服务端的许多资源就这样白白浪费了。
采用三次握手的办法能够防止上述现象的发生。好比在上述的场景下,客户端不向服务端的发出确认请求,服务端因为收不到确认,就知道客户端并无要求创建链接。
TCP四次握手
TCP三次握手是TCP链接创建的过程,TCP四次握手则是TCP链接释放的过程。下面是TCP四次握手的流程图:
当客户端没有数据再须要发送给服务端时,就须要释放客户端的链接,这整个过程为:
这里的一个问题是,为何TCP链接的创建只须要三次握手而TCP链接的释放须要四次握手呢:
由于服务端在LISTEN状态下,收到创建请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而链接关闭时,当收到对方的FIN报文时,仅仅表示对方没有须要发送的数据了,可是还能接收数据,己方未必数据已经所有发送给对方了,因此己方能够当即关闭,也能够将应该发送的数据所有发送完毕后再发送FIN报文给客户端来表示赞成如今关闭链接。
从这个角度而言,服务端的ACK和FIN通常都会分开发送。
使用Wireshark抓包验证TCP三次握手过程
为了加深对TCP三次握手的理解,抓包看一下TCP三次握手的过程。我这里访问的是咱们公司本身的网站,不打广告,访问的具体什么页面、哪一个ip就不透露了。
抓包下来的内容为:
这里多说一句,因为wireshark抓包针对的是网卡,所以只要某张网卡上有网络访问,就会有数据包,这会致使Wireshark的抓包结果里面会有大量数据包,而大多数都不是想要的,这种状况可使用Wireshark的过滤规则。我这里因为知道目标ip,所以使用的是"ip.src == xxx.xxx.xxx.xxx or ip.dst == xxx.xxx.xxx.xxx"这条规则只过滤特定的ip。
从抓包结果看来,整个过程符合TCP三次握手的预期:
至于Sequence Number和Acknowledge Number就不看了,可是注意,前面说了Sequence Number是随机产生的一个值,可是这里确是0,不光这里是0,抓其余的任何包这个值都是0。但其实这里并非真的0,而是Wireshark为了显示更好阅读,使用了relative sequence number相对序号,Sequence Number具体值咱们也是能够看到的:
第一个红框就是上面说的relative sequence number,第二个红框就是Sequence Number的真实值0xc978aa7e,转换为十进制为3380128382,就是随机产生的Sequence Number。
顺便能看到,下一个数据包就是HTTP的数据包,由于TCP三次握手已完成,链接创建,正式传输应用层数据,传输的HTTP内容大小为704字节。
TCP的backlog
在学习TCP的时候发现的一个比较重要的知识点。
在TCP链接创建的过程当中有以下的流程和队列:
如图所示,这里面有两个队列,分别为syns queue(半链接队列)与accept queue(全链接队列)。整个流程总结用文字以下:
backlog的定义是已链接但未进行accept处理的socket队列大小,若是这个队列满了,将会发送一个ECONNREFUSED错误信息给到客户端,即 linux 头文件 /usr/include/asm-generic/errno.h中定义的“Connection refused”。
Java支持原生的Socket,咱们能够写一段代码来验证一下。首先是一个普通的客户端Socket,模拟向本地的8888端口发起链接:
1 public class ClientSocketClass { 2 3 private static Socket[] clients = new Socket[30]; 4 5 public static void main(String[] args) throws Exception { 6 for (int i = 0; i < 10; i++) { 7 clients[i] = new Socket("127.0.0.1", 8888); 8 System.out.println("Client:" + i); 9 } 10 } 11 12 }
接着是服务端Socket,监听8888端口,ServerSocket构造函数的第二个参数就是backlog的大小,若是backlog小于1或者不传会给一个默认值50,代码很简单:
1 public class ServerSocketClass { 2 3 public static void main(String[] args) throws Exception { 4 ServerSocket server = new ServerSocket(8888, 5); 5 6 while (true) { 7 // server.accept(); 8 } 9 } 10 11 }
先把注释关闭,运行ServerSocketClass,先发起监听,再运行ClientSocketClass,运行结果为:
1 Client:0 2 Client:1 3 Client:2 4 Client:3 5 Client:4 6 Exception in thread "main" java.net.ConnectException: Connection refused: connect 7 at java.net.DualStackPlainSocketImpl.connect0(Native Method) 8 at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79) 9 at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339) 10 at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) 11 at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182) 12 at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) 13 at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) 14 at java.net.Socket.connect(Socket.java:579) 15 at java.net.Socket.connect(Socket.java:528) 16 at java.net.Socket.<init>(Socket.java:425) 17 at java.net.Socket.<init>(Socket.java:208) 18 at org.xrq.test.socket.ClientSocketClass.main(ClientSocketClass.java:11)
看到Client只发起了五个请求,第六个请求发起被拒绝了,由于三次握手创建后,前五个请求占据了全链接队列并无被处理,因而第六个请求进来,全链接队列中没有它的位置了,所以请求被拒绝。
若是注释打开,又是不同的效果:
1 Client:0 2 Client:1 3 Client:2 4 Client:3 5 Client:4 6 Client:5 7 Client:6 8 Client:7 9 Client:8 10 Client:9
这里全部的十个客户端请求所有被接受,由于accept()方法从全链接队列中取出了链接请求进行处理。看得出来,backlog提供了容量限制功能,避免过多的客户端Socket占据大量的服务端资源。
全链接队列大小的问题
接着说说全链接队列大小的问题。首先上面提到了backlog,不一样的应用对backlog的默认值定义不一样,好比:
Tomcat能够经过server.xml配置文件中<Connector />节点中的acceptCount来修改backlog。若是请求量不是很大,使用Tomcat默认的100也能够,但若是访问量比较大,建议这个值设置得大一些,好比1024或者更大。若是Tomcat前一层对SYC FLOOD攻击的防护没有把握的话,最好将SYN COOKIE防护也开启。
可是,全链接队列的大小未必是backlog的值,它是backlog与somaxconn(一个os级别的系统参数)的较小值。Linux环境下能够经过执行"cat /proc/sys/net/core/somaxconn"来查看:
这个值系统默认的是128,假如传入的backlog是10,取128和10的较小值,那么最终的全链接队列大小就是10。一样,若是要修改Linux系统默认的全链接队列大小的话,能够经过修改/proc/sys/net/core路径下的somaxconn。
半链接队列大小的问题
说完了全链接队列大小的问题,接着说一下半链接队列大小的问题,它是64与tcp_max_syn_backlog的较大值。
能够经过"cat /proc/sys/net/ipv4/tcp_max_syn_backlog"命令或者"cat /etc/sysctl.conf"命令来查看半链接队列的大小。之后者为例,其实就是打开了/ect/sysctl.conf这个文件:
标红的即tcp_max_syn_backlog默认值,默认值为1024,能够经过修改这个值来修改系统默认的半链接队列大小。
经过ss查看Socket统计状态
前面说了这么多全链接队列,那么如何查看全链接队列大小?
在Linux环境下能够经过ss命令查看,ss命令全称为Socket Statistics,顾名思义它用于统计Socket。netstat命令其实也能够显示相似内容,可是ss命令相比netstat命令可以显示更多更详细的有关TCP和链接状态的信息,并且比netstat更快速更高效。
ss命令的参数就不列举了,能够本身上网查看,这里使用ss -lnt,即查看处于LISTEN状态的TCP套接字,且不解析服务名称:
Send-Q表示当前端口的全链接队列大小,Recv-Q表示全链接队列当前使用了多少。
从Send-Q能够看到,它的值只有三种:12八、50、1。这也印证了咱们的结论,全链接队列的大小为传入的backlog与somaxconn的较小值。
参考文章
http://blog.csdn.net/oney139/article/details/8103223
http://www.jellythink.com/archives/705
http://jm.taobao.org/2017/05/25/525-1/