「生活可以更简单, 欢迎来到我的开源世界」
  1. 8.1 概述
  2. 8.2 recvfrom和sendto函数
  3. 8.3 UDP回射服务器程序:main函数
  4. 8.4 UDP回收服务器程序:dg_echo函数
  5. 8.5 UDP回射客户程序:main函数
  6. 8.6 UDP回射客户程序:dg_cli函数
  7. 8.7 数据报的丢失
  8. 8.8 验证接收到的响应
  9. 8.9 服务器进程未运行
  10. 8.10 UDP程序例子小结
  11. 8.11 UDP的connect函数
    1. 8.11.1 给一个UDP套接字多次调用connect
    2. 8.11.2 性能
  12. 8.12 dg_cli函数(修订版)
  13. 8.13 UDP缺乏流量控制
  14. 8.14 udp中的外出接口的确定
  15. 8.15 使用select函数的tcp和udp回射服务器程序
Unix网络编程-第8章 基本UDP套接字编程
2019-12-30
」 「

第8章 基本UDP套接字编程

8.1 概述

UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。

有些场合确实适合使用UDP,常见的应用程序有:

image-20200815203716894

8.2 recvfrom和sendto函数

类似于标准的read和write函数,不过需要三个额外的参数:

#include <sys/socket.h>
//成功则均返回读或写的字节数,出错返回-1
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockadd *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);

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

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

写一个长度为0的数据报是可行的。在UDP情况下,会形成一个只包含IP首部和UDP首部而没有数据的IP数据报,即recvfrom返回0值是可接受的。

UDP是无连接的,不存在关闭连接之类的事情。

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

image-20200815211416476

#include    "unp.h"

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

//通过指定SOCK_DGRAM,创建一个UDP套接字
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));
}

8.4 UDP回收服务器程序:dg_echo函数

#include    "unp.h"

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

//迭代服务器,永不终止,无连接
for ( ; ; ) {
len = clilen;
n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}

大多数TCP服务器是并发的,大多数UDP服务器是迭代的。每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区,当进程调用recvfrom函数时,缓冲区中的下一个数据报以FIFO顺序返回给进程。

dg_echo函数是协议无关的:调用者分配一个正确大小的套接字地址结构,将其地址指针和大小传参给dg_echo,dg_echo绝不查看该结构的内容,而是把一个指向该结构的指针传递给recvfrom和sendto。

image-20200815212534596

image-20200815212550160

8.5 UDP回射客户程序:main函数

#include    "unp.h"

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);
}

8.6 UDP回射客户程序:dg_cli函数

dg_cli函数也是协议无关的,不过main函数都是协议相关的。

#include    "unp.h"

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

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

//首次调用sendto时没有绑定一个本地接口,内核在此时为它选择一个临时端口
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

//最后两个参数是空指针,表示并不关心应答数据报由谁发送
//任何接收的数据报均被认为是服务器的内容
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

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

8.7 数据报的丢失

UDP客户/服务器例子是不可靠的:如果客户数据报到达服务器,但是服务器的应答丢失了,则客户将永远阻塞于dg_cli函数的recvfrom调用,等待一个永远不会到达的服务器应答。

防止永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,但是这并不是完整的解决办法。

8.8 验证接收到的响应

知道客户临时端口的任何进程都可以往客户发送数据报,而这些数据报会与正常的服务器应答混杂。

通过在dg_cli函数的recvfrom调用中,通知内核返回数据报发送者的地址,通过比较recvfrom在值-结果传参中返回的长度,然后用memcmp比较套接字地址结构本身,验证接收到的响应。

#include    "unp.h"

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);
}
}

如果服务器运行在只有单个IP的主机上,那么新版的客户将正常工作,如果服务器主机是多宿的,该客户可能失败:发送到服务器数据的地址和接收服务器数据的地址可能不同。

解决办法:

8.9 服务器进程未运行

服务器进程不启动的情况下,客户永远阻塞在它的recvfrom调用,等待一个永不出现的服务器应答:

这个ICMP错误称为异步错误,该错误由sendto引起,但是sendto本身却成功返回。UDP输出操作成功后仅仅返回表示在接口输出队列中具有存放所形成IP数据报的空间,该ICMP错误直到后来才返回,故称其为异步。

