第8章 基本 UDP 套接字编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lc250123/article/details/81660185

目录

概述

recvfrom、sendto 函数

UDP 回射服务器程序

UDP 回射客户端程序

UDP 客户-服务器程序小节

UDP 的 connect 函数

UDP 缺乏流量控制

UDP 中的外出接口的确定

使用 select 函数的 UDP 回射服务器程序


概述

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

客户不与服务器建立连接,而是只管使用 sendto 函数给服务器发送数据报,其中必须指定目的地的地址作为参数。服务器不接受来自客户的连接,而只管调用 recvfrom 函数,等待来自某个客户的数据到达。


recvfrom、sendto 函数

这两个函数类似于 read 和 write

       #include <sys/socket.h>

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

返回:成功返回读或写的字节数,出错返回 -1。

sockfd:描述符,buf:读或写缓冲区指针,len:读或写字节数,flags:暂且置为 0,后两个参数为地址和地址长度。

写一个长度为 0 的数据报是可行的。在 UDP 情况下,这会形成一个只包含一个 IP 首部和一个 8 字节 UDP 首部而没有数据的 IP 数据报。这也意味着对于数据报协议,recvfrom 返回 0 值是可接受的:它并不像 TCP 套接字上 read 返回 0 值那样表示对端已关闭连接。既然 UDP 是无连接的,因此也就没有诸如关闭一个 UDP 连接之类事情。如果 recvfrom 的 src_addr 参数为一个空指针,那么相应的长度参数(addrlen)也必须是一个空指针,表示我们并不关心数据发送者的协议地址。


UDP 回射服务器程序

程序模型如下

#include	"unp.h"

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{
	int		n;
	socklen_t	len;
	char		mesg[MAXLINE];

	for ( ; ; ) {
		len = clilen;
		Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
		Sendto(sockfd, mesg, n, 0, pcliaddr, len);
	}
}

int main(int argc, char **argv)
{
	int			sockfd;
	struct sockaddr_in	servaddr, cliaddr;

	sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));
	dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
}

一般来说,大多数 TCP 服务器是并发的,而大多数 UDP 服务器是迭代的。TCP 服务器调用 fork 为每个客户建立一个处理进程,而 UDP 往往是单个服务器就得处理所有客户。事实上每个 UDP 套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区。当进程调用 recvfrom 时,缓冲区中的下一个数据报以 FIFO 顺序返回给进程。这些数据报会在缓冲区中排好队。缓冲区大小是有限制的,可以通过 SO_RCVBUF 选项修改。这个例程中的服务进程仅有一个套接字,它要接收所有客户的数据报,而所有这些数据报都放在一个接收缓冲区中。


UDP 回射客户端程序

#include	"unp.h"

#define SERV_PORT 7

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int			n;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	socklen_t		len;
	struct sockaddr	*       preply_addr;

	preply_addr = Malloc(servlen);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		len = servlen;
		n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
		if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {
			printf("reply from %s (ignored)\n",
					Sock_ntop(preply_addr, len));
			continue;
		}

		recvline[n] = 0;	/* null terminate */
		Fputs(recvline, stdout);
	}
}

int main(int argc, char **argv)
{
	int			sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: udpcli <IPaddress>");

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

	dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));

	exit(0);
}

如果进程首次调用 sendto 时它没有绑定一个本地端口,那么内核就在此时为它选择一个临时端口。recvfrom 的后两个参数是 NULL 表示我们不关心应答数据报由谁来发送,但是会有风险:任何进程都可向本客户的 IP 地址和端口发送数据报。本例程加了个验证,即 memcmp 接收套接字是否和 server 套接字相同。但是如果服务器有两个 IP 地址(这是可能的),那么回复消息的 IP 是另外一个 IP 的话,那么 memcmp 就返回非 0。所以本例程是有局限性的。

解决服务器有多个 IP 地址的方法是:

  • 如果是客户端的话,得到由 recvfrom 返回的 IP 地址后,客户通过在 DNS 中查找服务器主机名来验证该主机的域名,而不是 IP 地址。
  • 在 UDP 服务器上的话,给每个 IP 创建一个套接字,用 bind 捆绑每个 IP 到其套接字上,然后在所有套接字上使用 select 等待任何一个变得可读,再从可读的套接字给出应答。

    UDP 客户-服务器程序小节


UDP 的 connect 函数

