Linux网络协议栈 -- socket connect 发起连接请求

1、sys_connect
 
对于客户端来说,当创建了一个套接字后,就可以连接它了。 
                case SYS_CONNECT: 
                        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); 
                        break;[/code] 
 
asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) 

        struct socket *sock; 
        char address[MAX_SOCK_ADDR]; 
        int err; 
 
        sock = sockfd_lookup(fd, &err); 
        if (!sock) 
                goto out; 
        err = move_addr_to_kernel(uservaddr, addrlen, address); 
        if (err < 0) 
                goto out_put; 
 
        err = security_socket_connect(sock, (struct sockaddr *)address, addrlen); 
        if (err)                 goto out_put; 
 
        err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen, 
                                 sock->file->f_flags); 
out_put: 
        sockfd_put(sock); 
out: 
        return err; 
}
 
跟其它操作类似,sys_connect 接着调用 inet_connect: 
/* 
*        Connect to a remote host. There is regrettably still a little 
*        TCP 'magic' in here. 
*/ 
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, 
                        int addr_len, int flags) 

        struct sock *sk = sock->sk; 
        int err; 
        long timeo; 
 
        lock_sock(sk); 
 
        if (uaddr->sa_family == AF_UNSPEC) { 
                err = sk->sk_prot->disconnect(sk, flags); 
                sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED; 
                goto out; 
        }
 
提交的协议簇不正确,则断开连接。 
 
        switch (sock->state) { 
        default: 
                err = -EINVAL; 
                goto out; 
        case SS_CONNECTED: 
                err = -EISCONN; 
                goto out; 
        case SS_CONNECTING: 
                err = -EALREADY; 
                /* Fall out of switch with err, set for this state */ 
                break;[/code] socket 处于不正确的连接状态,返回相应的错误值。 
 
        case SS_UNCONNECTED: 
                err = -EISCONN; 
                if (sk->sk_state != TCP_CLOSE) 
                        goto out; 
                /*调用协议的连接函数*/ 
                err = sk->sk_prot->connect(sk, uaddr, addr_len); 
                if (err < 0) 
                        goto out; 
                /*协议方面的工作已经处理完成了,但是自己的一切工作还没有完成,所以切换至正在连接中*/ 
                  sock->state = SS_CONNECTING; 
 
                /* Just entered SS_CONNECTING state; the only 
                 * difference is that return value in non-blocking 
                 * case is EINPROGRESS, rather than EALREADY. 
                 */ 
                err = -EINPROGRESS; 
                break; 
        }
 
对于 TCP的实际的连接,是通过调用 tcp_v4_connect()函数来实现的。 
 
二、tcp_v4_connect函数
对于 TCP 协议来说,其连接,实际上就是发送一个 SYN 报文,在服务器的应到到来时,回答它一个 ack 报文,也就是完成三次握手中的第一和第三次。 
 
要发送 SYN 报文,也就是说,需要有完整的来源/目的地址,来源/目的端口,目的地址/端口由用户态提交,但是问题是没有自己的地址和端口,因为并没有调  用过 bind(2),一台主机,对于端口,可以像 sys_bind()那样,从本地未用端口中动态分配一个,那地址呢?因为一台主机可能会存在多个 IP地  址,如果随机动态选择,那么有可能选择一个错误的来源地址,将不能正确地到达目的地址。换句话说,来源地址的选择,是与路由相关的。 
 
调用路由查找的核心函数 ip_route_output_slow(),在没有提供来源地址的情况下,会根据实际情况,调用 inet_select_addr()函数来选择一个合适的。同时,如果路由查找命中,会生成一个相应的路由缓存项,这个缓存项,不但对当前发送 SYN 报  文有意义,对于后续的所有数据包,都可以起到一个加速路由查找的作用。这一任务,是通过 ip_route_connect()函数完成的,它返回相应的路由缓存项(也就是说,来源地址也在其中了): 
 
static inline int ip_route_connect(struct rtable **rp, u32 dst, 
                                   u32 src, u32 tos, int oif, u8 protocol, 
                                   u16 sport, u16 dport, struct sock *sk) 
{         struct flowi fl = { .oif = oif, 
                            .nl_u = { .ip4_u = { .daddr = dst, 
                                                 .saddr = src, 
                                                 .tos   = tos } }, 
                            .proto = protocol, 
                            .uli_u = { .ports = 
                                       { .sport = sport, 
                                         .dport = dport } } }; 
 
        int err; 
        if (!dst || !src) { 
                err = __ip_route_output_key(rp, &fl); 
                if (err) 
                        return err; 
                fl.fl4_dst = (*rp)->rt_dst; 
                fl.fl4_src = (*rp)->rt_src; 
                ip_rt_put(*rp); 
                *rp = NULL; 
        } 
        return ip_route_output_flow(rp, &fl, sk, 0); 
}
 
