
对于客户端来说,当创建了一个套接字后,就可以连接它了。
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 statethe 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、如果超时还没有应答,则使用超时重发定时器;
看情况1、采用TCP通信时,客户端不需要bind()他自己的IP和端口号,而服务器必须要bind()自己本机的IP和端口号
2、若采用UDP通信时(这里是有客户端和服务器之分才这么说的,若是指定特定端口的UDP对等通信则不一样了),客户端也可以不需要bind()他自己的IP和端口号,而服务器需要bind自己IP地址和端口号
socket - Linux 套接字接口本手册页描述了 Linux 网络套接字层用户接口。 套接字是用户进程和内核中网络协议栈之间的统一接口。 协议模块分为协议族(protocol families)(如 AF_INET、AF_IPX 和 AF_PACKET)和套接字类型(socket types)(如 SOCK_STREAM 或 SOCK_DGRAM)。 有关families和types的更多信息,请参阅 socket(2) 。
用户进程使用这些函数来发送或接收数据包以及执行其他套接字 *** 作。 有关更多信息,请参阅它们各自的手册页。
socket(2) 创建套接字,connect(2) 将套接字连接到远程套接字地址,bind(2) 函数将套接字绑定到本地套接字地址,listen(2) 告诉套接字应接受新连接, accept(2) 用于获取具有新传入连接的新套接字。 socketpair(2) 返回两个连接的匿名套接字(仅为少数本地families如 AF_UNIX 实现)
send(2)、sendto(2) 和sendmsg(2) 通过套接字发送数据,而recv(2)、recvfrom(2)、recvmsg(2) 从套接字接收数据。 poll(2) 和 select(2) 等待数据到达或准备好发送数据。 此外,还可以使用 write(2)、writev(2)、sendfile(2)、read(2) 和 readv(2) 等标准 I/O *** 作来读取和写入数据。
getsockname(2) 返回本地套接字地址, getpeername(2) 返回远程套接字地址。 getsockopt(2) 和 setsockopt(2) 用于设置或获取套接字层或协议选项。 ioctl(2) 可用于设置或读取一些其他选项。
close(2) 用于关闭套接字。 shutdown(2) 关闭全双工套接字连接的一部分。
套接字不支持使用非零位置查找或调用 pread(2) 或 pwrite(2)。
通过使用 fcntl(2) 在套接字文件描述符上设置 O_NONBLOCK 标志,可以在套接字上执行非阻塞 I/O。 然后所有会阻塞的 *** 作(通常)将返回 EAGAIN( *** 作应稍后重试); connect(2) 将返回 EINPROGRESS 错误。 然后用户可以通过 poll(2) 或 select(2) 等待各种事件。
如果不使用poll(2) 和 select(2) ,还让内核通过 SIGIO 信号通知应用程序有关事件的信息。 为此,必须通过 fcntl(2) 在套接字文件描述符上设置 O_ASYNC 标志,并且必须通过 sigaction(2) 安装有效的 SIGIO 信号处理程序。 请参阅下面的信号讨论。
每个套接字域(families)都有自己的套接字地址格式,具有特定于域的地址结构。 这些结构的首字段都是整数类型的“家族”字段(类型为 sa_family_t),即指出自己的套接字域或者说是protocol families。 这允许对所有套接字域可以使用统一的系统调用(例如,connect(2)、bind(2)、accept(2)、getsockname(2)、getpeername(2)),并通过套接字地址来确定特定的域。
为了允许将任何类型的套接字地址传递给套接字 API 中的接口,定义了类型 struct sockaddr。 这种类型的目的纯粹是为了允许将特定于域的套接字地址类型转换为“通用”类型,以避免编译器在调用套接字 API 时发出有关类型不匹配的警告。
struct sockaddr 以及在AF_INET常用的地址结构struct sockaddr_in如下所示,sockaddr_in.sin_zero是占位符:
此外,套接字 API 提供了数据类型 struct sockaddr_storage。 这种类型适合容纳所有支持的特定于域的套接字地址结构; 它足够大并且正确对齐。 (特别是它足够大,可以容纳 IPv6 套接字地址。)同struct sockaddr一样,该结构体包括以下字段,可用于标识实际存储在结构体中的套接字地址的类型: sa_family_t ss_family
sockaddr_storage 结构在必须以通用方式处理套接字地址的程序中很有用(例如,必须同时处理 IPv4 和 IPv6 套接字地址的程序)。
下面列出的套接字选项可以使用setsockopt(2) 设置并使用getsockopt(2) 读取。
当写入已关闭(由本地或远程端)的面向连接的套接字时,SIGPIPE 被发送到写入进程并返回 EPIPE。 当写调用指定 MSG_NOSIGNAL 标志时,不发送信号。
当使用 FIOSETOWN fcntl(2) 或 SIOCSPGRP ioctl(2) 请求时,会在 I/O 事件发生时发送 SIGIO。 可以在信号处理程序中使用 poll(2) 或 select(2) 来找出事件发生在哪个套接字上。 另一种方法(在 Linux 2.2 中)是使用 F_SETSIG fcntl(2) 设置实时信号; 实时信号的处理程序将使用其 siginfo_t 的 si_fd 字段中的文件描述符调用。 有关更多信息,请参阅 fcntl(2)。
在某些情况下(例如,多个进程访问单个套接字),当进程对信号做出反应时,导致 SIGIO 的条件可能已经消失。 如果发生这种情况,进程应该再次等待,因为 Linux 稍后会重新发送信号。
核心套接字网络参数可以通过目录 /proc/sys/net/core/ 中的文件访问。
These operations can be accessed using ioctl(2):
error = ioctl(ip_socket, ioctl_type, &value_result)
Valid fcntl(2) operations:
Linux assumes that half of the send/receive buffer is used for internal kernel structuresthus the values in the corresponding /proc files are twice what can be observed on the wire. Linux will allow port reuse only with the SO_REUSEADDR option when this option was set both in the previous program that performed a bind(2) to the port and in the program that wants to reuse the port. This differs from some implementations (e.g., FreeBSD) where only the later program needs to set the SO_REUSEADDR option. Typically this difference is invisible, since, for example, a server program is designed to always set this option.
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)