一个基本的规则:对于一个UDP套接字,由它引起的异步错误却并不返回给它,除非它已连接。ICMP出错信息包含引起错误的数据报的IP首部和UDP首部,而recvfrom可以返回的信息只有errno值,没法返回出错数据报的目的IP地址和目的UDP端口号,因此做出决定:仅在进程已将其UDP套接字连接到恰恰一个对端后,这些异步错误才返回给进程。

只要SO_BSDCOMPAT套记者选项没有开启,Linux甚至对未连接的套接字也返回大多数ICMP “destination unreachable”错误。

8.10 UDP程序例子小结

image-20200816101704301

image-20200816101727333

8.11 UDP的connect函数

UDP套接字的connect没有三路握手过程,内核只是检查是否存在立即可知的错误,记录对端的IP地址和端口号,然后立即返回到调用进程:

已连接UDP套接字对比默认的未连接套接字的三个变化:

image-20200816103351241

image-20200816103527775

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

8.11.1 给一个UDP套接字多次调用connect

一个已连接UDP套接字的进程可由下列两个目的再次调用connect:

8.11.2 性能

在一个未连接UDP套接字上给两个数据报调用sendto函数涉及6个步骤(源自Berkeley内核):

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

8.12 dg_cli函数(修订版)

#include    "unp.h"

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);
}
}

函数不查看传递给connect的套接字地址结构的内容,仍是协议无关的。

8.13 UDP缺乏流量控制

UDP套接字接收缓冲区:由UDP给某个特定套接字排队的UDP数据报数目受限于该套接字接收缓冲区的大小,可以使用SO_RCVBUF套接字选项修改改制。

增加流量控制的UDP示例:

#include    "unp.h"

#define NDG 2000 /* datagrams to send */
#define DGLEN 1400 /* length of each datagram */

void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int i;
char sendline[DGLEN];

for (i = 0; i < NDG; i++) {
Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);
}
}
#include    "unp.h"

static void recvfrom_int(int);
static int count;

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

Signal(SIGINT, recvfrom_int);

n = 220 * 1024;
Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

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

count++;
}
}

static void
recvfrom_int(int signo)
{
printf("\nreceived %d datagrams\n", count);
exit(0);
}

8.14 udp中的外出接口的确定

已连接UDP套接字还可以用来确定用于某个特定目的地址的外出接口。因为connect函数应用到UDP套接字时有一个副作用:内核选择本地IP地址(未使用bind),这个本地IP地址通过为目的地址搜索路由表得到外出接口,然后选用该接口的主IP地址而选定。

在UDP套接字上调用connect并不给对端主机发送任何信息,它完全是一个本地操作,只是保存对端的IP地址和端口号。

在一个未绑定端口号的UDP套接字上调用connect同时也给该套接字指派一个临时端口。

//使用connect来确定输出接口的UDP程序
#include "unp.h"

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

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

sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

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

Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

len = sizeof(cliaddr);
Getsockname(sockfd, (SA *) &cliaddr, &len);
printf("local address %s\n", Sock_ntop((SA *) &cliaddr, len));

exit(0);
}

8.15 使用select函数的tcp和udp回射服务器程序

将并发TCP回射服务器程序与迭代UDP回射服务器程序组合成单个使用select来复用TCP和UDP套接字的服务器程序。

/* include udpservselect01 */
#include "unp.h"

int
main(int argc, char **argv)
{
int listenfd, connfd, udpfd, nready, maxfdp1;
char mesg[MAXLINE];
pid_t childpid;
fd_set rset;
ssize_t n;
socklen_t len;
const int on = 1;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);

/* create listening TCP socket */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);

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

//设置SO_REUSEADDR套接字选项防止该端口上已有连接存在
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

Listen(listenfd, LISTENQ);

/* 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));
/* end udpservselect01 */

/* include udpservselect02 */
//给SIGCHLD建立信号处理程序,因为TCP连接将由某个子进程处理
Signal(SIGCHLD, sig_chld); /* must call waitpid() */

FD_ZERO(&rset);
maxfdp1 = max(listenfd, udpfd) + 1;
for ( ; ; ) {
FD_SET(listenfd, &rset);
FD_SET(udpfd, &rset);
if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
//sig_chld信号处理程序可能会中断select调用,需要处理EINTR错误
err_sys("select error");
}

if (FD_ISSET(listenfd, &rset)) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &len);

if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}

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);
}
}
}
/* end udpservselect02 */

<⇧>