当前位置: 首页 > news >正文

台州黄岩网站建设搭建网站合同

台州黄岩网站建设,搭建网站合同,wordpress特别卡 iis,网站建造免费本文为《深入理解Linux网络》学习笔记#xff0c;使用的Linux源码版本是3.10#xff0c;网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读#xff1a;https://elixir.bootlin.com/linux/v3.10/source 5、深度理解TCP连接建立过程 1#xff09;、深入理解liste… 本文为《深入理解Linux网络》学习笔记使用的Linux源码版本是3.10网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读https://elixir.bootlin.com/linux/v3.10/source 5、深度理解TCP连接建立过程 1、深入理解listen 在服务端程序里在开始接收请求之前都需要先执行listen系统调用 1listen系统调用 可以在net/socket.c下找到listen系统调用的源码 // net/socket.c SYSCALL_DEFINE2(listen, int, fd, int, backlog) {...// 根据fd查找socket内核对象sock sockfd_lookup_light(fd, err, fput_needed);if (sock) {// 获取内核参数net.core.somaxconnsomaxconn sock_net(sock-sk)-core.sysctl_somaxconn;if ((unsigned int)backlog somaxconn)backlog somaxconn;// 调用协议栈注册的listen函数err security_socket_listen(sock, backlog);...}return err; }用户态的socket文件描述符只是一个整数而已内核是没有办法直接用的。所以该函数中第一行代码就是根据用户传入的文件描述符来查找对应的socket内核对象 再接着获取了系统里的net.core.somaxconn内核参数的值和用户传入的backlog比较后取一个最小值传入下一步 所以虽然listen允许我们传入backlog该值和半连接队列、全连接队列都有关系但是如果用户传入的值比net.core.somaxconn还大的话是不会起作用的 接着通过调用sock-ops-listen进入协议栈的listen函数 2协议栈listen sock-ops-listen指针指的是inet_listen函数 // net/ipv4/af_inet.c int inet_listen(struct socket *sock, int backlog) {...// 还不是listen状态(尚未listen过)if (old_state ! TCP_LISTEN) {...// 开始监听err inet_csk_listen_start(sk, backlog);...}// 设置全连接队列长度sk-sk_max_ack_backlog backlog;... }sk-sk_max_ack_backlog是全连接队列的最大长度。所以服务端的全连接队列长度是执行listen函数时传入backlog和net.core.somaxconn之间较小的那个值 如果在线上遇到了全连接队列溢出的问题想加大该队列长度那么可能需要同时考虑执行listen函数式传入的backlog和net.core.somaxconn之 inet_csk_listen_start函数源码如下 // net/ipv4/inet_connection_sock.c int inet_csk_listen_start(struct sock *sk, const int nr_table_entries) {...struct inet_connection_sock *icsk inet_csk(sk);// icsk-icsk_accept_queue是接收队列// 1.接收队列数据结构的定义// 2.接收队列的申请和初始化int rc reqsk_queue_alloc(icsk-icsk_accept_queue, nr_table_entries);... }在函数一开始将struct sock对象强制转换成了inet_connection_sock名叫icsk 这里简单讲讲为什么可以这么强制转换这是因为inet_connection_sock是包含sock的。tcp_sock、inet_connection_sock、inet_sock、sock是逐层嵌套的关系如下图所示类似面向对象里继承的概念 对于TCP的socket来说sock对象实际上是一个tcp_sock。因为TCP中的sock对象随时可以强制类型转换为tcp_sock、inet_connection_sock、inet_sock来使用 在接下来的一行reqsk_queue_alloc中实际上包含了两件重要的事情 接收队列数据结构的定义接收队列的申请和初始化 3接收队列定义 icsk-icsk_accept_queue定义在inet_connection_sock下是一个request_sock_queue类型的对象是内核用来接收客户端请求的主要数据结构。我们平时说的全连接队列、半连接队列全都是在这个数据结构里实现的如下图所示 // include/net/inet_connection_sock.h struct inet_connection_sock {struct inet_sock icsk_inet;struct request_sock_queue icsk_accept_queue;... };request_sock_queue的定义如下 // include/net/request_sock.h struct request_sock_queue {// 全连接队列struct request_sock *rskq_accept_head;struct request_sock *rskq_accept_tail;...// 半连接队列struct listen_sock *listen_opt;... };对于全连接队列来说在它上面不需要进行复杂的查找工作accept处理的时候只是先进先出地接受就好了。所以全连接队列通过rskq_accept_head和rskq_accept_tail以链表的形式来管理 和半连接队列相关的数据对象是listen_opt它是listen_sock类型的 // include/net/request_sock.h struct listen_sock {u8 max_qlen_log;...u32 nr_table_entries;struct request_sock *syn_table[0]; };因为服务端需要在第三次握手时快速地查找出来第一次握手时留存的request_sock对象所以其实是用了一个哈希表来管理就是struct request_sock *syn_table[0]。max_qlen_log和nr_table_entries都和半连接队列的长度有关 4接收队列申请和初始化 了解了全/半连接队列数据结构以后再回到inet_csk_listen_start函数中。它调用了reqsk_queue_alloc来申请和初始化icsk_accept_queue这个重要对象 // net/ipv4/inet_connection_sock.c int inet_csk_listen_start(struct sock *sk, const int nr_table_entries) {...int rc reqsk_queue_alloc(icsk-icsk_accept_queue, nr_table_entries);... }在reqsk_queue_alloc这个函数中完成了接收队列request_sock_queue内核对象的创建和初始化。其中包括内存申请、半连接队列长度的计算、全连接队列头的初始化等等 // net/core/request_sock.c int reqsk_queue_alloc(struct request_sock_queue *queue,unsigned int nr_table_entries) {size_t lopt_size sizeof(struct listen_sock);struct listen_sock *lopt;// 计算半连接队列的长度nr_table_entries min_t(u32, nr_table_entries, sysctl_max_syn_backlog);nr_table_entries max_t(u32, nr_table_entries, 8);nr_table_entries roundup_pow_of_two(nr_table_entries 1);// 为listen_sock对象申请内存,这里包含了半连接队列lopt_size nr_table_entries * sizeof(struct request_sock *);if (lopt_size PAGE_SIZE)lopt vzalloc(lopt_size);elselopt kzalloc(lopt_size, GFP_KERNEL);...// 全连接队列头初始化queue-rskq_accept_head NULL;// 半连接队列设置lopt-nr_table_entries nr_table_entries;...queue-listen_opt lopt;... }开头定义了一个struct listen_sock指针。这个listen_sock就是半连接队列 接下来计算半连接队列的长度。计算出来实际大小以后开始申请内存。最后将全连接队列头queue-rskq_accept_head设置成了NULL将半连接队列挂到了接收队列queue上 半连接队列上每个元素分配的是一个指针大小sizeof(struct request_sock *)。这其实是一个哈希表。真正的半连接用的request_sock对象是在握手过程中分配的计算完哈希值后挂到这个哈希表上 5半连接队列长度计算 reqsk_queue_alloc函数计算了半连接队列的长度 // net/core/request_sock.c int reqsk_queue_alloc(struct request_sock_queue *queue,unsigned int nr_table_entries) {...// 计算半连接队列的长度nr_table_entries min_t(u32, nr_table_entries, sysctl_max_syn_backlog);nr_table_entries max_t(u32, nr_table_entries, 8);nr_table_entries roundup_pow_of_two(nr_table_entries 1);...// 为了效率,不记录nr_table_entries// 而是记录2的N次幂等于nr_table_entriesfor (lopt-max_qlen_log 3;(1 lopt-max_qlen_log) nr_table_entries;lopt-max_qlen_log);... }传进来nr_table_entries在最初调用reqsk_queue_alloc的地方可以看到它是内核参数net.core.somaxconn和用户调用listen时传入的backlog二者之间的较小值 在这个reqsk_queue_alloc函数里又将会完成三次的对比和计算 min_t(u32, nr_table_entries, sysctl_max_syn_backlog)这句是再次和sysctl_max_syn_backlog内核对象取了一次最小值max_t(u32, nr_table_entries, 8)这句保证nr_table_entries不能比8小这是用来避免新手用户传入一个太小的值导致无法建立连接的roundup_pow_of_two(nr_table_entries 1)是用来上对齐到2的整数次幂的 下面通过两个实际的案例计算一下 假设某服务器上内核参数net.core.somaxconn为128net.ipv4.tcp_max_sync_backlog为8192。那么当用户backlog传入5时半连接队列到底是多长呢 和代码一样计算分为四步最终结果为16 min(backlog, somaxconn)min(5, 128)5min(5, tcp_max_sync_backlog)min(5, 8192)5max(5, 8)8roundup_pow_of_two(81)16 somaxconn和tcp_max_sync_backlog保持不变listen时的backlog加大到512。再算一遍结果为256 min(backlog, somaxconn)min(512, 128)128min(128, tcp_max_sync_backlog)min(128, 8192)128max(128, 8)128roundup_pow_of_two(1281)256 半连接队列的长度是min(backlog, somaxconn, tcp_max_sync_backlog)1再向上取整到2的N次幂但最小不能小于16 如果在线上遇到了半连接队列溢出的问题想加大该队列长度那么就需要同时考虑somaxconn、backlog和tcp_max_sync_backlog三个内核参数 为了提升性能内核并没有直接记录半连接队列的长度。而是采用了一种晦涩的方法只记录其N次幂。假设队列长度为16则记录max_qlen_log为42的4次方等于16假设队列长度为256则记录max_qlen_log为82的8次方等于256 6listen过程小结 listen最主要的工作就是申请和初始化接收队列包括全连接队列和半连接队列。其中全连接队列是一个链表而半连接队列由于需要快速地查找所以使用的是一个哈希表。全/半两个队列是三次握手中很重要的两个数据结构有了它们服务端才能正常响应来自客户端的三次握手。所以服务端都需要调用listen才行 全连接队列的长度对于全连接队列来说其最大长度是listen时传入的backlog和net.core.somaxconn之间较小的那个值。如果需要加大全连接队列长度那么就要调整backlog和somaxconn 半连接队列的长度对于半连接队列来说其最大长度是min(backlog, somaxconn, tcp_max_sync_backlog)1再向上取整到2的N次幂但最小不能小于16。如果需要加大半连接队列长度那么需要一并考虑backlog、somaxconn和tcp_max_sync_backlog这三个参数 2、深入理解connect 客户端在发起连接的时候创建一个socket然后瞄准服务端调用connect就可以了 int main() {fd socket(AF_INET, SOCK_STREAM, 0);connect(fd, ...);... }socket函数执行完毕后从用户层视角看到返回了一个文件描述符fd。但在内核中其实是一套内核对象组合包含file、socket、sock等多个相关内核对象构成每个内核对象还定义了ops操作函数集合 接下来就进入connect函数的执行过程 1connect调用链展开 当在客户端机上调用connect函数的时候事实上会进入内核的系统调用源码中执行 // net/socket.c SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,int, addrlen) {struct socket *sock;...// 根据用户fd查找内核中的socket对象sock sockfd_lookup_light(fd, err, fput_needed);...// 进行connecterr sock-ops-connect(sock, (struct sockaddr *)address, addrlen,sock-file-f_flags);... }这段代码首先根据用户传入的fd文件描述符来查找对应的socket内核对象。对于AF_INET类型的socket内核对象来说sock-ops-connect指针指向的是inet_stream_connect函数 // net/ipv4/af_inet.c int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,int addr_len, int flags) {...err __inet_stream_connect(sock, uaddr, addr_len, flags);... }int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,int addr_len, int flags) {struct sock *sk sock-sk;...switch (sock-state) {...case SS_UNCONNECTED:...err sk-sk_prot-connect(sk, uaddr, addr_len);...sock-state SS_CONNECTING;...break;}... }刚创建完毕的socket的新状态就是SS_UNCONNECTED所以__inet_stream_connect中的switch判断会进入case SS_UNCONNECTED的处理逻辑中 上述代码中sk取的是sock对象。对于AF_INET类型的TCP socket来说sk-sk_prot-connect指针指向的是tcp_v4_connect方法 // net/ipv4/tcp_ipv4.c int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {...// 设置socket状态为TCP_SYN_SENTtcp_set_state(sk, TCP_SYN_SENT);// 动态选择一个端口err inet_hash_connect(tcp_death_row, sk);...// 函数用来根据sk中的信息,构建一个syn报文,并将它发送出去err tcp_connect(sk);... }在这里将把socket状态设置为TCP_SYN_SENT。再通过inet_hash_connect来动态地选择一个可用的端口 2选择可用端口 // net/ipv4/inet_hashtables.c int inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk) {return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),__inet_check_established, __inet_hash_nolisten); }在调用__inet_hash_connect时传入的两个重要参数 inet_sk_port_offset(sk)这个函数根据要连接的目的IP和端口等信息生成一个随机数__inet_check_established检查是否和现有ESTABLISH状态的连接冲突的时候用的函数 __inet_hash_connect函数比较长先看前面这一段 // net/ipv4/inet_hashtables.c int __inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk, u32 port_offset,int (*check_established)(struct inet_timewait_death_row *,struct sock *, __u16, struct inet_timewait_sock **),int (*hash)(struct sock *sk, struct inet_timewait_sock *twp)) {...// 是否绑定过端口const unsigned short snum inet_sk(sk)-inet_num;...if (!snum) {// 获取本地端口配置inet_get_local_port_range(low, high);...// 遍历查找for (i 1; i remaining; i) {port low (i offset) % remaining;...}...}... }在这个函数中首先判断了inet_sk(sk)-inet_num如果调用过bind那么这个函数会选择好端口并设置是在inet_num上。假设没有调用过bind所以snum为0 接着调用inet_get_local_port_range这个函数读取的是net.ipv4.ip_local_port_range这个内核参数来读取管理员配置的可用的端口范围 net.ipv4.ip_local_port_range的默认值为32768 61000意味着端口总可用的数量是61000-3276828233个 接下来进入了for循环。其中offset是通过inet_sk_port_offset(sk)计算出的随机数。那这段循环的作用就是从某个随机数开始把整个可用端口范围遍历一遍。直到找到可用的端口后停止 接下来看看如何确定一个端口是否可用 // net/ipv4/inet_hashtables.c int __inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk, u32 port_offset,int (*check_established)(struct inet_timewait_death_row *,struct sock *, __u16, struct inet_timewait_sock **),int (*hash)(struct sock *sk, struct inet_timewait_sock *twp)) {...for (i 1; i remaining; i) {port low (i offset) % remaining;// 查看是否是保留端口,是则跳过if (inet_is_reserved_local_port(port))continue;// 查找和遍历已经使用的端口的哈希链表head hinfo-bhash[inet_bhashfn(net, port,hinfo-bhash_size)];...inet_bind_bucket_for_each(tb, head-chain) {// 如果端口已经被使用if (net_eq(ib_net(tb), net) tb-port port) {...// 通过check_established继续检查是否可用if (!check_established(death_row, sk,port, tw))goto ok;...}}// 未使用的话tb inet_bind_bucket_create(hinfo-bind_bucket_cachep,net, head, port);...goto ok;...}...return -EADDRNOTAVAIL;ok:... }首先调用inet_is_reserved_local_port判断要选择的端口是否在inet.ipv4.ip_local_reserved_ports中在的话就不能用 整个系统中会维护一个所有使用过的端口的哈希表它就是hinfo-bhash。接下来的代码就会在这里查找端口。如果在哈希表中没有找到那么说明这个端口是可用的。至此端口就算是找到了。这个时候通过inet_bind_bucket_create申请一个inet_bind_bucket来记录端口已经使用了并用哈希表的形式都管理了起来 遍历完所有端口都没找到合适的就返回-EADDRNOTAVAIL在用户程序上看到的就是Cannot assign requested address这个错误 当遇到Cannot assign requested address错误应该去查一下net.ipv4.ip_local_port_range中设置的可用端口的范围是不是太小了 3端口被使用过怎么办 // net/ipv4/inet_hashtables.c int __inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk, u32 port_offset,int (*check_established)(struct inet_timewait_death_row *,struct sock *, __u16, struct inet_timewait_sock **),int (*hash)(struct sock *sk, struct inet_timewait_sock *twp)) {...for (i 1; i remaining; i) {port low (i offset) % remaining;...inet_bind_bucket_for_each(tb, head-chain) {// 如果端口已经被使用if (net_eq(ib_net(tb), net) tb-port port) {...// 通过check_established继续检查是否可用if (!check_established(death_row, sk,port, tw))goto ok;...}}...}... }port在bhash中如果已经存在就表示有其他的连接使用过该端口了。请注意如果check_established返回0该端口仍然可以接着使用 一个端口怎么可以被用多次呢 回忆一下四元组的概念两对四元组中只要任意一个元素不同都算是两条不同的连接。以下的两条TCP连接完全可以同时存在假设192.168.1.101是客户端192.168.1.100是服务端 连接1192.168.1.101 5000 192.168.1.100 8090连接2192.168.1.101 5000 192.168.1.100 8091 check_established作用就是检测现有的TCP连接中是否四元组和要建立的连接四元素完全一致。如果不完全一致那么该端口仍然可用 这个check_established是由调用方传入的实际上使用的是__inet_check_established源码如下 // net/ipv4/inet_hashtables.c static int __inet_check_established(struct inet_timewait_death_row *death_row,struct sock *sk, __u16 lport,struct inet_timewait_sock **twp) {...// 找到哈希桶struct inet_ehash_bucket *head inet_ehash_bucket(hinfo, hash);...// 便利看看有没有四元组一样的,一样的话就报错sk_nulls_for_each(sk2, node, head-twchain) {if (sk2-sk_hash ! hash)continue;if (likely(INET_TW_MATCH(sk2, net, acookie,saddr, daddr, ports, dif))) {tw inet_twsk(sk2);if (twsk_unique(sk, sk2, twp))goto unique;elsegoto not_unique;}}... unique:// 要用了,记录,返回0(成功)return 0;not_unique:...return -EADDRNOTAVAIL; }该函数首先找到inet_ehash_bucket这个和bhash类似只不过这是所有ESTABLISH状态的socket组成的哈希表。然后遍历这个哈希表使用INET_TW_MATCH来判断是否可用 INET_TW_MATCH源码如下 // include/net/inet_hashtables.h #define INET_TW_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif) \((inet_twsk(__sk)-tw_portpair (__ports)) \(inet_twsk(__sk)-tw_daddr (__saddr)) \(inet_twsk(__sk)-tw_rcv_saddr (__daddr)) \(!(__sk)-sk_bound_dev_if || \((__sk)-sk_bound_dev_if (__dif))) \net_eq(sock_net(__sk), (__net)))在INET_TW_MATCH中将__saddr、__daddr、__ports都进行了比较。当然除了IP和端口INET_TW_MATCH还比较了其他一些项目 如果匹配就是四元组完全一致的连接所以这个端口不可用也返回-EADDRNOTAVAIL 如果不匹配哪怕四元组中有一个元素不一样例如服务端的端口号不一样那么就返回0表示该端口仍然可用于建立新连接 所以一台客户端机最大能建立的连接数并不是65535。只要服务端足够多单机发出百万条连接没有任何问题 4发起syn请求 再回到tcp_v4_connect // net/ipv4/tcp_ipv4.c int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {...// 设置socket状态为TCP_SYN_SENTtcp_set_state(sk, TCP_SYN_SENT);// 动态选择一个端口err inet_hash_connect(tcp_death_row, sk);...// 函数用来根据sk中的信息,构建一个syn报文,并将它发送出去err tcp_connect(sk);... }这时inet_hash_connect已经返回了一个可用端口接下来就进入tcp_connect // net/ipv4/tcp_output.c int tcp_connect(struct sock *sk) {...// 申请并设置skbbuff alloc_skb_fclone(MAX_TCP_HEADER 15, sk-sk_allocation);...tcp_init_nondata_skb(buff, tp-write_seq, TCPHDR_SYN);...// 添加到发送队列sk_write_queuetcp_connect_queue_skb(sk, buff);...// 实际发出synerr tp-fastopen_req ? tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk-sk_allocation);...// 启动重传定时器inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,inet_csk(sk)-icsk_rto, TCP_RTO_MAX);return 0; }tcp_connect做了这么几件事 申请一个skb并将其设置为SYN包添加到发送队列上调用tcp_transmit_skb将该包发出启动一个重传定时器超时会重发 该定时器的作用是等到一定时间后收不到服务端的反馈的时候来开启重传。首次超时时间是在TCP_TIMEOUT_INIT宏中定义的该值在Linux 3.10版本中是1秒 // net/ipv4/tcp_output.c void tcp_connect_init(struct sock *sk) {...// 初始化为TCP_TIMEOUT_INITinet_csk(sk)-icsk_rto TCP_TIMEOUT_INIT;... }TCP_TIMEOUT_INIT在include/net/tcp.h中被定义成了1秒 // include/net/tcp.h #define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))5connect小结 客户端在执行connect函数的时候把本地socket状态设置成了TCP_SYN_SENT选了一个可用的端口接着发出SYN握手请求并启动重传定时器 TCP连接中客户端的端口会在两个位置确定 第一个位置在connect的时候会随机地从ip_local_port_range选择一个位置开始循环判断。找到可用端口后发出syn握手包。如果端口查找失败会报错Cannot assign requested address。这个时候应该首先想到去检查一下服务器上的net.ipv4.ip_local_port_range参数是不是可以再放得多一些 如果因为某些原因不希望某些端口被用到那么把它们写到inet.ipv4.ip_local_reserved_ports参数中就行了内核在选择的时候会跳过这些端口 另外还要注意一个端口是可以被用于多条TCP连接 这里选择端口都是从ip_local_port_range范围中的某一个随机位置开始循环的。如果可用端很充足则能快一些找到可用端口那循环很快就能退出。假设实际中ip_local_port_range中的端口快被用光了这时候内核就大概率要把循环多执行很多轮才能找到可用端口这会导致connect系统调用的CPU开销上涨 如果在connect之前使用了bind将会使得connect系统调用时的端口选择方式无效。转而使用bind时确定的端口。调用bind时如果传入了端口号会尝试首先使用该端口号如果传入了0也会自动选择一个。但默认情况下一个端口只会被使用一次。所以对于客户端角色的socket不建议使用bind 3、完整TCP连接建立过程 在基于TCP的服务开发中三次握手的主要流程如下图所示 服务端核心逻辑时创建socket绑定端口listen监听最后accept接收客户端的请求 // 服务端核心代码 int main() {int fd socket(AF_INET, SOCK_STREAM, 0);bind(fd, ...);listen(fd, 128);accept(fd, ...); }客户端的核心逻辑是创建socket然后调用connect连接服务端 // 客户端核心代码 int main() {fd socket(AF_INET, SOCK_STREAM, 0);connect(fd, ...);... }1客户端connect 客户端通过调用connect来发起连接。在connect系统调用中会进入内核源码的tcp_v4_connect // net/ipv4/tcp_ipv4.c int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {...// 设置socket状态为TCP_SYN_SENTtcp_set_state(sk, TCP_SYN_SENT);// 动态选择一个端口err inet_hash_connect(tcp_death_row, sk);...// 函数用来根据sk中的信息,构建一个syn报文,并将它发送出去err tcp_connect(sk);... }在这里将完成把socket状态设置为TCP_SYN_SENT。再通过inet_hash_connect来动态地选择一个可用的端口后进入tcp_connect // net/ipv4/tcp_output.c int tcp_connect(struct sock *sk) {...// 申请并设置skbbuff alloc_skb_fclone(MAX_TCP_HEADER 15, sk-sk_allocation);...tcp_init_nondata_skb(buff, tp-write_seq, TCPHDR_SYN);...// 添加到发送队列sk_write_queuetcp_connect_queue_skb(sk, buff);...// 实际发出synerr tp-fastopen_req ? tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk-sk_allocation);...// 启动重传定时器inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,inet_csk(sk)-icsk_rto, TCP_RTO_MAX);return 0; }在tcp_connect申请和构造SYN包然后将其发出。同时还启动了一个重传定时器该定时器的作用是等到一定时间后收不到服务端的反馈的时候来开启重传。在Linux 3.10版本中首次超时时间是1秒 总结一下客户端在调用connect的时候把本地socket状态设置成了TCP_SYN_SENT选了一个可用的端口接着发出SYN握手请求并启动重传定时器 2服务端响应SYN 在服务端所有的TCP包包括客户端发来的SYN握手请求都经过网卡、软中断进入tcp_v4_rcv。在该函数中根据网络包skbTCP头信息中的目的IP信息查到当前处于listen状态的socket然后继续进入tcp_v4_do_rcv处理握手过程 // net/ipv4/tcp_ipv4.c int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) {...// 服务端收到第一步握手SYN或者第三步ACK都会走到这里if (sk-sk_state TCP_LISTEN) {struct sock *nsk tcp_v4_hnd_req(sk, skb);...}...if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb-len)) {rsk sk;goto reset;}... }在tcp_v4_do_rcv中判断当前socket时listen状态后首先会到tcp_v4_hnd_req查看半连接队列。服务端第一次响应SYN的时候半连接队列里必然空空如也所以相当于什么也没干就返回了 // net/ipv4/tcp_ipv4.c static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb) {...// 查找listen socket的半连接队列struct request_sock *req inet_csk_search_req(sk, prev, th-source,iph-saddr, iph-daddr);... }在tcp_rcv_state_process里根据不同的socket状态进行不同的处理 // net/ipv4/tcp_input.c int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len) {...switch (sk-sk_state) {...// 第一次握手case TCP_LISTEN:...// 判断是否为SYN握手包if (th-syn) {...if (icsk-icsk_af_ops-conn_request(sk, skb) 0)return 1;...}...}... }其中conn_request是一个函数指针指向tcp_v4_conn_request。服务端响应SYN的主要处理逻辑都在这个tcp_v4_conn_request里 // net/ipv4/tcp_ipv4.c int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb) {...// 看看半连接队列是否满了if (inet_csk_reqsk_queue_is_full(sk) !isn) {want_cookie tcp_syn_flood_action(sk, skb, TCP);if (!want_cookie)goto drop;}// 在全连接队列满的情况下,如果有young_ack,那么直接丢弃if (sk_acceptq_is_full(sk) inet_csk_reqsk_queue_young(sk) 1) {NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}// 分配request_sock内核对象req inet_reqsk_alloc(tcp_request_sock_ops);...// 构造synack包skb_synack tcp_make_synack(sk, dst, req,fastopen_cookie_present(valid_foc) ? valid_foc : NULL);...if (likely(!do_fastopen)) {...// 发送synack响应err ip_build_and_send_pkt(skb_synack, sk, ireq-loc_addr,ireq-rmt_addr, ireq-opt);...// 添加到半连接队列,并开启计时器inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);...}... }在这里首先判断半连接队列是否满了如果满了进入tcp_syn_flood_action去判断是否开启了tcp_syncookies内核参数。如果队列满并未开启tcp_syncookies那么该握手包将被直接丢弃 接着还要判断全连接队列是否满。因为全连接队列满也会导致握手异常那干脆就在第一次握手的时候也判断了。如果全连接队列满了且young_ack数量大于1的话那么同样也是直接丢弃 young_ack是半连接队列里保持着的一个计数器。记录的是刚有SYN到达没有被SYN_ACK重传定时器重传过SYN_ACK同时也没有完成过三次握手的sock数量 接下来是构造synack包然后通过ip_build_and_send_pkt把它发送出去 最后把当前握手信息添加到半连接队列并开启计时器。计时器的作用是如果某个时间内还收不到客户端的第三次握手服务端会重传synack包 总结一下服务端响应ack的主要工作是判断接收队列是否满了。满的话可能会丢弃该请求否则发出synack。申请request_sock添加到半连接队列中同时启动定时器 3客户端响应SYNACK 客户端收到服务端发来的synack包的时候也会进入tcp_rcv_state_process函数。不过由于自身socket的状态TCP_SYN_SENT所以会进入另一个不同的分支 // net/ipv4/tcp_input.c // 除了ESTABLISHED和TIME_WAIT,其他状态下的TCP处理都走这里 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len) {...switch (sk-sk_state) {...// 服务器收到第一个ACK包case TCP_LISTEN:...// 客户端第二次握手处理 case TCP_SYN_SENT:// 处理synack包queued tcp_rcv_synsent_state_process(sk, skb, th, len);...return 0;}... }tcp_rcv_synsent_state_process是客户端响应synack的主要逻辑 // net/ipv4/tcp_input.c static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len) {...tcp_ack(sk, skb, FLAG_SLOWPATH);...// 连接建立完成tcp_finish_connect(sk, skb);...if (sk-sk_write_pending ||icsk-icsk_accept_queue.rskq_defer_accept ||icsk-icsk_ack.pingpong) {// 延迟确认...} else {tcp_send_ack(sk);}... }tcp_ack()-tcp_clean_rtx_queue() // net/ipv4/tcp_input.c static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets,u32 prior_snd_una) {// 删除发送队列...// 删除定时器tcp_rearm_rto(sk);... }// net/ipv4/tcp_input.c void tcp_finish_connect(struct sock *sk, struct sk_buff *skb) {...// 修改socket状态tcp_set_state(sk, TCP_ESTABLISHED);...// 初始化拥塞控制tcp_init_congestion_control(sk);...// 保活计时器打开if (sock_flag(sk, SOCK_KEEPOPEN))inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));... }客户端将自己的socket状态修改为ESTABLISHED接着打开TCP的保活计时器 // net/ipv4/tcp_output.c void tcp_send_ack(struct sock *sk) {...// 申请和构造ack包buff alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));...// 发送出去tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC)); }在tcp_send_ack中构造ack包并把它发送出去 客户端响应来自服务端的synack时清除了connect时设置的重传定时器把当前socket状态设置为ESTABLISHED开启保活计时器后发出第三次握手的ack确认 4服务端响应ACK 服务端响应第三次握手的ack时同样会进入tcp_v4_do_rcv // net/ipv4/tcp_ipv4.c int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) {...if (sk-sk_state TCP_LISTEN) {struct sock *nsk tcp_v4_hnd_req(sk, skb);...if (nsk ! sk) {...if (tcp_child_process(sk, nsk, skb)) {...}return 0;}}... }不过由于这已经是第三次握手了半连接队列里会存在第一次握手时留下的半连接信息所以tcp_v4_hnd_req的执行逻辑会不太一样 // net/ipv4/tcp_ipv4.c static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb) {...struct request_sock *req inet_csk_search_req(sk, prev, th-source,iph-saddr, iph-daddr);if (req)return tcp_check_req(sk, skb, req, prev, false);... }inet_csk_search_req负责在半连接队列里进行查找找到以后返回一个半连接request_sock对象然后进入tcp_check_req // net/ipv4/tcp_minisocks.c struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,struct request_sock *req,struct request_sock **prev,bool fastopen) {...// 创建子socketchild inet_csk(sk)-icsk_af_ops-syn_recv_sock(sk, skb, req, NULL);...// 清理半连接队列inet_csk_reqsk_queue_unlink(sk, req, prev);inet_csk_reqsk_queue_removed(sk, req);// 添加全连接队列inet_csk_reqsk_queue_add(sk, req, child);return child;... }创建子socket 先来详细看看创建子socket的过程icsk_af_ops-syn_recv_sock是一个指针它指向的是tcp_v4_syn_recv_sock函数 // net/ipv4/tcp_ipv4.c const struct inet_connection_sock_af_ops ipv4_specific {....conn_request tcp_v4_conn_request,.syn_recv_sock tcp_v4_syn_recv_sock,... };// 这里创建sock内核对象 struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,struct request_sock *req,struct dst_entry *dst) {...// 判断接收队列是不是满了if (sk_acceptq_is_full(sk))goto exit_overflow;// 创建sock并初始化newsk tcp_create_openreq_child(sk, req, skb);... }注意在第三次握手这里又继续判断一次全连接队列是否满了如果满了修改一下计数器就丢弃了。如果队列不满那么就申请创建新的sock对象 删除半连接队列 把连接请求块从半连接队列中删除 // include/net/inet_connection_sock.h static inline void inet_csk_reqsk_queue_unlink(struct sock *sk,struct request_sock *req,struct request_sock **prev) {reqsk_queue_unlink(inet_csk(sk)-icsk_accept_queue, req, prev); }reqsk_queue_unlink函数中把连接请求块从半连接队列中删除 添加全连接队列 接着添加新创建的sock对象 // include/net/inet_connection_sock.h static inline void inet_csk_reqsk_queue_add(struct sock *sk,struct request_sock *req,struct sock *child) {reqsk_queue_add(inet_csk(sk)-icsk_accept_queue, req, sk, child); }在reqsk_queue_add中握手成功的request_sock对象插到全连接队列链表的尾部 // include/net/request_sock.h static inline void reqsk_queue_add(struct request_sock_queue *queue,struct request_sock *req,struct sock *parent,struct sock *child) {req-sk child;sk_acceptq_added(parent);if (queue-rskq_accept_head NULL)queue-rskq_accept_head req;elsequeue-rskq_accept_tail-dl_next req;queue-rskq_accept_tail req;req-dl_next NULL; }设置连接为ESTABLISHED 第三次握手的时候进入tcp_rcv_state_process的路径有点不太一样是通过子socket进来的。这时的子socket的状态是 // net/ipv4/tcp_input.c int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len) {...switch (sk-sk_state) {// 服务器第三次握手处理case TCP_SYN_RECV:...// 改变状态为连接tcp_set_state(sk, TCP_ESTABLISHED);...}... }将连接设置为TCP_ESTABLISHED状态。服务端响应第三次握手ACK所做的工作是把当前半连接对象删除创建了新的sock后加入全连接队列最后将新连接状态设置为ESTABLISHED 5服务端accept // net/ipv4/inet_connection_sock.c struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) {...// 从全连接队列中获取struct request_sock_queue *queue icsk-icsk_accept_queue;...req reqsk_queue_remove(queue);newsk req-sk;...return newsk;... }reqsk_queue_remove这个操作很简单就是从全连接队列的链表里获取一个头元素返回就行了 // include/net/request_sock.h static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue) {struct request_sock *req queue-rskq_accept_head;WARN_ON(req NULL);queue-rskq_accept_head req-dl_next;if (queue-rskq_accept_head NULL)queue-rskq_accept_tail NULL;return req; }所以accept的重点工作就是从已经建立好的全连接队列中取出一个返回给用户进程 6连接建立过程总结 三次握手详细过程总结如下图 一条TCP连接需要消耗多长时间。以上几步操作可以简单划分为两类 第一类是内核消耗CPU进行接收、发送或者是处理包括系统调用、软中断和上下文切换。它们的消耗基本都是几微妙左右第二类是网络传输当包被从一台机器上发出以后中间要经过各式各样的网线各种交换机路由器。所以网络传输的耗时相比本机的CPU处理就要高得多了。根据网络远近一般在几毫秒到几百毫秒不等 在正常的TCP连接的建立过程中一般考虑网络延时即可。一个RTT指的是包从一台服务器到另一台服务器的一个来回的延时时间所以从全局来看TCP建立连接的网络耗时大约需要三次传输再加上少许的双方CPU开销总共大约比1.5被RTT大一点点。不过从客户端视角来看只要ACK包发出了内核就认为连接建立成功可以开始发送数据了。所以如果在客户端打点统计TCP连接建立耗时只需两次传输耗时——即1个RTT多一点的时间对于服务端视角来看同理从SYN包收到开始算到收到ACK中间也是一次RTT耗时 推荐阅读 4.1 TCP 三次握手与四次挥手面试题 4.4 TCP 半连接队列和全连接队列 内核参数 tcp_syncookies-- 默认开启tcp_syncookies
http://www.yutouwan.com/news/416381/

