1、网络通信和TCPIP协议
OSI七层模型
TCP/IP模型
OSI模型比较复杂且学术化,所以我们实际使用的TCP/IP模型,分5层,物理层、数据链路层**(也有TCP/IP模型将物理层、数据链路层合称为网络接口层**,与之对应的,协议就被称为TCP/IP四层协议模型**)、网络层、传输层、应用层**。两个模型之间的对应关系如图所示:

无论什么模型,每一个抽象层建立在低一层提供的服务上,并且为高一层提供服务。大致来说,可以这么理解(只是帮助我们理解,实际上肯定会有点出入),对于我们的PC机来说,物理层可以看成网卡,数据链路层可以看成网卡驱动程序,网络层和传输层由操作负责处理,应用层则是常用的一些网络应用程序和我们自己所编写的网络应用程序。
TCP/IP协议族
Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。协议采用了5层的层级结构。然而在很多情况下,它是利用 IP 进行通信时所必须用到的协议群的统称。也就是说,它其实是个协议家族,由很多个协议组成,并且是在不同的层, 是互联网的基础通信架构。

IP、TCP和UDP
在上述图形中,网际协议IP是TCP/IP中非常重要的协议,往往用来确定网络中唯一的一台计算设备,它的作用就好比我们现实生活中的电话号码或者或者通讯地址。所以这层负责对数据加上IP地址(有发送它的主机的地址(源地址)和接收它的主机的地址(目的地址))和其他的数据以确定传输的目标。
而TCP和UDP都是传输层的协议,传输层主要为两台主机上的应用程序提供端到端的通信。
TCP有点类似于我们日常生活中的打电话,电话接通后通过“喂”确认对方身份,听不清会要求对方重说,对方说的太快了会要求对方说慢点,讲完了各说一句“再见”结束通话。TCP提供了一种可靠的数据传输服务,TCP是面向连接的,也就是说,利用TCP通信的两台主机首先要经历一个建立连接的过程,等到连接建立后才开始传输数据,而且传输过程中采用“带重传的肯定确认”技术来实现传输的可靠性。TCP还采用一种称为“滑动窗口”的方式进行流量控制,发送完成后还会关闭连接。
UDP(User Datagram Protocol的简称, 中文名是用户数据报协议)有点类似于我们日常生活中通过不靠谱的物流系统寄东西。UDP是把数据直接发出去,而不管对方是不是在接收,也不管对方是否能接收的了,也不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。
所以TCP要比UDP可靠的多。
注意:
我们一些常见的网络应用基本上都是基于TCP和UDP的,这两个协议又会使用网络层的IP协议。但是我们完全可以绕过传输层的TCP和UDP,直接使用IP,比如Linux内核中的LVS就可以直接基于IP层进行负载平衡调度;甚至还可以直接访问链路层,比如tcpdump程序就是直接和链路层进行通信的。
地址和端口号
端口号
在传输层也有这种类似于地址的概念,那就是端口号。端口号用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。
一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。

面试题:为什么端口号有65535个?
因为在TCP、UDP协议报文的开头,会分别有16位二进制来存储源端口号和目标端口号,所以端口个数是 2^16=65536个,但是0号端口用来表示所有端口,所以实际可用的端口号是65535个。
观察端口号
Windows下使用netstat -ano 查看所有端口号,netstat -ano|findstr “<端口号>”查看指定端口号。
Linux下可以用root 用户执行 lsof -i:端口号查看指定端口占用。
lsof -i -U:显示所有打开的UNIX domain和端口文件
我们用的更多的是netstat
netstat -tunlp 用于显示 tcp,udp 的端口和进程等相关情况。
netstat 查看端口占用语法格式:
netstat -tunlp | grep 端口号
-t (tcp) 仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数字的全部转化为数字
-l 仅列出在Listen(监听)的服务状态
-p 显示建立相关链接的程序名