异步错误:由于 UDP 不是面向连接的,所以我们可以向一个没有开启服务的服务器发送消息,而且 sendto 会成功返回,但是随后才能得到一个 ICMP 的 “port unreachable” 的错误,这就是异步错误。

一个基本规则是:对于一个 UDP 套接字,由它引发的异步错误却并不返回给它,除非它已连接。

UDP 的 connect 函数不会有三路握手过程。内核只是检查是否存在立即可知的错误(如一个显然不可到达的目的地),记录对端的 IP 地址和端口号(取自传递给 connect 的套接字地址结构),然后立即返回到调用进程。

调用该函数后:

(1)我们再也不能给输出操作指定目的 IP 地址和端口号。此时使用 write 或 send,而不是 sendto,如果要使用 sendto,则第 5 个参数设为 NULL,第 6 个参数设为 0。

(2)我们不必使用 recvfrom 以获悉数据报发送者,而改为 read、recv 或 recvmsg。一个已连接的 UDP 套接字能且仅能与一个对端交换数据报。确切地说,是与一个 IP 地址交换数据报,因为 connect 到多播或广播地址是可能的。

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

但是,值得注意的是,使用 connect 迫使该套接字只能与唯一的对端进行通信。所以看下图:

对于右边数第二个客户,不能对一个套接字使用 connect。

注意:

(1)给一个 UDP 套接字多次调用 connect 有以下两个目的:

  • 指定新的 IP 地址和端口号;
  • 断开套接字。在已连接 UDP 套接字上调用 connect 的进程才能再次调用 connect 断开连接。调用时,把套接字地址结构的地址族成员设置为 AF_UNSPEC,调用时可能会返回 EAFNOSUPPORT 错误,不过没有关系。

(2)性能比较

当一个套接字使用 sendto 对一个未使用 connect 进行连接的套接字发送两次数据时,内核执行步骤为:

  • 连接套接字
  • 输出第一个数据报
  • 断开套接字连接
  • 连接套接字
  • 输出第一个数据报
  • 断开套接字连接

使用了 connect 的套接字,步骤如下:

  • 连接套接字
  • 输出第一个数据报
  • 输出第二个数据报

所以性能上使用 connect 要更好。因为临时连接的操作会耗费每个 UDP 传输的 1/3 的时间。

#include	"unp.h"

// 使用 connect 的客户端程序
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int	n;
	char	sendline[MAXLINE], recvline[MAXLINE + 1];

	Connect(sockfd, (SA *) pservaddr, servlen);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Write(sockfd, sendline, strlen(sendline));

		n = Read(sockfd, recvline, MAXLINE);

		recvline[n] = 0;	/* null terminate */
		Fputs(recvline, stdout);
	}
}

UDP 使用 connect 函数去连接一个没有开启服务的服务器也不会返回错误,内核只是检查是否存在立即可知的错误,像这种异步错误仍然不会立即出现。


UDP 缺乏流量控制

UDP 没有流量控制并且是不可靠的。接收缓冲区满时,数据就会被丢弃,虽然可以增大接收缓冲区,但是并不能解决问题。


UDP 中的外出接口的确定

已连接(使用 connect)的 UDP 套接字可以用来确定用于某个特定目的地的外出接口。未调用 bind 显式绑定的的套接字,在使用 connect 时,内核会选择本地 IP 地址,这个本地 IP 地址通过为目的 IP 地址搜索路由表得到外出接口。调用 connect 并不给对端发送任何消息,它完全是一个本地操作。


使用 select 函数的 UDP 回射服务器程序

#include	"unp.h"

int main(int argc, char **argv)
{
	int				udpfd, nready, maxfdp1;
	char				mesg[MAXLINE];
	fd_set				rset;
	ssize_t				n;
	socklen_t			len;
	struct sockaddr_in	        cliaddr, servaddr;
	void				sig_chld(int);

		/* create UDP socket */
	udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(udpfd, (SA *) &servaddr, sizeof(servaddr));

	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */

	FD_ZERO(&rset);
	maxfdp1 = udpfd + 1;
	for ( ; ; ) {
		FD_SET(udpfd, &rset);
		if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("select error");
		}

		if (FD_ISSET(udpfd, &rset)) {
			len = sizeof(cliaddr);
			n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);

			Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len);
		}
	}
}

猜你喜欢

转载自blog.csdn.net/lc250123/article/details/81660185