相关文章:

  • 无锡营销型网站建设做离心开关的企业的网站
  • 网站整体运营思路业余从事网站开发
  • 网站建设制作公司都选万维科技单个网页打不开是什么原因
  • 网站建设电话合肥市城乡建设局2019网站
  • 马鞍山网站开发怎么运营一个淘宝店铺
  • 类似网站的建设广州做网站专业公司
  • 建立网站要钱吗大连市建设学校网站
  • 网站自助建站企业登记信息查询
  • 网站首页布局设计模板wordpress首页多样式
  • 泉州app网站开发价格新手做淘宝客网站教程
  • 如何用网站做课件内江市网站建设
  • 莱州网站建设制作北京市住房和城乡建设厅
  • 网站开发技术案例自助建站系统模板
  • 企业网站建设方案优化专业的企业级cms建站系统
  • 网页设计网页标签图标怎么做天津网络排名优化
  • php图片网站源码黑河做网站公司
  • php网站建设设计方法服务器吗放几个网站
  • linux网站如何做ip解析wordpress自定义排序
  • 新楼盘网站模板南宁兴宁区建设局网站
  • 网站被百度蜘蛛爬了多久放出来网站建设 源码
  • 西安搭建网站苏州比较大的设计公司
  • 科技服务 网站建设app 小程序wordpress整合ck播放器
  • 北京网站制作公司排名我想学制作网站吗
  • 外包 网站开发公司在茂名哪里可以做网站
  • 河南新乡做网站公司网站建设需要的资质
  • 唐山市住房和城乡建设局网站小程序怎么开发自己的微信小程序
  • 昆明建站公司推荐高端网页设计模板
  • 网站开发 团队协作wordpress主题 制作教程
  • 专做公司网站 大庆商务网站开发课程体会
  • 十堰门户网站建设装修合同电子版