面试题:一台主机上只能保持最多 65535 个 TCP 连接,对吗?
这个说法不对,我们分服务器和客户端分开讨论,以下的讨论都基于服务器和客户端都只有1个IP地址。
服务端
我们已经知道网络通信五元组是由过源IP地址、目标IP地址、协议号(协议类型)、源端口号以及目标端口号构成。现在考察的是 TCP 连接,自然五元组中的协议号已经定下来了,于是网络通信五元组就变化为TCP四元组。
那就是说TCP连接四元组是由源IP地址、源端口、目的IP地址和目的端口构成。
很明显当四元组中任意一个元素发生了改变,那么就代表的是一条完全不同的新连接。拿我们常用的MySQL 举例,假设它的 IP 是 X,端口3306。用户A基于IP地址A1,端口PA连接MySQL ,于是构成了一个TCP连接四元组(A1,PA,X,3306)。用户B基于IP地址B1,端口PB连接同一个MySQL,这个时候MySQL需要开启一个新端口来和用户B通信吗?从我们日常的开发就可以知道,MySQL并不需要这么做,所以用户B就和MySQL构成了一个新的TCP连接四元组(B1,PB,X,3306)。
服务端理论上能达成的最高并发数量是多少?从我们上面的用户A和用户B构成的TCP连接四元组:
(A1,PA,X,3306)
(B1,PB,X,3306)
可以看到目的IP地址和目的端口(X,3306)是不变的,这样就只剩下源IP地址、源端口是可变的。IP 地址是一个 32 位的整数,所以源 IP 最大有 2 的 32 次方这么多个。 端口是一个 16 位的整数,所以端口的数量就是 2 的 16 次方。2 的 32 次方(ip数)× 2的 16 次方(port数)大约等于两百多万亿。所以理论上,我们每个 server 可以接收的连接上限就是两百多万亿。
当然实际上做不到,目前工程实践中可以达到的连接数在千万级别。基于Java的应用程序大概能支持百万级别,具体怎么做会在本课程第五章中详细说明。
客户端
前面我们已经说过,“客户端应用程序完全可以不用自己设置端口号,而全权交给操作系统进行分配”,可用的端口号只有6万多,从这个角度考虑,客户端最多只能发起6万多条 TCP 连接。但其实也不是。
从TCP连接四元组来考虑:源IP地址、源端口、目的IP地址和目的端口,目的IP地址和目的端口指的是服务器的IP和端口,源IP地址、源端口自然就是客户端的。
只要服务器的 IP 或者端口不一样,即使客户端的 IP 和端口是一样的。这个四元组也是属于一条完全不同的新连接。比如:
连接1:客户端IP 10000 服务器IP 10000
连接2:客户端IP 10000 服务器IP 20000 虽然客户端的 IP 和端口完全一样,但由于服务器侧的端口不同,所以仍然是两条不同的连接。问题来了,客户端同一个端口可以连接不同的服务器吗?答案是可以的。
客户端只要启动时不显示绑定到某个端口上,内核是可以使用一个端口连不同的服务端,内核会自己进行选择并恰当地复用的,而且完全不会产生数据混乱,因为“源IP地址、目标IP地址、源端口号以及目标端口号就能唯一性确定一个TCP连接”。
那么对客户端来说,四元组里有3个可变,自然客户端能同时支持的连接数比服务器还要大得多。
TCP特性
在我们上面的讲述中,存在着客户端和服务端两者角色,在网络通信里是怎么区分的?这个就牵涉到了TCP的相关特性。
TCP(Transmission Control Protocol)是面向连接的通信协议,通过三次握手建立连接,然后才能开始数据的读写,通讯完成时要拆除连接,由于TCP是面向连接的所以只能用于端到端的通讯。
TCP提供的是一种可靠的数据流服务,数据有可能被拆分后发送,那么采用超时重传机制是和应答确认机制是组成TCP可靠传输的关键设计。
而超时重传机制中最最重要的就是重传超时(RTO,Retransmission TimeOut)的时间选择,很明显,在工程上和现实中网络环境是十分复杂多变的,有时候可能突然的抽风,有时候可能突然的又很顺畅。在数据发送的过程中,如果用一个固定的值一直作为超时计时器的时长是非常不经济也非常不准确的方法,这样的话,超时的时长就需要根据网络情况动态调整,就需要采样统计一个数据包从发送端发送出去到接收到这个包的回复这段时长来动态设置重传超时值,这个时长就是为RTT,学名round-trip time,然后再根据这个RTT通过各种算法和公式平滑RTT值后,最终确定重传超时值。
而IP层进行数据传输时,是不能保证数据包按照发送的顺序达到目的机器。当IP将把它们向‘上’传送到TCP层后,TCP将包排序并进行错误检查。TCP数据包中包括序号和确认,所以未按照顺序收到的包可以被排序,而损坏的包可以被重传。
TCP还采用一种称为“滑动窗口”的方式进行流量控制,所谓窗口实际表示接收能力,用以限制发送方的发送速度。
同时TCP还允许在一个TCP连接上,通信的双方可以同时传输数据,也就是所谓的全双工。
面向连接的服务(例如Telnet、FTP、rlogin、X Windows和SMTP)需要高度的可靠性,所以它们使用了TCP。DNS在某些情况下使用TCP(发送和接收域名数据库),但使用UDP传送有关单个主机的信息。
TCP三次握手
TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。
所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,所以网络通信中,发起连接的一方我们称为客户端,接收连接的一方我们称之为服务端。
简易速记版

第一次握手:客户端将请求报文标志位SYN置为1,请求报文的Sequence Number字段(简称seq)中填入一个随机值J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:服务器端收到数据包后由请求报文标志位SYN=1知道客户端请求建立连接,服务器端将应答报文标志位SYN和ACK都置为1,应答报文的Acknowledgment Number字段(简称ack)中填入ack=J+1,应答报文的seq中填入一个随机值K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:客户端收到应答报文后,检查ack是否为J+1,ACK是否为1,如果正确则将第三个报文标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。

