天天看點

Linux TCP/IP 協定棧之 Socket的實作分析(Connect用戶端發起連接配接請求)

<b>sys_connect</b>

對于用戶端來說,當建立了一個套接字後,就可以連接配接它了。

               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, &amp;err);

        if (!sock)

                goto out;

        err = move_addr_to_kernel(uservaddr, addrlen, address);

        if (err                 goto out_put;

        err = security_socket_connect(sock, (struct sockaddr *)address, addrlen);

        if (err)                 goto out_put;

        err = sock-&gt;ops-&gt;connect(sock, (struct sockaddr *) address, addrlen,

                                 sock-&gt;file-&gt;f_flags);

out_put:

        sockfd_put(sock);

out:

        return err;

}[/code]

跟其它操作類似,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-&gt;sk;

        long timeo;

        lock_sock(sk);

        if (uaddr-&gt;sa_family == AF_UNSPEC) {

                err = sk-&gt;sk_prot-&gt;disconnect(sk, flags);

                sock-&gt;state = err ? SS_DISCONNECTING : SS_UNCONNECTED;

        }

送出的協定簇不正确,則斷開連接配接。

switch (sock-&gt;state) {

        default:

                err = -EINVAL;

        case SS_CONNECTED:

                err = -EISCONN;

        case SS_CONNECTING:

                err = -EALREADY;

                /* Fall out of switch with err, set for this state */

                break;

        socket 處于不正确的連接配接狀态,傳回相應的錯誤值。

        case SS_UNCONNECTED:

                if (sk-&gt;sk_state != TCP_CLOSE)

                        goto out;

                /*調用協定的連接配接函數*/

                err = sk-&gt;sk_prot-&gt;connect(sk, uaddr, addr_len);

                if (err                         goto out;

                /*協定方面的工作已經處理完成了,但是自己的一切工作還沒有完成,是以切換至正在連接配接中*/

                 sock-&gt;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;

對于 TCP的實際的連接配接,是通過調用 tcp_v4_connect()函數來實作的。

<b>tcp_v4_connect函數</b>

對于 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 } } };

        if (!dst || !src) {

                err = __ip_route_output_key(rp, &amp;fl);

                if (err)

                        return err;

                fl.fl4_dst = (*rp)-&gt;rt_dst;

                fl.fl4_src = (*rp)-&gt;rt_src;

                ip_rt_put(*rp);

                *rp = NULL;

        return ip_route_output_flow(rp, &amp;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;

        if (addr_len                 return -EINVAL;

        if (usin-&gt;sin_family != AF_INET)

                return -EAFNOSUPPORT;[/code] 校驗位址長度和協定簇。

        nexthop = daddr = usin-&gt;sin_addr.s_addr;[/code]

        将下一跳位址和目的位址的臨時變量都暫時設為使用者送出的位址。

        if (inet-&gt;opt &amp;&amp; inet-&gt;opt-&gt;srr) {

                if (!daddr)

                        return -EINVAL;

                nexthop = inet-&gt;opt-&gt;faddr;

        如果使用了來源位址路由,選擇一個合适的下一跳位址。

        tmp = ip_route_connect(&amp;rt, nexthop, inet-&gt;saddr,

                               RT_CONN_FLAGS(sk), sk-&gt;sk_bound_dev_if,

                               IPPROTO_TCP,

                               inet-&gt;sport, usin-&gt;sin_port, sk);

        if (tmp                 return tmp;

        if (rt-&gt;rt_flags &amp; (RTCF_MULTICAST | RTCF_BROADCAST)) {

                ip_rt_put(rt);

                return -ENETUNREACH;

        }[/code]

        進行路由查找,并校驗傳回的路由的類型,TCP是不被允許使用多點傳播和廣播的。

        if (!inet-&gt;opt || !inet-&gt;opt-&gt;srr)

                daddr = rt-&gt;rt_dst;[/code]

        更新目的位址臨時變量——使用路由查找後傳回的值。

        if (!inet-&gt;saddr)

                inet-&gt;saddr = rt-&gt;rt_src;

        inet-&gt;rcv_saddr = inet-&gt;saddr;[/code]

        如果還沒有設定源位址,和本地發送位址,則使用路由中傳回的值。

        if (tp-&gt;rx_opt.ts_recent_stamp &amp;&amp; inet-&gt;daddr != daddr) {

                /* Reset inherited state */

                tp-&gt;rx_opt.ts_recent           = 0;

                tp-&gt;rx_opt.ts_recent_stamp = 0;

                tp-&gt;write_seq                   = 0;

        if (sysctl_tcp_tw_recycle &amp;&amp;

            !tp-&gt;rx_opt.ts_recent_stamp &amp;&amp; rt-&gt;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 &amp;&amp; peer-&gt;tcp_ts_stamp + TCP_PAWS_MSL &gt;= xtime.tv_sec) {

                        tp-&gt;rx_opt.ts_recent_stamp = peer-&gt;tcp_ts_stamp;

                        tp-&gt;rx_opt.ts_recent = peer-&gt;tcp_ts;

                }

        這個更新初始狀态方面的内容,還沒有去分析它。

        inet-&gt;dport = usin-&gt;sin_port;

        inet-&gt;daddr = daddr;

        儲存目的位址及端口。

        tp-&gt;ext_header_len = 0;

        if (inet-&gt;opt)

                tp-&gt;ext_header_len = inet-&gt;opt-&gt;optlen;

        tp-&gt;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(&amp;rt, inet-&gt;sport, inet-&gt;dport, sk);

        /* OK, now commit destination to socket.  */

        __sk_dst_set(sk, &amp;rt-&gt;u.dst);

        tcp_v4_setup_caps(sk, &amp;rt-&gt;u.dst);

        因為本地端口已經改變,使用新端口,重新查找路由,并用新的路由緩存項更新 sk 中儲存的路由緩存項。

        if (!tp-&gt;write_seq)

tp-&gt;write_seq =

secure_tcp_sequence_number(inet-&gt;saddr,                                                           

inet-&gt;daddr,

                                                           inet-&gt;sport,

                                                           usin-&gt;sin_port);[/code]

        為 TCP封包計算一個 seq值(實際使用的值是 tp-&gt;write_seq+1)。

        inet-&gt;id = tp-&gt;write_seq ^ jiffies;

        err = tcp_connect(sk);

        rt = NULL;

        return 0;

        tp_connect()函數用來根據 sk 中的資訊,建構一個完成的 syn 封包,并将它發送出去。

        在分析 tcp棧的實作時再來分析它。

根據 TCP協定,接下來的問題是,

1. 可能收到了伺服器的應答,則要回送一個 ack 封包;

2. 如果逾時還沒有應答,則使用逾時重發定時器;

繼續閱讀