Linux TCP/IP 協議棧之 Socket的實現分析(Connect客戶端發起連接請求)


http://blog.chinaunix.net/uid-13746440-id-3076372.html

 

對於客戶端來說,當創建了一個套接字后,就可以連接它了。 
               case SYS_CONNECT: 
                        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); 
                        break; 
 
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;

        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;[/code] 校驗地址長度和協議簇。 
 
        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);[/code] 
        為 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. 如果超時還沒有應答,則使用超時重發定時器; 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM