概述

使用UDP编写的一些流行的应用程序有:DNS(域名系统)、NFS(网络文件系统)和SNMP(简单网络管理协议)。

如下图所示,给出了典型的UDP客户/服务器程序的函数调用:

客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须作为参数指定目的地(即服务器)的地址。类似地,服务器不接受来自客户的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将与所接收的数据报一道返回客户的协议地址,因此服务器可以把响应发送给正确的客户。

recvfrom和sendto函数

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen); 两者均返回:读写字节数——成功,-1——出错

前三个参数:sockfd、buff和nbytes等同于read和write函数的三个参数:描述字、指向读入或写出缓冲区的指针和读写字节数。

暂时flags总是设置为0.

sendto的to参数指向一个含有数据报接收者的协议地址(例如IP地址和端口号)的套接口地址结构,其大小由addrlen参数指定。

recvfrom的from参数指向一个将由该函数在返回时填写数据报发送者的协议地址的套接口地址结构,而在该套接口地址结构中填写的字节数则放在addrlen参数所指的整数中返回给调用者。

注意,sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值-结果参数)。

recvfrom的最后两个参数类似于accept的最后两个参数:返回时其中套接口地址结构的内容告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。

sendto的最后两个参数类似于connect的最后两个参数:调用时其中套接口地址结构被我们填入数据报发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址。

这两个函数都把所读写数据的长度作为函数返回值。在recvfrom使用数据报协议的典型用途中,返回值就是所接收数据报中的用户数据量。

写一个长度为0的数据报是可行的。在UDP情况下,这导致一个只包含一个IP头部和一个UDP头部而没有数据的IP数据报。这也意味着对于数据报协议,recvfrom返回0值是可接受的:它并不像TCP套接口上read返回0值那样表示对端已关闭连接。

如果recvfrom的from参数是一个空指针,那么相应的长度参数(addrlen)也必须是一个空指针,表示我们并不关心数据发送者的地址。

UDP回射服务器程序:main函数

#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <strings.h> int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr, cliaddr; if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket");
exit(1);
} bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9877); if(bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("bind");
exit(1);
} dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}

UDP回射服务器程序:dg_echo函数

#include <sys/types.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <errno.h> void
dg_echo(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen)
{
int n;
socklen_t len;
char mesg[4096]; for(;;)
{
len = clilen;
if((n = recvfrom(sockfd, mesg, 4096, 0, pcliaddr, &len)) < 0)
{
perror("recvfrom");
exit(1);
}
if(sendto(sockfd, mesg, n, 0, pcliaddr, len) < 0)
{
perror("sendto");
exit(1);
}
}
}

 

此函数需考虑的细节:

(1)首先,该函数永不终止,因为UDP是一个无连接的协议,它没有像TCP中的EOF之类的东西。

(2)其次,该函数提供的是一个迭代服务器(iterative server),而不是像TCP服务器那样可以提供一个并发服务器。一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。

对于本套接口,UDP层中隐含有排队发生。事实上,每个UDP套接口都有一个接收缓冲区,到达该套接口的每个数据报都进入这个套接口缓冲区。当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO(先进先出)顺序返回给进程。这样,在进程能够读该套接口中任何已排好队的数据报之前,如果有多个数据报到达该套接口,那么相继到达的数据报仅仅加到该套接口的接收缓冲区中。然而这个缓冲区的大小是有限的。我们可以使用SO_RCVBUF套接口选项来增大它。

下图总结了TCP客户/服务器在两个客户与服务器建立连接时的情形:

如上图所示:服务器主机上有两个已连接套接口,其中每一个都有各自的套接口接收缓冲区。

下图展示了两个客户发送数据报到UDP服务器的情形:

如上图所示,其中只有一个服务器进程,它仅有的单个套接口用于接收所有到达的数据报并发回所有的响应。该套接口有一个接收缓冲区用来存放所有到达的数据报。

UDP回射客户程序:main函数

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>
#include <strings.h> int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr; if(argc != 2)
{
printf("usage: udpcli <IPaddress>");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr); sockfd = socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); exit(0);
}

UDP回射客户程序:dg_cli函数

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h> void
dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen)
{
int n;
char sendline[4096], recvline[4097]; while(fgets(sendline, 4096, fp) != NULL)
{
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); n = recvfrom(sockfd, recvline, 4096, 0, NULL, NULL); recvline[n] = 0; /* null terminate */
fputs(recvline, stdout);
}
}

对于一个UDP套接口,如果其进程首次调用sendto时它没有绑定一个本地端口,那么内核就在此时为它选择一个临时端口。跟TCP一样,客户可以显示地调用bind,不过很少这样做。