首先,构建一个搜索 key fl,在搜索要素中,来源地址/端口是不存在的。所以,当通过__ip_route_output_key 进行查找时,第一次是不会命中缓存的。 __ip_route_output_key 将继续调用ip_route_output_slow()函数,在路由表中搜索,并返回一个合适的来源地址,  并且生成一个路由缓存项。 路由查找的更多细节,我会在另一个贴子中来分析。 
 
/* This will initiate an outgoing connection. */ 
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) 

        struct inet_sock *inet = inet_sk(sk); 
        struct tcp_sock *tp = tcp_sk(sk); 
        struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; 
        struct rtable *rt; 
        u32 daddr, nexthop; 
        int tmp; 
        int err; 
 
        if (addr_len < sizeof(struct sockaddr_in)) 
                return -EINVAL; 
 
        if (usin->sin_family != AF_INET) 

                return -EAFNOSUPPORT;


校验地址长度和协议簇。 

nexthop = daddr = usin->sin_addr.s_addr;
将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。 
 
        if (inet->opt && inet->opt->srr) { 
                if (!daddr) 
                        return -EINVAL; 
                nexthop = inet->opt->faddr; 
        }
如果使用了来源地址路由,选择一个合适的下一跳地址。 
 
        tmp = ip_route_connect(&rt, nexthop, inet->saddr, 
                               RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, 
                               IPPROTO_TCP, 
                               inet->sport, usin->sin_port, sk); 
        if (tmp < 0) 
                return tmp; 
 
        if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { 
                ip_rt_put(rt); 
                return -ENETUNREACH; 
        }
进行路由查找,并校验返回的路由的类型,TCP是不被允许使用多播和广播的。 
 
       if (!inet->opt || !inet->opt->srr) 
                daddr = rt->rt_dst;
更新目的地址临时变量——使用路由查找后返回的值。 
 
       if (!inet->saddr) 
                inet->saddr = rt->rt_src; 
        inet->rcv_saddr = inet->saddr;
如果还没有设置源地址,和本地发送地址,则使用路由中返回的值。 
 
       if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) { 
                /* Reset inherited state */ 
                tp->rx_opt.ts_recent           = 0; 
                tp->rx_opt.ts_recent_stamp = 0; 
                tp->write_seq                   = 0; 
        } 
 
        if (sysctl_tcp_tw_recycle && 
            !tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) { 
                struct inet_peer *peer = rt_get_peer(rt);  
                /* VJ's idea. We save last timestamp seen from 
                 * the destination in peer table, when entering state TIME-WAIT 
                 * and initialize rx_opt.ts_recent from it, when trying new connection. 
                 */ 
 
                if (peer && peer->tcp_ts_stamp + TCP_PAWS_MSL >= xtime.tv_sec) { 
                        tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp; 
                        tp->rx_opt.ts_recent = peer->tcp_ts; 
                } 
        }
这个更新初始状态方面的内容,还没有去分析它。 
 
        inet->dport = usin->sin_port; 
        inet->daddr = daddr;
保存目的地址及端口。 
 
        tp->ext_header_len = 0; 
        if (inet->opt) 
                tp->ext_header_len = inet->opt->optlen;
 
tp->rx_opt.mss_clamp = 536;
设置最小允许的 mss 值 

tcp_set_state(sk, TCP_SYN_SENT);
套接字状态被置为 TCP_SYN_SENT, 
 
        err = tcp_v4_hash_connect(sk); 
        if (err) 
                goto failure;
动态选择一个本地端口,并加入 hash 表,与bind(2)选择端口类似。 
 
        err = ip_route_newports(&rt, inet->sport, inet->dport, sk); 
        if (err) 
                goto failure; 
 
        /* OK, now commit destination to socket.  */ 
        __sk_dst_set(sk, &rt->u.dst); 
        tcp_v4_setup_caps(sk, &rt->u.dst);
因为本地端口已经改变,使用新端口,重新查找路由,并用新的路由缓存项更新 sk 中保存的路由缓存项。 
 
       if (!tp->write_seq) 

                tp->write_seq = secure_tcp_sequence_number(inet->saddr,  

                                                           inet->daddr, 

                                                           inet->sport, 
                                                           usin->sin_port);
为 TCP报文计算一个 seq值(实际使用的值是 tp->write_seq+1)。 
 
inet->id = tp->write_seq ^ jiffies; 
 
        err = tcp_connect(sk); 
        rt = NULL; 
        if (err) 
                goto failure; 
 

        return 0;


tp_connect()函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。在分析 tcp栈的实现时再来分析它。 
 
根据 TCP协议,接下来的问题是, 
1、可能收到了服务器的应答,则要回送一个 ack 报文; 
2、如果超时还没有应答,则使用超时重发定时器; 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值