第4章 使用UDP套接字
UDP(User Datagram Protocol,用户数据报协议)提供了比TCP更简单的端到端服务。
UDP只执行两种功能:(1)向IP层添加了另一个寻址(端口)层;(2)它会检测传输中可能发生的数据损坏,并丢弃任何损坏的数据报。
UDP套接字使用前不进行连接,它会保留消息边界。UDP提供的端到端服务是一种“尽力而为”的服务:不保证通过UDP套接字发送的消息将会到达其目的地。这意味着UDP套接字程序必须准备处理消息的丢失和重排。
4.1 UDP客户
与TCP的区别:
(1) 不会调用connect
(2) 使用sentdto和recvfrom发送消息,而不是sent和recv
(3) 只要执行一次接受,UDP套接字会保留消息边界。
UDP客户只与UDP服务器通信。
UDP应答服务器简单的把接受的任何消息发送回它们的任何源发地。
UDP应答客户端:
(1) 把应答字符串发送给服务器
(2) 接受应答
(3)关闭程序
UDP应答客户端udp_echo_client.c程序代码:
#include "practical.h"
#define MAXSTRLEN 1000
int main(int argc, char **argv)
{
if (argc < 3 || argc > 4) {
err_quit("Usage: a.exe <Server Address/Name> <Echo Word> [<Server Prot>/Service]");
}
char *server = argv[1];
char *echo_str = argv[2];
size_t echo_strlen = strlen(echo_str);
if (echo_strlen > MAXSTRLEN) {
err_quit("%s string too long", echo_str);
}
char *service = (argc == 4) ? argv[3] : "echo";
struct addrinfo hint;
bzero(&hint, sizeof(hint));
hint.ai_family = AF_UNSPEC;
hint.ai_socktype = SOCK_DGRAM;
hint.ai_protocol = IPPROTO_UDP;
struct addrinfo *servaddr;
int rtnval = getaddrinfo(server, service, &hint, &servaddr);
if (rtnval != 0) {
err_quit("getaddrinfo() failed:%s", gai_strerror(rtnval));
}
int sock = socket(servaddr->ai_family, servaddr->ai_socktype, servaddr->ai_protocol);
if (sock < 0) {
err_sys("socket() failed");
}
ssize_t nbytes = sendto(sock, echo_str, echo_strlen, 0,
servaddr->ai_addr, servaddr->ai_addrlen);
if (nbytes < 0) {
err_sys("sendto() failed");
} else if (nbytes != echo_strlen) {
err_quit("sentto() error: sent unexpcted number of bytes");
}
struct sockaddr_storage fromaddr;
size_t fromaddrlen = sizeof(fromaddr);
char buf[MAXSTRLEN + 1];
nbytes = recvfrom(sock, buf, MAXSTRLEN, 0, (struct sockaddr*)&fromaddr, &fromaddrlen);
if (nbytes < 0) {
err_sys("recvfrom() failed");
} else if (nbytes != echo_strlen) {
err_quit("recvfrom() error: reveived unexpected number of bytes");
}
if (!sockaddr_equal(servaddr->ai_addr, (struct sockaddr*)&fromaddr)) {
err_quit("recvform(): received a packet form unknown source");
}
freeaddrinfo(servaddr);
buf[echo_strlen] = '\0';
printf("Received: %s\n", buf);
close(sock);
exit(0);
}
int sockaddr_equal(struct sockaddr *desaddr, struct sockaddr *srcaddr)
{
return 1;
}
这个程序大多数时间会正常工作,但是不适用于生产应用,因为发送到服务器或者从服务器发出的消息丢失,对recvfrom的调用会永远阻塞,并且程序不会终止。客户一般使用超时(timeout)来处理这个问题。
4.2 UDP服务器
程序永远循环,接收一条消息,然后把相同的消息发送回它的任何源发地。服务器只会接受并发送回消息的前255个字符,任何多余的字符都会被套接字实现悄悄的丢弃。
UDP服务器udp_echo_server.c程序代码:
socket(), bind(), 不需要listen(), sento(), recvfrom()。
#include "practical.h" #define MAXSTRLEN 1000 int main(int argc, char **argv) { if (argc != 2) { err_quit("Usage: a.exe <Server Port/Service>"); } char *service = argv[1]; struct addrinfo hint; memset(&hint, 0, sizeof(hint)); hint.ai_flags = AI_PASSIVE; hint.ai_family = AF_UNSPEC; hint.ai_socktype = SOCK_DGRAM; hint.ai_protocol = IPPROTO_UDP; struct addrinfo *servaddr; int rtnval = getaddrinfo(NULL, service, &hint, &servaddr); if (rtnval != 0) { err_quit("getaddrinfo() failed: %s", gai_strerror(rtnval)); } int sock = socket(servaddr->ai_family, servaddr->ai_socktype, servaddr->ai_protocol); if (sock < 0) { err_sys("socket() failed"); } if (bind(sock, servaddr->ai_addr, servaddr->ai_addrlen)) { err_sys("bind() failed"); } freeaddrinfo(servaddr); for(;;) { struct sockaddr_storage clntaddr; socklen_t clntaddrlen = sizeof(clntaddr); char buf[MAXSTRLEN]; ssize_t nbytes = recvfrom(sock, buf, MAXSTRLEN, 0, (struct sockaddr*)&clntaddr, &clntaddrlen); if (nbytes < 0) { err_sys("recvfrom() failed"); } fputs("Handle client: ", stdout); print_addrinfo((struct sockaddr*)&clntaddr, stdout); fputc('\n', stdout); ssize_t numbytes = sendto(sock, buf, nbytes, 0, (struct sockaddr*)&clntaddr, sizeof(clntaddr)); if (numbytes < 0) { err_sys("sendto() failed"); } else if (numbytes != nbytes) { err_sys("sendto(): sent unexpected number of bytes"); } } //end for(;;) }
4.3 使用UDP套接字发送和接收
一旦创建了UDP套接字,就可以使用它向任何地址发送消息和接收来自任何地址的消息,以及接连向许多不同的地址发送消息和接收来自不同地址的消息。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t nbytes,int flags,
const struct sockaddr *destaddr, socklen_t destlen);
返回值:成功返回发送的字节数,出错返回-1
函数sendto类似send,不过sendto允许在无连接的套接字上指定一个目标地址。
对于面向连接的套接字,目的地址忽略,因为目的地址蕴含在连接中。对于
无连接的套接字不能使用send,除非调用connect时预设了目的地址,或者采用sendto来提供另一种发送报文方式。
可以使用不止一个选择来通过套接字发送数据。可以调用带有msghdr结构的sendmsg来指定多重缓冲区传输数据,和writev类似
#include<sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
返回值:成功返回发送字节数,出错返回-1
msghdr至少有下列成员:
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* address size in bytes */
struct *msg_iov; /* array of I/O buffers */
int msg_iovlen; /* number of element in array */
void *msg_control; /* ancillary data */
socklen_t msg_controllen; /* number of ancillary bytes */
int msg_flags; /* flags for reveived message */
};
使用recvfrom来得到数据发送者的源地址。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,
struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
返回值:以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,出错返回-1
若addr非空,它包含数据发送者的套接字端点地址。需要设置addrlen参数指向一个包含addr所指的套接字缓冲区字节大小的整数。返回时,该整数设为该地址的实际字节大小。
Addrlen是一个输入/输出型参数:输入时指定地址缓冲区addr的大小,在与IP版本无关的代码中通常是struct sockaddr_storage。输出时,指向实际复制到缓冲区的地址的大小。
通常易犯的错误:(1) 给addrlen传递一个整数而不是一个指针。(2)忘记初始化所指向的长度变量以包含合适的大小。
由于可以获得发送者的地址,revcfrom通常用于无连接的套接字,否则就等同于recv。
UDP不会保留消息边界,每次调用recvform都会返回至多一个sendto调用的数据。不同的recvfrom永远不会返回来自不同的sendto调用的数据(除非设置MSG_PEEK标志);
send()返回时,调用者知道数据已经复制到缓冲区中以进行传输。数据可能会也可能不会实际的进行传输。但是UDP不会缓冲数据已进行可能的重传,因为它不会从错误中恢复。意味着UDP调用sendto()调用返回时,就已经把消息传递给底层信道以进行传输,并且已经(或很快将要)发送出去。
在消息从网络到达的时间与通过recv()或recvfrom()返回其数据的时间之间,将把数据存储在先进先出(first-in,first-out,FIFO)接受缓冲区中。
已连接的TCP套接字中,所有已接受但尚未递送的字节都视作一个连续的字节序列。
不过UDP套接字,来自不同消息的字节可能来自不同的发送者,缓冲区包含 “数据块”的FIFO序列,每个数据块都具有一个关联的源地址。调用recvfrom永远不会返回多个数据块。但是若缓冲区大小为n,并且接受FIFO中的第一个数据块的大小大于n时,则只会返回数据块的前n个字节。剩余的字节将悄悄的被丢弃,而不会向接受程序指示这一点。
因此调用者要为recvfrom提供较大的缓冲区,使之能够存放程序协议允许的最大消息,这种技术保证不会丢失数据。UDP套接字可以返回最大数据量65507。
MSG_PEEK标志:导致接收的数据保留在套接字的FIFO中,使得它可以被接受多次。适用于:内存不足,应用程序的消息大小差别很大,并且每条消息的前几个字节携带了其大小信息。
接收者可以利用MSG_PEEK和较小的缓冲区调用recvfrom,检测消息的前几个字节确定大小,然后利用足以存放整个消息的较大的缓冲区再次调用recvfrom(不使用MSG_PEEK);
内存足够时,使用足够大缓冲区是个好方法。
4.4 连接UDP套接字
UDP套接字可以调用connect()固定通过套接字发送的进来数据报的地址。一旦连接就可以使用send代替sendto传输数据,因为不需要指定目的地址。可以用类似的方法使用recv代替recvfrom,因为连接的UDP套接字只能接收来自关联的外部地址和端口的数据报,调用connect之后,就会知道任何进入的数据报的源地址。
注意:使用UDP连接后,调用send和recv不会改变UDP的行为方式,消息边界仍会保留,数据报可能会丢失等等。可以利用AF_UNSPEC的地址族调用connect断开连接。
UDP连接套接字的优点:它允许接收由套接字上以前的动作产生的错误指示。