[Linux网络编程]深入了解TCP通信API,看看底层做了啥(看了这篇文章,你也算是读了Linux源码的高手!)
本篇文章篇幅不长,就当茶余饭后、无聊之余看个乐呵。
体验一下知识过脑,却不留下一丝痕迹的奇妙感觉!
文章目录
- 前言:
- 接收连接以前
- socket()
- bind()
- listen()
- 接收连接后
- connect()
- accept()-1
- 全连接队列&半连接队列
- accept()-2
前言:
在学了Linux网络编程时,有几个问题一直困扰着我
- listen的第二个参数backlog到底是什么
- 为什么网络套接字也是文件描述符
- 为什么要bind(),他起了什么作用
- …
于是就决心梳理一下整套通信的过程,建立完整的通信框架,便有了这篇文章
tips:以下内容均在Linux2.6内核讨论,不同内核的实现方式大致相同,但是某些细节略有不同!!
接收连接以前
在初始化服务器时,server端会经过socket(),bind()、listen(),他们分别实现了
- 创建套接字 -> socket()
- 将套接字绑定到对应端口 -> bind()
- 开启监听状态 -> listen()
socket()
Linux内,套接字的本质也是文件描述符。所以socket()调用创建的是一个文件。
上图是socket()函数调用的全过程
task_struct
进程控制块在进程启动时已经创建struct socket
会被创建。如果socket()调用成功(返回>0),则会创建一个struct file
结构体,并为其分配一个文件描述符表的下标(fd_array[n])。struct file
内部含有一个private_data
指针,指向创建的struct socket
结构体。struct sock
会被创建并用调用socket()时传入的参数来初始化struct socket
的file字段初始化为指向它的private_data所属的结构体,sk字段指向struct sock
结构体
bind()
bind()函数调用可以将创建的套接字文件描述符绑定到某一端口、IP上
上图是bind()函数调用的全过程
- 在socket()函数调用的时候,也会有一个
struct inet_sock
结构体被创建。而这个结构体的第一个对象就是struct sock
,所以我们只要知道了struct sock
对象的地址就可以知道struct inet_sock
对象的地址(这是linux常用的一种类似于联合体的方法)- 调用bind()函数传入的参数会被用来初始化
struct inet_sock
结构体。这样一来,这个文件就有了网络的性质:端口、IP
listen()
listen()函数将套接字绑定监听状态,等待客户端发起连接请求
上图是listen()函数调用的全过程
struct proto
结构体中是一组函数指针,这就是C风格多态,用来实现不同协议的多态调用- 将
sock_common
结构体中的skc_state变量初始化为LISTEN状态
接收连接后
connect()
客户端调用connect()时,实际上是发生了TCP连接建立的3次握手。
需要明确一点:客户端发起connect()之前,自身也进行了socket()、bind()(隐式bind)操作
上图是connect()函数调用时发生的情况
- 创建了
struct tcp_sock
结构体,该结构体的起始地址也是struct inet_connection_sock
、struct inet_sock
和struct sock
的起始地址(其中struct inet_connect_sock
结构体是struct tcp_sock
独有的来管理半连接队列的结构体,后面会讨论)- 将
strcut tcp_sock
中的skc_state字段初始化为TCP_SYN_SENT
到此,本地的工作做完,然后就是发起三次握手
本地的
struct tcp_sock
结构体已经建立好了,现在就发送连接请求,请求服务器端处理我的连接请求(发起三次握手)
accept()-1
客户端发起connect()后,服务器端使用accept()接收新连接。让我们回顾三次握手
其中,分为几个阶段
- 第一次client发起SYN请求时,server会将该请求放入半连接队列(用于处理临时连接的小结构体)
struct tcp_sock
中的struct inet_connection_sock
中的struct request_sock_queue
管理的就是半连接队列- 如果connect()再次确认要建立连接,则server确认建立后,会将该链接放入全连接队列,并将其从半连接队列删除
struct sock
中的struct sk_buff_head
管理的就是全连接队列(找不到十足的证据,所以不能保证就是,但是大概率确定)
全连接队列&半连接队列
概念:
-
半连接队列: client发送的连接请求还未完整进行三次握手,此时server为了对其进行管理、但是又不想使用太大的空间(因为client可能会放弃这个连接),所以将其放入半连接队列,使用链表进行管理(链表队列)
-
全连接队列: 连接请求已经经过了三次握手,等待server调用accept()连接请求,便将其放入全连接队列(链表队列)中等待被accept()
-
backlog: 全连接队列允许存放节点的最大长度,也就是允许处于等待accept()调用的最大新连接个数
为何要设置全连接队列?
- 服务器维护的全连接队列就是个中间商(生产消费模型),来提高执行效率
- 如果队列过短/为空,可能导致用户发起连接的时候,恰巧碰到OS正在accept()别的连接,那么这个连接就会被丢弃
- 如果队列过长,可能导致用户发起连接后,等了好久,server既没有接收我的请求,也没有返回我任何信息(比如打开一个网页,此时用户不好判断具体是什么原因导致了网页半天打不开:网络不好?还是连接没有被接收),这样会影响用户的体验
accept()-2
前面的accept()只讨论到了放入全连接队列,此处继续讨论
当上层调用accpet()后,OS会为其创建一个新的套接字,并为其分配一个新的文件描述符并返回,并将该连接请求从全连接队列中删除,然后就可以使用该套接字进行通信了
对于开头的另外两个问题,这里做出简单回答:
1. 为什么网络套接字也是文件描述符
Linux下一切皆文件。对于网络套接字,抽象来看就是对网络作IO操作罢了,所以就可以像操作普通文件一样,使用文件描述符管理网络套接字
2.bind()起到什么作用
bind()将本地的某一个具体的文件和网络绑定到了一起。具体来说,他为文件绑定了端口、IP,它定义了应用程序将要监听的地址和端口(client要请求的地址和端口),正是这个方式,确保了应用能通过网络与其他设备进行通信