注意 调用recvfrom指定的第5和第6个参数是空指针。这 告知内核我们并不关心应答数据包由谁发送。这样做存在一个风险:任何进程(不论是在于本客户进程相同的主机上还是在不同的主机上)都可以向本客户的IP地址和端口发送数据报,这些数据报将被客户读入并认为是服务器的应答。

UDP程序例子小结

服务器可能想从到达的IP数据报上取得至少四条信息:源IP地址、宿IP地址、源端口号和宿端口号。下图给出了从TCP服务器或UDP服务器返回这些信息的函数调用。

TCP服务器总是能便捷地访问已连接套接口的所有这四条信息,而且这四个值在连接的整个生命周期内保持不变。然而对于UDP套接口,宿IP地址只能通过为IPv4设置IP_RECVDSTADDR套接口选项或为IPv6设置IPV6_PKTINFO套接口选项,然后调用recvmsg而不是recvfrom取得。由于UDP是无连接的,因此宿IP地址可随发送到服务器的每个数据报而改变。

UDP的connect调用

我们确实可以给UDP套接口调用connect,但这样做的结果与TCP连接完全不同:它没有三路握手过程。相反,内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号(取自传递给connect的套接口地址结构),然后立即返回到调用进程。

有了这个能力后,我们必须区分:

未连接UDP套接口(unconnected UDP socket):新创建的UDP套接口缺省如此;

已连接UDP套接口(connected UDP socket):对UDP套接口调用connect的结果。

对于已连接的UDP套接口,与缺省的未连接UDP套接口相比,发生了三个变化:

(1)我们再也不能给输出操作指定宿(目的)IP地址和端口号。也就是说,我们不使用sendto,而改用write或send。写到已连接UDP套接口上的任何内容都自动发送到由connect指定的协议地址(例如IP地址和端口号)。

(2)我们不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接口上由内核为输入操作返回的数据报仅仅是那些来自connect所指定协议地址的数据报。目的地为这个已连接UDP套接口的 本地协议地址(例如IP地址和端口号),发源地却不是该套接口早先connect到的协议地址的数据报,不会投递到该套接口。这样就限制了一个已连接UDP套接口能且仅能与一个对端交换数据报。

(3)由已连接UDP套接口引发的异步错误返回给它们所在的进程。相反,未连接的UDP套接口不接收任何异步错误。

我们可以说UDP客户进程或服务器进程仅仅在使用自己的UDP套接口与确定的唯一对端进行通信时,才可以调用connect。调用connect的通常是UDP客户,不过有些网络应用中的UDP服务器会与单个客户长时间通信(例如TFTP),这种情况下,客户和服务器都可调用connect。

DNS提供了另一个例子,如下图所示:

通常通过在/etc/resolv.conf文件中列出服务器主机的IP地址,一个DNS客户主机就能被配置成使用一个或多个DNS服务器。如果列出的是单个服务器主机(图中最左边的方框),客户进程就可以调用connect,但是如果列出的是多个服务器主机(图中从右边数第二个方框),客户进程就不能调用connect。另外DNS服务器进程通常处理任何客户请求,因此服务器进程不能调用connect。

给一个UDP套接口多次调用connect

拥有一个已连接UDP套接口的进程可以为下列目的之一再次调用connect:

(1)指定新的IP地址和端口号。对于TCP套接口,connect只能调用一次。

(2)断开套接口。再次调用connect时把套接口地址结构的地址组成员设置为AF_UNSPEC。

性能

当应用进程在一个未连接的UDP套接口上调用sendto时,源自Berkeley的内核暂时连接该套接口,发送数据报,然后断开该连接。在一个未连接的UDP套接口上给两个数据报调用sendto函数于是涉及内核执行下列6个步骤:

(1)连接套接口;

(2)输出第一个数据报;

(3)断开套接口连接;

(4)连接套接口;

(5)输出第二个数据报;

(6)断开套接口连接。

当应用进程知道自己要给同一宿地址发送多个数据报时,显式连接套接口效率更高。调用connect后调用两次write涉及内核执行如下步骤:

(1)连接套接口;

(2)输出第一个数据报;

(3)输出第二个数据报。

