目录
connect作用
客户端在执行connect时,
- 把本地socket状态设置成TCP_SYN_SENT
- 选择一个可用的端口,发出SYN握手请求并重置定时器
函数调用关系
connect -> __sys_connect-> __sys_connect_file
int __sys_connect_file(struct file *file, struct sockaddr_storage *address,
int addrlen, int file_flags)
{
//根据file查找sock对象
sock = sock_from_file(file, &err);
if (!sock)
goto out;
//connect
err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,
sock->file->f_flags | file_flags);
}
inet_stream_connect
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
err = __inet_stream_connect(sock, uaddr, addr_len, flags, 0);
}
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
//刚创建完的socket状态SS_UNCONNECTED
case SS_UNCONNECTED:
if (sk->sk_state != TCP_CLOSE)
goto out;
err = sk->sk_prot->connect(sk, uaddr, addr_len);
sock->state = SS_CONNECTING;
break;
}
}
tcp_v4_connect
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
//设置socket状态TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
//动态选择一个端口
err = inet_hash_connect(tcp_death_row, sk);
//根据sk中的信息,构建一个SYNC的报文,并将它发送出去
err = tcp_connect(sk);
}
inet_hash_connect
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 port = inet_sk(sk)->inet_num;//获取端口
//是否绑定端口
if (port) {
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
tb = inet_csk(sk)->icsk_bind_hash;
spin_lock_bh(&head->lock);
if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
inet_ehash_nolisten(sk, NULL);
spin_unlock_bh(&head->lock);
return 0;
}
spin_unlock(&head->lock);
ret = check_established(death_row, sk, port, NULL);
local_bh_enable();
return ret;
}
l3mdev = inet_sk_bound_l3mdev(sk);
//获取本地端口配置
inet_get_local_port_range(net, &low, &high);
high++; /* [32768, 60999] -> [32768, 61000[ */
remaining = high - low;
if (likely(remaining > 1))
remaining &= ~1U;
offset = (hint + port_offset) % remaining;
offset &= ~1U;
other_parity_scan:
port = low + offset;//如何确定端口可用
for (i = 0; i < remaining; i += 2, port += 2) {
if (unlikely(port >= high))
port -= remaining;
//是否是保留端口, net.ipv4.ip_local_reversed_ports
if (inet_is_local_reserved_port(net, port))
continue;
//查找已经使用的端口的hash表
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain) {
//如果端口被使用
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
tb->port == port) {
if (tb->fastreuse >= 0 ||
tb->fastreuseport >= 0)
goto next_port;
WARN_ON(hlist_empty(&tb->owners));
//检测是否有可用继续使用,检测四元组是否一样
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}
//没有被使用情况
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev);
if (!tb) {
spin_unlock_bh(&head->lock);
return -ENOMEM;
}
tb->fastreuse = -1;
tb->fastreuseport = -1;
goto ok;
next_port:
spin_unlock_bh(&head->lock);
cond_resched();
}
return -EADDRNOTAVAIL;//cannot assign requested address
}
关键函数解析
- 首先判断inet_sk(sk)->inet_num,如果调用过bind,这个函数会选择好端口并设置在inet_num上。如果没有bind,port为0;
- inet_get_local_port_range这个函数获取net.ipv4.ip_local_port_range内核参数;
- inet_is_local_reserved_port判断要选择的端口是否在net.ipv4.ip_local_reserved_local_port中;
- 在hash表中查找端口,如果没有找到则可以使用;inet_bind_bucket_for_each申请一个inet_bind_bucket来记录端口使用,并用hash表形式管理起来;
- 如果已经被使用,通过check_established继续检测是否可用,如果返回0则仍然可用;
- 如果未使用,则inet_bind_bucket_create 创建一个
- 如果没有找到端口,则返回EADDRNOTAVAIL,应用层提示 Cannot assign requested address
//检测是否和现有ESTABLISH状态连接冲突
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp)
{
//查找hash
struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
//查找四元组有没有一样的
sk_nulls_for_each(sk2, node, &head->chain) {
if (sk2->sk_hash != hash)
continue;
//对saddr,daddr,port进行了比较
if (likely(INET_MATCH(sk2, net, acookie,
saddr, daddr, ports, dif, sdif))) {
if (sk2->sk_state == TCP_TIME_WAIT) {
tw = inet_twsk(sk2);
if (twsk_unique(sk, sk2, twp))
break;
}
goto not_unique;
}
}
return 0;//有可用的
not_unique:
return -EADDRNOTAVAIL;//没有可用的
}
INET_MATCH源码如下,将__saddr,__daddr,__ports都进行了比较。如果匹配则端口不可以用,如果不匹配,端口可用。
#define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif, __sdif) \
(((__sk)->sk_portpair == (__ports)) && \
((__sk)->sk_daddr == (__saddr)) && \
((__sk)->sk_rcv_saddr == (__daddr)) && \
(((__sk)->sk_bound_dev_if == (__dif)) || \
((__sk)->sk_bound_dev_if == (__sdif))) && \
net_eq(sock_net(__sk), (__net)))
#endif /* 64-bit arch */
tcp_connect
/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
//申请skb
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
//添加到发送队列sk_write_queue
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);
//发送syn tcp_transmit_skb
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
//重启定时器
/* Timer for repeating the SYN until an answer. */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
//超时时间1s
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */
//初始化时 时间设置
void tcp_init_sock(struct sock *sk)
{
..
icsk->icsk_rto = TCP_TIMEOUT_INIT;
}
tcp_connect的作用
- 申请skb,并将其设置为SKB包
- 添加到发送队列上
- 调用tcp_transmit_skb将该包发送出去
- 重启定时器,超时后会重发
总结
- Cannot assign requested address 在端口没有找到可用的时候发出的错误;可用通过调整net.ipv4.ip_local_port_range参数,放开一些端口;
- 如果某些不想被使用的端口,内核参数ip_local_reserved_ports设置。
参考
https://www.kernel.org/doc/html/v5.6/