UNP Chapter 8 - 基本UDP套接口编程

8.1. 概述

有些流行的应用程序是用UDP实现的:DNS(域名系统),NFS(网络文件系统),SNMP(简单网络管理协议)就是这样的例子。

 

8.2. recvfrom和sendto函数

这两个函数类似于标准的read和write函数,但要求有三个附加参数

#include <sys/socket>
ssize_t recvfrom(int sockfd, void * buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen); // 返回: 读写字节数-成功, -1-出错
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的前三个参数:描述字,指向读入或者写出缓冲区的指针,读写字节数。

函数sendto的参数to是一个含有数据将发往的协议地址(例如IP地址和端口号)的套接口地址结构,它的大小由addrlen来指定。函数recvfrom用数据报发送者的协议地址装填由from所指的套接口地址结构,存储在此套接口地址结构中的字节数也以addrlen所指的整数返回给调用者。注意,sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数值是一个指向整数值的指针(值-结果参数)。

recvfrom的最后两个参数类似于accept的最后两个参数:返回时套接口地址结构的内容告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。sendto的最后两个参数类似于connect的最后两个参数:我们用数据报将发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址来装填套接口地址结构。

写一个长度为0的数据报是可行的,这也意味着对于数据报协议,recvfrom返回0值也是可行的;它不表示对方已经关闭了连接,这与TCP套接口上的read返回0的情况不同。由于UDP是无连接的,这就没有诸如关闭UDP连接之类的事情。

 

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

 

#include "unp.h"
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));
}

函数socket第二个参数为SOCK_DGRAM,IPv4协议中的数据报套接口

 

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

许多问题需要考虑,首先,此函数从不终止,因为UDP是一个无连接协议,它没有像TCP中文件结束符之类的东西。其次,该函数提供一个迭代服务器(iterative server),而不是像TCP一样提供了一个并发服务器。没有对fork的调用,所以第一服务器进程就处理了所有客户。一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。

当进程调用recvfrom时,缓冲区中的下一个数据博以FIFO(先进先出)顺序返回给进程。

 

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

#include "unp.h"
int main(int argc, char ** argv)
{
int sockfd;
struct servaddr_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函数

#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(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terninate */
Fputs(recvline, stdout);
}
}

 

8.7. 数据报的丢失

上面的UDP客户-服务器例子是不可靠的。如果一个客户数据报丢失,客户将永远阻塞于dg_cli中对recvfrom的调用,等待一个永远不会到达的服务器应答。与此相似,如果客户数据报到达服务器,但服务器的应答丢失了,客户也将永远阻塞于recvfrom的调用。

8.8. 验证接收到的相应

8.9. 服务器进程未运行

8.10. UDP程序例子小结

 

 

 

 8.11. UDP的connect函数

在8.9节结尾我们提到,除非套接口已连接,否则异步错误是不会返回到UDP套接口的。实际上,我们可以给UDP套接口调用connect,但这样做的结果却与TCP连接毫不相同:没有三路握手过程。内核只是记录对方的IP地址和端口号,它们包含在传递给connect的套接口地址结构中,并立即返回给调用进程。

8.12. dg_cli函数(Revisited)

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

8.13. UDP缺乏流量控制

现在我们检查无任何流量控制的UDP对数据报传输的影响。首先,我们的函数dg_cli修改为发送固定数目的数据报,它不再从标准输入读。

#include "unp.h"
#define DNG 2000 /* #datagrams to read */
#define DGLEN 1400 /* length of each datagram */
void dg_cli(FILE * fp, int sockfd, const SA * pservaddr, socklen_t servlen)
{
int i;
char sendline[MAXLINE];
for(i = 0; i < NDG; i++)
{
Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);
}
}

然后我们修改服务器程序以接收数据报并对接收数目计数。此服务器不再将数据报回射给客户,当我们用终端中断键(SIGINT)终止服务器时,它输出所有接收到数据报的数目并终止。

#include "unp.h"
static void recvfrom_int(int);
static int count;
void dg_echo(int sockfd, SA * pcliaddr, socklen_t clilen)
{
socklen_t len;
char mesg[MAXLINE];
Signal(SIGINT, recvfrom_int);
for( ; ; )
{
len = clilen;
Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
count++;
}
}

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

UDP套接口接收缓冲区

由UDP给特定套接口排队的UDP数据报数目受限于套接口接收缓冲区的大小。我们可以用SO_RCVBUF套接口选项改变此值。

#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;
Signal(SIGINT, recvfrom_int);
n = 240 * 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("\n received %d datagrams \n", count);
exit(0);
}


8.14. UDP中的外出接口的确定

已连接UDP套接口还可用来确定用于特定目标的外出接口,这是由于函数connect被应用到UDP套接口时的副作用:内核选择本地IP地址(假设进程并没有调用bind以明确地址指派它)。这个本地IP地址是通过给目的IP地址搜索路由表,然后使用结果接口的主IP地址而选定的。

下面是一个简单的UDP程序,它连接到指定的IP地址并调用getsockname,输出本地IP地址和端口号。

#include "unp.h"
int main(int argc, char * * argv)
{
int sockfd;
socklen_t len;
struct sockaddr_in cliaddr, servaddr;
if(argc != 2)
err_quit("uasage: 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 "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_chid(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);
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));

 

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




posted on 2012-01-13 16:41  s7vens  阅读(593)  评论(0编辑  收藏  举报