<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, &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->ops->connect(sock, (struct sockaddr *) address, addrlen,
sock->file->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->sk;
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;
}
送出的協定簇不正确,則斷開連接配接。
switch (sock->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->sk_state != TCP_CLOSE)
goto out;
/*調用協定的連接配接函數*/
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err 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;
對于 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, &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;
if (addr_len return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;[/code] 校驗位址長度和協定簇。
nexthop = daddr = usin->sin_addr.s_addr;[/code]
将下一跳位址和目的位址的臨時變量都暫時設為使用者送出的位址。
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 return tmp;
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}[/code]
進行路由查找,并校驗傳回的路由的類型,TCP是不被允許使用多點傳播和廣播的。
if (!inet->opt || !inet->opt->srr)
daddr = rt->rt_dst;[/code]
更新目的位址臨時變量——使用路由查找後傳回的值。
if (!inet->saddr)
inet->saddr = rt->rt_src;
inet->rcv_saddr = inet->saddr;[/code]
如果還沒有設定源位址,和本地發送位址,則使用路由中傳回的值。
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);
/* 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;
return 0;
tp_connect()函數用來根據 sk 中的資訊,建構一個完成的 syn 封包,并将它發送出去。
在分析 tcp棧的實作時再來分析它。
根據 TCP協定,接下來的問題是,
1. 可能收到了伺服器的應答,則要回送一個 ack 封包;
2. 如果逾時還沒有應答,則使用逾時重發定時器;