连接过程
总的来说,基于 UDS 的连接过程比 inet 的 socket 连接过程要简单多了。客户端先创建一个自己用的 socket,然后调用 connect 来和服务器建立连接。
在 connect 的时候,会申请一个新 socket 给 server 端将来使用,和自己的 socket 建立好连接关系以后,就放到服务器正在监听的 socket 的接收队列中。 这个时候,服务器端通过 accept 就能获取到和客户端配好对的新 socket 了。
总的 UDS 的连接建立流程如下图。
我们找到 connect 函数的具体实现,unix_stream_connect。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26//file: net/unix/af_unix.c
static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;
...
// 1. 为服务器侧申请一个新的 socket 对象
newsk = unix_create1(sock_net(sk), NULL);
// 2. 申请一个 skb,并关联上 newsk
skb = sock_wmalloc(newsk, 1, 0, GFP_KERNEL);
...
// 3. 建立两个 sock 对象之间的连接
unix_peer(newsk) = sk;
newsk->sk_state = TCP_ESTABLISHED;
newsk->sk_type = sk->sk_type;
...
sk->sk_state = TCP_ESTABLISHED;
unix_peer(sk) = newsk;
// 4. 把连接中的一头(新 socket)放到服务器接收队列中
__skb_queue_tail(&other->sk_receive_queue, skb);
}
发送过程
看完了连接建立过程,我们再来看看基于 UDS 的数据的收发。这个收发过程一样也是非常的简单。发送方是直接将数据写到接收方的接收队列里的。
我们从 send 函数来看起。send 系统调用的源码位于文件 net/socket.c 中。在这个系统调用里,内部其实真正使用的是 sendto 系统调用。它只干了两件简单的事情,
第一是在内核中把真正的 socket 找出来,在这个对象里记录着各种协议栈的函数地址。 第二是构造一个 struct msghdr 对象,把用户传入的数据,比如 buffer地址、数据长度啥的,统统都装进去. 剩下的事情就交给下一层,协议栈里的函数 inet_sendmsg 了,其中 inet_sendmsg 函数的地址是通过 socket 内核对象里的 ops 成员找到的。大致流程如图。
在进入到协议栈 inet_sendmsg 以后,内核接着会找到 socket 上的具体协议发送函数。对于 Unix Domain Socket 来说,那就是 unix_stream_sendmsg。 我们来看一下这个函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//file:
static int unix_stream_sendmsg(struct kiocb *kiocb, struct socket *sock,
struct msghdr *msg, size_t len)
{
// 1.申请一块缓存区
skb = sock_alloc_send_skb(sk, size, msg->msg_flags&MSG_DONTWAIT,
&err);
// 2.拷贝用户数据到内核缓存区
err = memcpy_fromiovec(skb_put(skb, size), msg->msg_iov, size);
// 3. 查找socket peer
struct sock *other = NULL;
other = unix_peer(sk);
// 4.直接把 skb放到对端的接收队列中
skb_queue_tail(&other->sk_receive_queue, skb);
// 5.发送完毕回调
other->sk_data_ready(other, size);
}
和复杂的 TCP 发送接收过程相比,这里的发送逻辑简单简单到令人发指。申请一块内存(skb),把数据拷贝进去。根据 socket 对象找到另一端,直接把 skb 给放到对端的接收队列里了
接收函数主题是 unix_stream_recvmsg,这个函数中只需要访问它自己的接收队列就行了,源码就不展示了。所以在本机网络 IO 场景里,基于 Unix Domain Socket 的服务性能上肯定要好一些的。
抓包分析
做了两次代理,先将socket映射到8089端口上,然后再将8089端口映射到一个新的socket上,最后client访问这个新的socket。
实际流程如下: client -> 新socket -> 8089 -> 原来的socket 这个时候对8089可以任意抓包了。1
2
3
4yum install -y socat
socat TCP-LISTEN:8089,reuseaddr,fork UNIX-CONNECT:/es.sock.original
socat UNIX-LISTEN:/es.sock,fork TCP-CONNECT:127.0.0.1:8089
# 客户端再请求/es.sock就好了,然后抓包抓8089端口