全连接队列
listen第二个参数
服务器在调用listen的时候,listen的第二个参数 + 1,就是TCP全连接队列的长度。
当客户端的连接进入established 状态后,如果服务器没有调用accept将连接取走,那么该连接就会待在TCP全连接队列中,直到上层调用accept将其取走。
在全连接队列中的连接可以维持很长一段时间,除非上层调用accep取走,或者被上层主动关闭了;如果全连接队列满了,那么新来的链接就无法进入established状态,OS也会为这种状态的连接建立一种临时的数据结构,这个数据结构里的很多字段都未初始化完,会被OS保存在半连接队列中,在半连接队列中的连接保存时间一般比较短,一般是半分钟到一分钟。
为什么要有全连接队列?
当服务器很忙的时候,来不及调用accept接口,那么此时新连接就会被放在全连接队列当中,当服务器空闲下来时,就可以直接将全连接队列中的连接取走,然后进行业务处理。如果没有全连接队列,那么当服务器突然空闲下来的时候,这时候突然又没有连接来访问了,这就导致服务器的对资源的利用效率降低,吞吐量降低。会增加服务器的闲置率,减少给用户提供服务的效率和体验。
那全连接队列很长可以吗?
当然不可以。假设一个新来的用户一过来就在全连接末尾排队,说明此时的服务器压力已经很大了。如果这个队列很长,为了维护这个队列占用了大把的内存空间,服务器的处理速度慢,用户还要排很长时间,那为什么不让这个队列长度短一点,把节省下来的内存空间给服务器进行运算呢?
这个全连接的本质其实就是生产者消费者模型。服务器相当于消费者,负责处理连接,客户端相当于生产者,创造连接,并且将连接放入到缓冲区,也就是全连接队列中。
总结:
全连接队列本质上就是当服务器压力太大的时候,OS会在底层会为服务器将来不及处理的连接维护起来,等服务器空闲的时候再把连接获取上去。其中队列的长度就是listen的第二个参数 back_log + 1。
从内核层面理解socket和连接
连接的本质也是一种数据结构。
服务器也是一个进程,它有一个自己的task_struct 结构体,内部有一个自己的文件描述符表 struct files_struct 里面有一个 struct file* fd_array[] 文件描述符数组。
并且在进程启动的时候,OS会默认给我们打开 标准输入,标准输出,标准错误输出,分别占了 0,1,2三个文件描述符。在这里,当我们创建listen套接字的时候,会给用户返回3号这个文件描述符。既然它有文件描述符,那么也有自己的struct file结构体。
以上是文件系统部分。当我们创建socket套接字的时候,内核会帮我们创建一个 struct socket结构体。
我们发现这里面包含了一个回指向struct file结构体的指针
另外在struct file结构体中也包含了如下字段
void* private_data。
在创建套接字的时候,这个void* private_data会指向struct socket,于是它俩之间就关联起来了!
对于struct socket结构体,我们可以理解它是网络socket的入口。 为什么这么说呢,我们再来详细说明一下struct socket内部的一些字段。
const struct proto_ops* 这是一个指针,里面包含了一组方法簇。
所以上层在读写套接字时,就使用这里面的函数指针调用不同的方法。
当我们创建TCP套接字的时候,OS会在底层为用户创建一个struct tcp_sock
这就是在三次握手完成以后,OS会在内核给连接创建一个数据结构,就是它。
仔细留意这个结构体的第一个成员 ,又是一个结构体,类型是 inet_connection_sock。示意图:
在inet_connection_sock结构体中包含了很多跟tcp连接相关的信息,如下:
并且在这里面
struct request_sock_queue就是管理TCP全连接队列的。
但是我们又发现了它的第一个成员还是一个结构体
也就是struct inet_sock。 看一下它里面长啥样,inet_sock意思也就是网络套接字的意思。
所以我们看到了很多跟网络相关的字段,比如端口号,ip等。示意图又多了一层
但是又发现这个结构体居然还嵌套了一个结构体
也就是 struct sock 。所以示意图再加一层
但是这个结构体是不是很眼熟?没错,这就是一开始在 struct socket结构体里面,有一个指向它的指针:
struct sock结构体:
这里面包含的更多的是一些报文的信息。其中里面还包含了两个很重要的字段
它俩就是接收和发送缓冲区。
这里面也有指针
再回到刚刚,所以只要OS创建了一个 tcp_sock
那么刚刚嵌套的那些结构体自然也就都有了。并且我们注意到,这些嵌套的结构体都是上一层的第一个字段,也就意味着可以直接用指针来进行访问! 需要访问哪一个结构体内部的字段,只需要对这个指针进行相应的强转即可。这其实也就是C风格的多态。
另外UDP也有自己的套接字
它的第一个字段也是一个结构体,但是与TCP不一样的是,它的一个字段的结构体直接就是 inet_sock,也就是网络相关的套接字。因为UDP的实现比TCP简单,它不需要连接队列什么的,所以它不需要那么多字段。
所以相比之下,UDP不需要再嵌套 inet_connection_sock这个结构体。
同理
struct sock* 经过强转,同样也可以指向udp_sock。
它们的方法集也会变得不同。
在struct socket里面还有一个 type字段,就可以标识这是一个tcp还是一个udp套接字。
这样一看,struct socket可以看作基类,tcp_sock 和udp_sock可以看作是子类。
所以struct socket也成为 BSD socket 也就是通用网络接口。
学到这里其实可以在系统层面上给网络进行分层,
struct file属于第一层 :虚拟文件层。未来所有的套接字都可以变成文件。
struct socket 属于第二层:通用套接字层。
inet_sock 属于第三层:通用网络层,因为inet可以本地通信,所以不只有tcp和udp两种套接字。
struct inet_device 属于第四层: 网络设备层。跟网卡设备打交道的。(了解)
刚刚说的都是关于创建listen套接字的。
那么接收连接呢?
假设listen套接字的文件描述符是3,那么当三次握手成功之后,OS就会为新连接创建一个tcp_sock结构体,当然这个连接不用关系全连接队列的这些字段。创建好之后,就会把这个结构体放入到 3号文件的 全连接队列里。
当上层调用accept获取后,那么OS就会创建一个 struct file,和一个 struct socket,此时的文件描述符就是4,并将 struct socket 里面的 struct sock* 指向刚刚拿上来的 tcp_sock,这样也就关联起来了。那么以后就可以直接通过4号文件描述符,来对这个套接字进行读和写的操作了。
补充:
在sk_buff里面,通过控制指针的移动来对报文进行解包,提取有效载荷。
抓包
使用TCP dump抓包
Ubuntu中,如果没有安装,可以先安装
sudo apt-get update
sudo apt-get install tcpdump
常见使用:
sudo tcpdump -i any tcp
另外云服务和本地的抓包一般是不一样的。
用
ifconfig
lo就是本地环回。云服务器下网络通信用的就是eth0接口。
指定特定源IP地址 && 特定目的IP地址
比如
sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200
and tcp
src host表示的是指定源IP地址,dst host表示的是指定目的IP地址。
指定端口号抓包
例如捕获端口号为80的包(访问80端口的包)
sudo tcpdump port 80 and tcp
保存捕获的数据包到文件,例如:
sudo tcpdump -i eth0 port 80 -w data.pcap
将抓到的包保存在 data.pcap文件中。
另外使用 tcpdump 的时候,有些主机名会被云服务器解释成为随机的主机名,如果不 想要,就用 -n 选项
三次握手的抓包示例(服务器端启动的抓包软件):
一个包的标志位有一个 S,说明是对方发送了 SYN请求,接着服务器发送了 S. 说明了服务器发送了SYN + ACK。最后客户端发了 . ,说明是一个ACK,至此三次握手完成。并且注意一些细节,在第二个包 SYN + ACK的时候,注意到 ack是上一个 seq 的序列号 + 1,接着在第三个包客户端给服务器 ACK时,此时的 ack = 1,这次的ack不是说确认了序号1,它是被置为了1,而且没有携带自己的序号seq。
四次挥手的抓包示例:
这里看着好像只有三次挥手。其实是因为在服务器的代码逻辑中,客户端退了,服务器立马就关闭客户端的文件描述符了,所以在图中的第二个包,服务器发送给客户端的也是一个FIN + ACK,也就是捎带应答。
如果客户端关闭了,但是服务器还有数据要给客户端发送,然后再关闭,那么就会是正常的四次挥手,如下: