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. 如果超時還沒有應答,則使用超時重發定時器;