UNIX网络编程读书笔记:基本UDP套接口编程的更多相关文章

  1. UNIX网络编程读书笔记:原始套接口

    概述 应用程序可以绕过传输层而直接使用IPv4和IPv6,这称为原始套接口(raw socket).http://www.cnblogs.com/nufangrensheng/p/3583435.ht ...

  2. UDP套接口编程

    常用的UDP实现的程序:DNS域名系统,NFS网络文件系统,SNMP简单网络管理协议 ssize_t recvfrom(int sockfd,void *buff,size_t nbytes,int ...

  3. Python 网路编程读书笔记x UDP

    UDP 协议基础 在IP网络层,所有的数据包会向一个指定的主机传输 Source IP  -> Destination IP 但是两台机器之间可能有许多独立的应用需要进行通信,因此为了区分不同的 ...

  4. UNIX网络编程读书笔记:套接口选项

    概述 有很多方法来获取和设置影响套接口的选项: getsockopt和setsockopt函数 fcntl函数 ioctl函数 getsockopt和setsockopt函数 这两个函数仅用于套接口. ...

  5. UNIX网络编程读书笔记:基本SCTP套接口编程

    概述 SCTP是一个较新的传输协议,于2000年在IETF得到标准化(TCP是在1981年标准化的).它最初是为满足不断增长的IP电话市场设计的:具体地说,就是穿越因特网传输电话信令. SCTP是一个 ...

  6. UNIX网络编程读书笔记:TCP输出、UDP输出和SCTP输出

    TCP输出 下图展示了应用进程写数据到TCP套接口的过程. 每一个TCP套接口有一个发送缓冲区,我们可以用SO_SNDBUF套接口选项来改变这个缓冲区的大小. 当应用进程调用write时,内核从应用进 ...

  7. UNIX网络编程读书笔记:简介

    认知套接口编程接口 理解原始套接口(raw socket)的概念   值得注意的是,客户和服务器是典型的用户进程,而TCP和IP协议则通常是系统内核协议栈的一部分. 上图中在TCP和UDP之间留有间隙 ...

  8. UNIX网络编程读书笔记:UNIX域协议

    概述 UNIX域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API与在不同主机上执行客户/服务器通信所用的API(套接口API)相同.UNIX域协议可视为进程间通信 ...

  9. UNIX网络编程读书笔记:select函数

    select函数概况: select函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它. 作为一个例子,我们可以调用select,告知内核仅在下 ...

随机推荐

  1. 【BZOJ 4662】 4662: Snow (线段树+并查集)

    4662: Snow Time Limit: 40 Sec  Memory Limit: 256 MBSubmit: 136  Solved: 47 Description 2333年的某一天,临冬突 ...

  2. [SPOJ SEQN] [hdu3439]Sequence

    题目就是求C(n,k)*H(n - k)%m 0<= k<= n <=10^9, 1 <= m <= 10^5, n != 0 其中H(n)是错排第n项. 对于C(n,k ...

  3. 「NOI2017」游戏

    「NOI2017」游戏 题目描述 小 L 计划进行 \(n\) 场游戏,每场游戏使用一张地图,小 L 会选择一辆车在该地图上完成游戏. 小 L 的赛车有三辆,分别用大写字母 \(A\).\(B\).\ ...

  4. 【LCA/tarjan】POJ1470-Closest Common Ancestors

    [题意] 给出一棵树和多组查询,求以每个节点为LCA的查询数有多少? [错误点] ①读入的时候,注意它的空格是随意的呀!一开始不知道怎么弄,后来看了DISCUSS区大神的话: 询问部分输入: scan ...

  5. 20162304 实验一《Java开发环境的熟悉》实验报告

    Linux基础与Java开发环境 实验内容 1.熟悉Linux基础操作: 2.使用JDK编译.运行简单的Java程序: 实验要求 1.学习<Linux基础入门(新版)> 2.完成实验.撰写 ...

  6. Mysql插入数据时,报错this is incompatible with sql_mode=only_full_group_by

    Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated column 'inform ...

  7. jdk1.6,jdk1.7和jdk1.8多版本切换

    目录 一.配置jdk1.7(win7系统) 二.jdk1.8切换回jdk1.7 三.最后强调可能出现不成功的原因 首先要明确jdk一般都是默认安装在C:\Program Files\Java下的,在配 ...

  8. C++继承引入的隐藏与重写

    在区分隐藏和重写之前,先来理一理关于继承的东西... [继承] 继承是面向对象复用的重要手段,是类型之间的关系建模.通过继承一个类,共享公有的东西,实现各自本质不同的东西.简单的说,继承就是指一个对象 ...

  9. layoutit note

    Head: JavaScript -> Navbar Menu: JavaScript  -> Collapse Compnents -> Panels Compnents -> ...

  10. 用C++/CLI搭建C++和C#之间的桥梁(四)—— 网络资源

    关于C++/CLI的基础,我前面已经写过了几篇文章介绍过一些了,不过这些基本上都是管中窥豹,如果要详细了解C++/CLI,MSDN无疑是最好的教程. 使用 C++ 互操作(隐式 PInvoke) Vi ...