0%

tcp udp send流程分析

tips

net/ipv4/af_inet.c 查看:inet_stream_ops 和 inet_dgram_ops

TCP和UDP

TCP(传输控制协议)和UDP(用户数据报协议)是网络体系结构TCP/IP模型中传输层一层中的两个不同的通信协议。

  1. TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流,TCP套接口是字节流套接口(stream socket)的一种。
  2. UDP:用户数据报协议。UDP是一种无连接协议。UDP套接口是数据报套接口(datagram socket)的一种。

TCP Client-Server框架

TCP

服务器程序流程:

  1. 程序初始化
  2. 填写本机地址信息
  3. 绑定并监听一个固定的端口
  4. 收到Client的连接后建立一个socket连接
  5. 产生一个新的进程与Client进行通信和信息处理
  6. 子通信结束后中断与Client的连接

客户端程序流程:

  1. 程序初始化
  2. 填写服务器地址信息
  3. 连接服务器
  4. 与服务器通信和信息处理
  5. 通信结束后断开连接

UDP Client-Server框架

UDP

服务器程序流程:

  1. 程序初始化

  2. 填写本机地址信息

  3. 绑定一个固定的端口

  4. 收到Client的数据报后进行处理与通信

  5. 通信结束后断开连接

    客户端程序流程:

  6. 程序初始化

  7. 填写服务器地址信息

  8. 与服务器通信和信息处理

  9. 通信结束后断开连接

Socket编程

Socket接口是TCP/IP网络的API,
网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)数据报式Socket(SOCK_DGRAM)

  1. 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用
  2. 数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用

创建套接字

int socket(int domain, int type, int protocol);

建立地址和套接字的联系

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

服务器端侦听客户端的请求

int listen(int sockfd, int backlog);

建立服务器/客户端的连接 (面向连接TCP)

//客户端请求连接 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//服务器端从编号为Sockid的Socket上接收客户连接请求 
newsockid=accept(Sockid,Clientaddr, paddrlen)

发送/接收数据

//面向连接:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

//面向无连接:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

释放套接字

int close(int fd);

区别

  1. socket()的参数不同
  2. UDP Server不需要调用listen和accept
  3. UDP收发数据用sendto/recvfrom函数
  4. TCP:地址信息在connect/accept时确定
  5. UDP:在sendto/recvfrom函数中每次均需指定地址信息(客户端调用connect之后,不需要每次指定)
  6. UDP:shutdown函数无效

socket插入内核hash表

根据服务器和客户端的行为不同,bind()和sendto()都会调用到get_port(),
也就是说,在bind()或sendto()调用时,socket才被插入到内核表中。

bind() 绑定地址

sys_bind -> sock->ops->bind -> inet_bind -> sk->sk_prot->get_port

sk->sk_prot是udp_prot,这里实际调用udp_v4_get_port()函数。

sendto() 发送到指定地址

sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg

由于创建的是udp socket,因此sock->ops指向inet_dgram_ops,sendmsg()实际调用inet_sendmsg()函数。该函数中的有如下语句:

if (!inet_sk(sk)->inet_num && inet_autobind(sk))  
    return -EAGAIN;  

客户端在执行sendto()前仅仅执行了socket()操作,
此时inet_num=0,因此执行了inet_autobind(),
该函数会调用sk->sk_prot->get_port()。
从而回到了udp_v4_get_port()函数,它会将sk插入到内核表udp_table中。

UDP调用connect

标准的udp客户端开了套接口后,一般使用sendto和recvfrom函数来发数据,
udp发送数据有两种方法供大家选用的:

  1. socket–>sendto()或recvfrom()
  2. socket–>connect()–>send()或recv()

sendto和recvfrom在收发时指定地址,而send和recv则没有,地址是在connect指定的.

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

在udp编程中,如果你只往一个地址发送,那么你可以使用send和recv,
在使用它们之前用connect指定目的地址。connect函数在udp中就是这个作用,用它来检测udp端口的是否开放是没有用的。

udp中也有connect,只是它的connect不会进行三步握手,udp中调用connect时什么包也不发送。
调用connect是可选的,调用connect后就可以使用send、recv来进行UDP的收发包,而不必每次都要指定地址,
然后使用sendto、recvfrom进行操作,当然也可以调用sendto recvfrom。
没有调用connect那只能调用sendto、recvfrom,不可以调用send、recv。
调用sendto的时候第五个参数必须是NULL,第六个参数是0.
调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文.

UDP中使用connect可以提高效率.原因如下:

  1. 普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
  2. 采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询.

TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来.
UDP中可以多次调用connect,TCP只能调用一次connect. UDP多次调用connect有两种用途:

  1. 指定一个新的ip&port连结. 指定新连结,直接设置connect第二个参数即可.
  2. 断开和之前的ip&port的连结. 断开连结,需要将connect第二个参数中的sin_family设置成AF_UNSPEC即可.

UDP中使用connect的好处:

  1. 会提升效率
  2. 高并发服务中会增加系统稳定性

内核:发送时有两种调用方式:sys_send()和sys_sendto(),
两者的区别在于sys_sendto()需要给入目的地址的参数;
而sys_send()调用前需要调用sys_connect()来绑定目的地址信息;
两者的后续调用是相同的。如果调用sys_sendto()发送,
地址信息在sys_sendto()中从用户空间拷贝到内核空间,
而报文内容在udp_sendmsg()中从用户空间拷贝到内核空间。

