连接实际上是操作系统内核的一种数据结构,称为TCP控制块(TCB),对于linux而言是tcp_sock结构。不光连接,连数据包也是由一个数据结构来控制,linux里面称为sk_buff结构。完成三次握手就是连接,完成四次握手就是连接关闭。握手其实就是服务端和客户端都获取解析对方数据的方式(四元组是指source ip,source port,target ip,target port),放到对应的位置。这样两者就能一对一处理了,数据其实还是通过网卡传输的,但我只处理我认识的数据。这样比较好理解。

基本概念

短连接与长连接

1、短连接

连接->传输数据->关闭连接

HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。

也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

2、长连接

连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

TCP短连接与长连接

1、TCP短连接

我们模拟一下TCP短连接的情况,client向server发起连接请求,server接到请求,然后双方建立连接。client向server 发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起 close操作。为什么呢,一般的server不会回复完client后立即关闭连接的,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在 client/server间传递一次读写操作

短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段

2、TCP长连接

接下来我们再模拟一下长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

这边需要说一下TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资 源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务 器端检测到这种半开放的连接。

如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

  • 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
  • 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
  • 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
  • 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。

从上面可以看出,TCP保活功能主要为探测长连接的存活状况,不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。

在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问 题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的 客户端连累后端服务。

长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。

http短连接和长连接

什么是短连接和长连接(持久连接)。这里面并没有什么特别复杂的技术。

对于HTTP 1.0的http标准而言,默认连接是短连接,啥叫短连接?就是服务器当发送完最后一个字节的数据之后将关闭连接,也就是回收tcp_sock结构,这样,如果客户端再发送数据给服务器,将直接丢弃。即使此时客户端还有这样的结构,但是我们说连接已经关闭或者已经断了。

那客户端知不知道啥时候服务器的连接关闭?不知道,双方可以在任何时候来关闭自己的连接而没有必要通知对方。不过,对于短连接而言,通知不通知也没有意义了。

那短连接的弊端,大家可能都已经知道了,如果对一个服务器要连续发送多个请求,还需要为每次请求建立新的连接。

为了降低建立连接的时间,HTTP 1.1引入了长连接的概念,并把它搞成了默认的连接方式。啥叫长连接?就是当完成一个业务之后,socket结构并不回收。这样,只要在socket结构还存在的时候,客户端发送的任何数据,服务器都可以收到,这就是所谓的长连接。

相比短连接而言,长连接并没有什么特别的新的技术,只是维护socket结构时间长了。因为,说http长连接更不如说是tcp长连接。

pipeline

还有一种连接方式是pipeline,或者叫管道化连接。这又是一种啥呢?实际上,管道化连接是一种特殊形式的长连接。我们知道长连接节约了建立连接的时间,但是对于连续N个请求,我们还是需要等前一个响应收到之后才能发送下一个请求,如果在一个timeline上看,有点象一个锯齿。那我们知道网络传输的时间是很长的,那如果我们需要发起N个请求,客户端到服务器的传输时间为t,那总的时间为N*t*2;

如何缩短这部分时间呢?有人想到了个很自然的方法,我可不可以不等前一个响应回来就直接发送请求?在timeline图中就像在一个管子里不停的发请求,至于服务器的状态,我也根本不在乎。

管道化连接的方法的确降低了网络传输时间,但是它可能也引入了新的问题。因为客户端并不知道服务器啥时候关闭连接,那有可能管道里的请求,server处理了一部分就关闭了。但是客户端并不知道server处理了哪些?客户端只能选择重新建立连接并重传这些请求。如果这些请求全是对静态数据的请求也就罢了,如果是动态post数据,比如一个订单数据,再不幸的是server已经处理了这个数据,再来一份数据,server将再会处理一遍。这对用户实际的意图讲相差甚远。

也正是这个原因,管道化连接最好不要轻易使用

基本使用

长连接与短连接的使用时机

长连接:长连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。

每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。

如果每次操作都要建立连接然后再操作的话处理速度会降低,所以每次操作后,下次操作时直接发送数据就可以了,不用再建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,频繁的socket创建也是对资源的浪费。

当然连接池,可以更好的解决这个问题。还可以控制连接的数量。

短连接:web网站的http服务一般都用短连接。因为长连接对于服务器来说要耗费一定的资源。像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接更省一些资源。试想如果都用长连接,而且同时用成千上万的用户,每个用户都占有一个连接的话,可想而知服务器的压力有多大。所以并发量大,但是每个用户又不需频繁操作的情况下需要短连接。总之:长连接和短连接的选择要根据需求而定。

短连接就是在完成一次读写操作之后就断开连接,短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。

长连接是指双方建立连接,Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

实例

用Go实现一个长连接的思路是这样的:

1.创建一个套接字对象, 指定其IP以及端口.

2.开始监听套接字指定的端口.

3.如有新的客户端连接请求, 则建立一个goroutine, 在goroutine中, 读取客户端消息, 并转发回去, 直到客户端断开连接

4.主进程继续监听端口.

func main() {
    var tcpAddr *net.TCPAddr

    tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:9999")

    tcpListener, _ := net.ListenTCP("tcp", tcpAddr)

    defer tcpListener.Close()

    for {
        tcpConn, err := tcpListener.AcceptTCP()
        if err != nil {
            continue
        }

        fmt.Println("A client connected : " + tcpConn.RemoteAddr().String())
        go tcpPipe(tcpConn)
    }

}

