前面介绍的函数基本上都是TCP协议的,如listen,connect,accept 等函数,这都是为可靠传输协议TCP定制的。对于另一个不可靠udp协议(通信系统其可靠性交由上层应用层负责),则主要由两个函数完成,sendto 和 recvfrom 函数。这里先介绍 sendto 函数。
说明:sendto 和 recvfrom 函数不限于udp协议,这里只是udp协议当中是采用这两个函数实现的,所以就放在udp协议中介绍。
对于 udp 协议的介绍和编程实现请参考下文:UDP 客户/服务器简单 Socket 程序
简要介绍下UDP数据报格式,相比TCP数据报格式,实在是简洁不少。
上面的各个字段含义一目了然(上面是16是表示该字段占16bit,udp头部占8字节),其中长度指的是此 UDP 数据报的长度(包括 UDP 数据报头部和 “数据” 部分)。
一、应用层——sendto 函数
[cpp] view plain copy print?
- #include <sys/socket.h>
- ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags,
- const struct sockaddr *to, socklen_t *addrlen);
- //若成功则返回写的字节数,出错则返回-1
- /*参数解析
- 前面三个参数分别表示:套接字描述符,指向写出缓冲区的指针和写字节数。
- to:指向一个含有数据报接收者的协议地址(如IP地址和端口号)的套接字地址结构,其大小由addrlen参数指定
- */
该函数的作用是:向指定端口发送给定地址中的指定大小数据(如客户端sockfd,向 to 指定的远端套接字发送buff 缓冲区内nbytes 个字节数据)
二、BSD Socket层——sock_sendto 函数
[cpp] view plain copy print?
- /*
- * Send a datagram to a given address. We move the address into kernel
- * space and check the user space data area is readable before invoking
- * the protocol.
- */
- //发送数据给指定的远端地址,主要用于UDP协议
- //前面三个参数分别表示套接口描述字、指向缓冲区的指针和读写字节数
- //addr指向一个含有数据包接收者的协议地址(含ip地址和端口号)的套接口地址结构
- //其大小由addr_len参数指定
- //该函数的作用就是向指定地址的远端发送数据包:将buff缓冲区中len大小的数据发送给addr指定的远端套接字
- static int sock_sendto(int fd, void * buff, int len, unsigned flags,
- struct sockaddr *addr, int addr_len)
- {
- struct socket *sock;
- struct file *file;
- char address[MAX_SOCK_ADDR];
- int err;
- //参数有效性检查
- if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL))
- return(-EBADF);
- //找到给定文件描述符对应的socket结构
- if (!(sock = sockfd_lookup(fd, NULL)))
- return(-ENOTSOCK);
- if(len<0)
- return -EINVAL;
- //检查权限,buff中len个字节区域是否可读
- err=verify_area(VERIFY_READ,buff,len);
- if(err)
- return err;
- //从addr拷贝addr_len大小的数据到address
- if((err=move_addr_to_kernel(addr,addr_len,address))<0)
- return err;
- //调用下层函数sendto,inet域为inet_sendto函数
- return(sock->ops->sendto(sock, buff, len, (file->f_flags & O_NONBLOCK),
- flags, (struct sockaddr *)address, addr_len));
- }
三、INET Socket层——inet_sendto 函数
[cpp] view plain copy print?
- //INET socket层
- tatic int inet_sendto(struct socket *sock, void *ubuf, int size, int noblock,
- unsigned flags, struct sockaddr *sin, int addr_len)
- //得到socket对应的sock结构
- struct sock *sk = (struct sock *) sock->data;
- //判断该套接字的有效性,是否处于关闭状态(半关闭)
- if (sk->shutdown & SEND_SHUTDOWN)
- {
- send_sig(SIGPIPE, current, 1);
- return(-EPIPE);
- }
- if (sk->prot->sendto == NULL)
- return(-EOPNOTSUPP);
- if(sk->err)
- return inet_error(sk);
- /* We may need to bind the socket. */
- //自动绑定一个本地端口号
- if(inet_autobind(sk)!=0)
- return -EAGAIN;
- //调用下层传输层函数udp_sendto函数
- return(sk->prot->sendto(sk, (unsigned char *) ubuf, size, noblock, flags,
- (struct sockaddr_in *)sin, addr_len));
四、传输层
udp_sento 函数
[cpp] view plain copy print?
- static int udp_sendto(struct sock *sk, unsigned char *from, int len, int noblock,
- unsigned flags, struct sockaddr_in *usin, int addr_len)
- {
- struct sockaddr_in sin;
- int tmp;
- /*
- * Check the flags. We support no flags for UDP sending
- */
- //udp除了MSG_DONTROUTE外,不支持任何其他标志位
- if (flags&~MSG_DONTROUTE)
- return(-EINVAL);
- /*
- * Get and verify the address.
- */
- //对远端地址的合法性检查,由于不涉及网络数据传送,所以无法验证这个地址存在性
- if (usin)
- {
- //如果明确指定远端地址,就直接检查该地址的有效性
- if (addr_len < sizeof(sin)) //大小
- return(-EINVAL);
- memcpy(&sin,usin,sizeof(sin));
- if (sin.sin_family && sin.sin_family != AF_INET) //本地地址有效性
- return(-EINVAL);
- if (sin.sin_port == 0) //端口号有效性
- return(-EINVAL);
- }
- else
- {
- //如果没有明确指定远端地址,则检查之前是否调用了connect函数进行了地址绑定
- if (sk->state != TCP_ESTABLISHED)
- return(-EINVAL);
- //如果进行了绑定,则将远端地址设置为这个绑定的地址
- sin.sin_family = AF_INET;
- sin.sin_port = sk->dummy_th.dest;
- sin.sin_addr.s_addr = sk->daddr;
- }
- /*
- * BSD socket semantics. You must set SO_BROADCAST to permit
- * broadcasting of data.
- */
- //处理尚未指定本地地址的情况
- if(sin.sin_addr.s_addr==INADDR_ANY)
- sin.sin_addr.s_addr=ip_my_addr();
- //处理广播的情况
- if(!sk->broadcast && ip_chk_addr(sin.sin_addr.s_addr)==IS_BROADCAST)
- return -EACCES; /* Must turn broadcast on first */
- sk->inuse = 1;//加锁
- /* Send the packet. */
- //转调用udp_send函数
- tmp = udp_send(sk, &sin, from, len, flags);
- /* The datagram has been sent off. Release the socket. */
- //数据包以发送,释放该套接字,前面介绍到这个函数的两个功能
- //取决于sk_dead字段是否设置
- release_sock(sk);
- return(tmp);
- }
udp_send 函数
[cpp] view plain copy print?
- //根据被调用出清楚参数情况
- static int udp_send(struct sock *sk, struct sockaddr_in *sin,
- unsigned char *from, int len, int rt)
- {
- struct sk_buff *skb;
- struct device *dev;
- struct udphdr *uh;
- unsigned char *buff;
- unsigned long saddr;
- int size, tmp;
- int ttl;
- /*
- * Allocate an sk_buff copy of the packet.
- */
- //计算所需要分配的封装数据的缓冲区大小
- size = sk->prot->max_header + len;
- //分配指定大小的sk_buff 结构用于封装数据
- skb = sock_alloc_send_skb(sk, size, 0, &tmp);
- if (skb == NULL)
- return tmp;
- skb->sk = NULL; /* to avoid changing sk->saddr */
- skb->free = 1;//发送完后数据包立即释放,udp不提供超时重传
- skb->localroute = sk->localroute|(rt&MSG_DONTROUTE);//指定路由类型
- /*
- * Now build the IP and MAC header.
- */
- buff = skb->data;//udp首部和有效负载
- saddr = sk->saddr;//本地地址
- dev = NULL;
- ttl = sk->ip_ttl;
- #ifdef CONFIG_IP_MULTICAST
- //如果目的地址是多播,则设置TTL值为1,表示局限于本地网络,不可跨越路由器
- if (MULTICAST(sin->sin_addr.s_addr))
- ttl = sk->ip_mc_ttl;
- #endif
- //创建MAC首部和IP首部
- tmp = sk->prot->build_header(skb, saddr, sin->sin_addr.s_addr,
- &dev, IPPROTO_UDP, sk->opt, skb->mem_len,sk->ip_tos,ttl);
- skb->sk=sk;//关联 /* So memory is freed correctly */
- /*
- * Unable to put a header on the packet.
- */
- if (tmp < 0 ) //创建失败
- {
- sk->prot->wfree(sk, skb->mem_addr, skb->mem_len);
- return(tmp);
- }
- buff += tmp;//定位到udp首部位置
- saddr = skb->saddr; /*dev->pa_addr;*/
- //数据报sk_buff中挂载的数据部分长度:下面注释,len是有效数据负载长度
- skb->len = tmp + sizeof(struct udphdr) + len; /* len + UDP + IP + MAC */
- skb->dev = dev;//网络接口设备
- /*
- * Fill in the UDP header.
- */
- //udp首部字段的初始化
- uh = (struct udphdr *) buff;
- uh->len = htons(len + sizeof(struct udphdr));//长度字段
- uh->source = sk->dummy_th.source;//源端端口,sk中tcp首部字段
- uh->dest = sin->sin_port;//目的端口
- buff = (unsigned char *) (uh + 1);//定位到数据部分
- //MAC header | IP Header | UDP Header | Data
- //uh本身已经指向了udp首地址,uh+1,表示后移一个udp首部大小位置,定位到了数据负载
- /*
- * Copy the user data.
- */
- //从from拷贝len大小的数据到buff,即把应用层中待发送的缓冲区的数据拷贝到数据包的数据负载中
- //然后通过数据包整体打包发送出去。
- //就好比货物搭上了货轮开往目的地,为啥不是火车呢,因为火车线路已经固定好了,只能这么走。
- memcpy_fromfs(buff, from, len);
- /*
- * Set up the UDP checksum.
- */
- //同tcp,这里进行udp校验和检查
- udp_send_check(uh, saddr, sin->sin_addr.s_addr, skb->len - tmp, sk);
- /*
- * Send the datagram to the interface.
- */
- udp_statistics.UdpOutDatagrams++;
- //调用ip_queue_xmit函数将数据包发往网络层模块处理。以下处理就和TCP协议一样了,二者的差异只在于传输层
- //该函数以及更下层数据传送前面已经介绍,
- sk->prot->queue_xmit(sk, dev, skb, 1);
- return(len);
- }
关于ip_queue_xmit 函数的介绍以及更下层的数据传送,参见博文:【Linux 内核网络协议栈源码剖析】数据包发送
可以看出,udp是一种无连接传输层协议,不像tcp那样需要服务器监听,也不必等待客户端与服务器建立连接后才能通信,效率优于tcp协议,但udp则不能保证数据传输的可靠性。
udp 的数据传输,实现并不像tcp那样要建立一条数据传输通道,而是直接创建套接字后,直接传送数据到给定的远端(提供远端地址),数据传送过程无超时重传和序列号校验工作,适用于数据传输的连续性比数据的完整性更重要的场合,允许数据在传输过程中有部分丢失,如IP电话、流媒体通信等。