TCP使用sendto

tcp_v4_connect函数代码片段:

/* 记录目的端口和目的IP */
inet->dport = usin->sin_port;
inet->daddr = daddr;

将目标地址及端口记录在inet中

同样的,TCP也可以调用sendto、recvfrom来完成数据的读写。Linux内核中send系统调用

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
    unsigned int, flags)
{
    return sys_sendto(fd, buff, len, flags, NULL, 0);
}

可以看到,send直接调用sendto,后面两个参数直接填NULL,0,
因此对于TCP调用来将,使用sendto,并将后两个参数置0,一样可以工作。

TCP/UDP Send流程代码分析

TCP调用sendto时没有给定地址,如何来完成TCP传输?sendto函数原型:

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
    unsigned, flags, struct sockaddr __user *, addr,
    int, addr_len)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err;
    struct msghdr msg;
    struct iovec iov;
    int fput_needed;

    if (len > INT_MAX)
        len = INT_MAX;
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    iov.iov_base = buff;
    iov.iov_len = len;
    msg.msg_name = NULL;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    msg.msg_namelen = 0;

    if (addr) {
        err = move_addr_to_kernel(addr, addr_len, (struct sockaddr *)&address);
        if (err < 0)
            goto out_put;
        msg.msg_name = (struct sockaddr *)&address;
        msg.msg_namelen = addr_len;
    }

    if (sock->file->f_flags & O_NONBLOCK)
        flags |= MSG_DONTWAIT;
    msg.msg_flags = flags;
    err = sock_sendmsg(sock, &msg, len);

    out_put:
        fput_light(sock->file, fput_needed);
    out:
        return err;
}                          

tips: 使用sock->file->f_flags & O_NONBLOCK 来检查是否是nonblock的传送。

sendto中首先调用函数move_addr_to_kernel,将地址copy进内核空间:

int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr *kaddr)                                                      
{
    if (ulen < 0 || ulen > sizeof(struct sockaddr_storage))
        return -EINVAL;
    if (ulen == 0)
        return 0;
    if (copy_from_user(kaddr, uaddr, ulen))
        return -EFAULT;
    return audit_sockaddr(ulen, kaddr);
}

对于TCP来说,传入参数分别为NULL,0,这部分不会执行到。

msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;

地址存入msg中,用于UDP连接

然后调用sock_sendmsg来完成数据发送,调用顺序:

sys_send -> sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg -> inet_sendmsg -> sk->sk_prot->sendmsg

static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,                                                          
        struct msghdr *msg, size_t size)
{
    struct sock_iocb *si = kiocb_to_siocb(iocb);
    int err;

    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;
    si->size = size;

    err = security_socket_sendmsg(sock, msg, size);
    if (err)
        return err;

    return sock->ops->sendmsg(iocb, sock, msg, size);
}

根据socket不同具体对应于tcp_sendmsg、udp_sendmsg

udp sendmsg

udp_sendmsg代码分析:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len);

struct inet_sock *inet = inet_sk(sk);
if (msg->msg_name) {
    struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
    if (msg->msg_namelen < sizeof(*usin))
        return -EINVAL;
    if (usin->sin_family != AF_INET) {
        if (usin->sin_family != AF_UNSPEC)
            return -EAFNOSUPPORT;
    }

    daddr = usin->sin_addr.s_addr;
    dport = usin->sin_port;
    if (dport == 0)
        return -EINVAL;
} else {
    if (sk->sk_state != TCP_ESTABLISHED)
        return -EDESTADDRREQ;
    daddr = inet->daddr;
    dport = inet->dport;
    /* Open fast path for connected socket.
       Route will not be used, if at least one option is set.
     */
    connected = 1;
}
ipc.addr = inet->saddr;

这段代码获取要发送数据的目的地址和端口号
一种情况是调用sendto()发送数据,此时目的的信息以参数传入,存储在msg->msg_name中,因此从中取出daddr和dport;
另一种情况是调用connect(), send()发送数据,在connect()调用时绑定了目的的信息,存储在inet中
并且由于是调用了connect(),sk->sk_state会设置为TCP_ESTABLISHED。
以后调用send()发送数据时,无需要再给入目的信息参数,因此从inet中取出dadr和dport。而connected表示了该socket是否已绑定目的。

tcp sendmsg

tcp_sendmsg代码分析,调用流程:

tcp_sendmsg -> tcp_push -> __tcp_push_pending_frames -> tcp_write_xmit -> tcp_transmit_skb

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
        gfp_t gfp_mask)
{
    struct inet_sock *inet;
    .
    .
    inet = inet_sk(sk);
    /* Build TCP header and checksum it. */
    th = tcp_hdr(skb);
    th->source      = inet->sport;
    th->dest        = inet->dport;
    th->seq         = htonl(tcb->seq);
    th->ack_seq     = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |
            tcb->flags);
    .
    .
}

tcp_sendmsg目标地址在函数tcp_transmit_skb中获取,获取的值是调用connect时存储在inet中!

  1. udp调用connect有什么作用
  2. UDP 的Connect函数的例子
  3. Linux内核分析 - 网络:UDP模块 - 收发
  4. Linux内核分析 - 网络:UDP模块 - socket
  5. linux-Tcp IP协议栈源码阅读笔记
  6. Linux TCP/IP源码分析 Connect
  7. Linux TCP/IP 协议栈源码分析