func tcpPipe(conn *net.TCPConn) {
    ipStr := conn.RemoteAddr().String()
    defer func() {
        fmt.Println("disconnected :" + ipStr)
        conn.Close()
    }()
    reader := bufio.NewReader(conn)

    for {
        message, err := reader.ReadString('\n')
        if err != nil {
            return
        }

        fmt.Println(string(message))
        msg := time.Now().String() + "\n"
        b := []byte(msg)
        conn.Write(b)
    }
}

总结,无论是tcp还是http等上层协议的长连接还是短链接,完全是代码编程控制的,不需要使用什么库,你不close就是长连接,close就是短链接,当然也可以设置timeout来close,当然存在一个检活问题(心跳)。针对实际应用(比如redis)也是一样的道理。除非把close封装在接口中了,就存在这个接口原生是否支持短连接,但是长连接也就是原生的连接而已,所以正常client库都是短链接的一套流程。可以自行实现长连接控制。

连接安全问题

一种常见的攻击是SYN洪水,它的原理就是,此时服务器会产生大量的socket结构,大量的占据内存,然而并没有ACK数据到达,这样,如果有成千上万个SYN请求,那服务器的内存很快就会耗完,服务器也就down了。

连接问题处理

今天在生产环境使用cachecloud,突然发现cachecloud登录时候密码校验错误,于是到数据库mysql中查看密码,发现没有改动,那就只有cachecloud连接不到mysql数据库了。检查应用都是正常启动的。在看一下连接数,发现问题了。

首先在主机上查看各种状态的连接数:

[root@hn-redis01 CFG]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
CLOSE_WAIT 54574
ESTABLISHED 21612
FIN_WAIT2 5
TIME_WAIT 1

因为Linux分配给一个用户的文件句柄是有限的(可以参考:http://blog.csdn.net/shootyou/article/details/6579139), 而TIME_WAIT和CLOSE_WAIT两种状态如果一直被保持,那么意味着对应数目的通道就一直被占着,而且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就无法被处理了,接着就是大量Too Many Open Files异常,tomcat崩溃。。。

好几万的连接数,文件描述符也就是句柄都不够用了吧,赶紧用ulimit -a查看一下,使用ulimit -n 65536改一下,还是不行,为什么会有这么多的close_wait状态没有回收呢,难道是系统回收机制没有开启,使用sysctl -a来查看一下以下的参数

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时间


修改系统的参数,系统默认超时时间的是7200秒,也就是2小时。
默认如下:
tcp_keepalive_time = 7200 seconds (2 hours)
tcp_keepalive_probes = 9
tcp_keepalive_intvl = 75 seconds

意思是如果某个TCP连接在idle 2个小时后,内核才发起probe.如果probe 9次(每次75秒)不成功,内核才彻底放弃,认为该连接已失效

修改后

sysctl -w net.ipv4.tcp_keepalive_time=30
sysctl -w net.ipv4.tcp_keepalive_probes=2
sysctl -w net.ipv4.tcp_keepalive_intvl=2

经过这个修改后,服务器会在短时间里回收没有关闭的tcp连接。

发现都是开启了,那是这么回事呢,那就要重socket通信来说起了

全部11种状态
2.1、客户端独有的:(1)SYN_SENT (2)FIN_WAIT1 (3)FIN_WAIT2 (4)CLOSING (5)TIME_WAIT 。
2.2、服务器独有的:(1)LISTEN (2)SYN_RCVD (3)CLOSE_WAIT (4)LAST_ACK 。
2.3、共有的:(1)CLOSED (2)ESTABLISHED 。

了解time_wait和close_wait的区别

一个主动关闭和被动关闭的区别

TIME_WAIT状态可以通过优化系统参数得到解决,因为发生TIME_WAIT的情况是自己可控的,自己连接结束主动发送结束请求自己没有迅速回收资源所产生的状态,保持一段时间是tcp设计规定的,有好处,不多说,可以调整,如上面所说。

但是CLOSE_WAIT就不一样了,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后自己没有进一步发出ack信号也就是主动关闭的信号。就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。换句话说,就是对方发送了关闭请求,而我是被动关闭,而我这边却没有发送主动关闭的信号,就会形成close_wait状态,这个时候就需要代码中手动关闭这些连接才可以解决。

服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。

那么可以用netstat去看一下连接的情况,后面的是客户端,前面的是服务端,后面的访问前面的。发现是服务器端没有发送关闭信号,难道是redis里面没有关连接,这么成熟的产品不应该啊,继续找

[root@hn-redis01 CFG]# netstat -anp | grep 16316  | grep tcp |  awk '{print $5}'  | awk -F ":" '{print $1}' | sort | uniq -c | sort -nr | head -20
  28233 10.147.0.16
  11435 10.147.0.31
   7364 10.147.0.46
   5875 10.147.0.32
   5692 10.147.0.47
   1612 10.147.0.62
    972 10.147.0.17
    868 10.147.0.77
    719 10.147.0.92
    433 10.147.0.91
    420 10.147.0.61
    326 10.147.0.107
    254 10.147.0.76
    196 10.147.0.1
      1 0.0.0.0
      1 

发现客户端都有大几千上万的连接连过来,而redis的连接池是1W,会不会是redis太密集,导致了cpu瓶颈,不能处理对应的连接而导致的?