为什么TCP握手需要三次?
TCP是可靠的传输控制协议,而三次握手是保证数据可靠传输又能提高传输效率的最小次数。为什么?RFC793,也就是TCP的协议RFC中就谈到了原因,这是因为:
为了实现可靠数据传输, TCP协议的通信双方,都必须维护一个序列号, 以标识发送出去的数据包中,哪些是已经被对方收到的。
举例说明:发送方在发送数据包(假设大小为 10 byte)时, 同时送上一个序号( 假设为 500),那么接收方收到这个数据包以后, 就可以回复一个确认号(510 = 500 + 10) 告诉发送方 “我已经收到了你的数据包, 你可以发送下一个数据包, 序号从 511 开始” 。
三次握手的过程即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值的必经步骤。
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
至于为什么不是四次,很明显,三次握手后,通信的双方都已经知道了对方序列号起始值,也确认了对方知道自己序列号起始值,第四次握手已经毫无必要了。
TCP的三次握手的漏洞-SYN洪泛攻击
但是在TCP三次握手中是有一个缺陷,被称为SYN洪泛攻击。三次握手中有一个第二次握手,服务端向客户端应答请求,应答请求是需要客户端IP的,而且因为握手过程没有完成,操作系统使用队列维持这个状态(Linux 2.2以后,这个队列大小参数可以通过/proc/sys/net/ipv4/tcp_max_syn_backlog设置)。于是攻击者就伪造这个IP,往服务器端狂发送第一次握手的内容,当然第一次握手中的客户端IP地址是伪造的,从而服务端忙于进行第二次握手,但是第二次握手是不会有应答的,所以导致服务器队列满,而拒绝连接。
面对这种攻击,有以下的解决方案,最好的方案是防火墙。
无效连接监视释放
这种方法不停监视所有的连接,包括三次握手的,还有握手一次的,反正是所有的,当达到一定(与)阈值时拆除这些连接,从而释放系统资源。这种方法对于所有的连接一视同仁,不管是正常的还是攻击的,所以这种方式不推荐。
延缓TCB分配方法
一般的做完第一次握手之后,服务器就需要为该请求分配一个TCB(连接控制资源),通常这个资源需要200多个字节。延迟TCB的分配,当正常连接建立起来后再分配TCB则可以有效地减轻服务器资源的消耗。
使用防火墙
防火墙在确认了连接的有效性后,才向内部的服务器(Listener)发起SYN请求,
TCP四次挥手(分手)
四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。过程简易记忆版:

由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

为什么TCP的挥手需要四次?
TCP是全双工的连接,必须两端同时关闭连接,连接才算真正关闭。
如果一方已经准备关闭写,但是它还可以读另一方发送的数据。发送给FIN结束报文给对方,对方收到后,回复ACK报文。当这方也已经写完了准备关闭,发送FIN报文,对方回复ACK。两端都关闭,TCP连接正常关闭。
为什么需要TIME-WAIT状态?
TIME_WAIT状态存在的原因有两点
1、可靠的终止TCP连接。
2、保证让迟来的TCP报文有足够的时间被识别并丢弃。
根据前面的四次握手的描述,我们知道,客户端收到服务器的连接释放的FIN报文后,必须发出确认。如最后这个ACK确认报文丢失,那么服务器没有收到这个ACK确认报文,就要重发FIN连接释放报文,客户端要在某个状态等待这个FIN连接释放报文段然后回复确认报文段,这样才能可靠的终止TCP连接。
在Linux系统上,一个TCP端口不能被同时打开多次,当一个TCP连接处于TIME_WAIT状态时,我们无法使用该链接的端口来建立一个新连接。反过来思考,如果不存在TIME_WAIT状态,则应用程序能过立即建立一个和刚关闭的连接相似的连接(这里的相似,是指他们具有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来连接的化身。新的化身可能受到属于原来连接携带应用程序数据的TCP报文段(迟到的报文段),这显然是不该发生的。这是TIME_WAIT状态存在的第二个原因。
MYSQL数据库大量TIME_WAIT的解决方法
Mysql参数调整
mysql> show variables like "time_timeout";
wait_timeout针对非交互式连接,一般来说通过mysql客户端连接数据库是交互式连接,通过jdbc连接数据库是非交互式连接,这个值代表服务器关闭非交互连接之前等待活动的秒数(也即是控制连接最大空闲时长),默认值:28800,单位秒,即8个小时。调整这个参数。
调整内核参数
vi /etc/sysctl.conf
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后执行 /sbin/sysctl -p 让参数生效。
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
可能产生的原因
连接Mysql的代码使用短连接来完成,用完后系统自动回收资源。由于访问量巨大,所以产生了很多连接。
另外,程序代码中没有使用mysql.colse(),导致MySQL空闲判断机制生效