概述
可以看到udp和tcp不一样的在于,udp不需要建立连接,没有connect和listen这一步,而且使用sendto
和recvfrom
函数来完成基本读取
recvfrom和sendto函数
函数定义
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);
解释
sockfd,buff,nbytes的参数含义与read、write上类似,flags我们目前只需要使用0即可
对于recvfrom来说 from和addrlen参数会在返回的时候被设置为该udp的来源,如果我们对于来源不感兴趣,我们可以设置为都是NULL。而对于sendto来说,to和addrlen参数设置了发送的目的IP地址。
recvfrom和sendto都会返回发送/读取的字节长度,udp允许发送0值,此时意味着数据报只有20字节的IP首部(如果是IPv4)和8字节的UDP首部,recvfrom会因此返回零值,跟read返回零值代表关闭不一样。
recvfrom和sendto也可以用于tcp,虽然通常没有用
基于udp的回射服务器
client端
#include "../unp.h"
void dgcli(FILE *fp, int sockfd, SA *pservaddr, socklen_t servlen)
{
int n;
socklen_t len;
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;
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;
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
dgcli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));
exit(0);
}
服务器端
#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));
}
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服务器都是迭代而不是并发的,也就是说udp一般使用for循环循环读取,在recvfrom调用的时候,隐含有了队列的先进先出的概念,因为新的数据报到来的时候,也是放入接收缓冲区内
目前而言,我们的服务器存在问题有1. 无法处理丢失(因此可能阻塞等待,需要使用超时) 2. 没有识别服务器(因此可能会将所有数据报误认为是服务器的应答)
验证接收到的响应
更新版本
在当前版本的客户端中,我们没有验证获取的数据报是否来自于服务器,我们可以添加上去
void dgcli(FILE *fp, int sockfd, SA *pservaddr, socklen_t servlen)
{
int n;
socklen_t len;
struct sockaddr *preply_addr;
char sendline[MAXLINE], recvline[MAXLINE + 1];
preply_addr=Malloc(servlen); //这里我们用malloc分配
while (Fgets(sendline, MAXLINE, fp) != NULL)
{
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, len);
if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) //我们在这里比较了原始设定的servaddr和preply_addr的异同
{
printf("reply from %s (ignored)\n", Sock_ntop(preply_addr, len));
continue;
}
recvline[n] = 0;
Fputs(recvline, stdout);
}
}
问题
其实上面的版本没有很好地解决我们的问题,原因在于服务器主机是多宿的话(多个接口、多个IP地址),服务器可能使用的是通配地址,此时服务器发送响应的时候选取的IP地址是外出接口的主IP地址,这与客户端发送时候的目的IP地址不一定一致
一种解决方法是我们用DNS来查找主机名来验证而不是通过IP地址
第二种解决方法是udp服务器可以为每个IP地址配置套接字(用bind),然后用select监听所有的套接字,在用可读的套接字来发送应答,这样能确保发送的ip地址和接受的ip地址一致
服务器进程未运行
当我们在服务器未启动的时候启动客户端的话 ,我们键入一行之后,客户端就会阻塞在recvfrom上。
这里核心逻辑在于sendto发送之后,由于服务器未启动,因此返回了一个port unreachable的ICMP消息,然而该消息并没有返回给 客户进程。这种错误称之为异步错误,因为其产生的原因是sendto,但是udp上输出成功返回仅仅表示在接口输出队列中具有存放IP数据报的空间,所以sendto本身成功返回了。
一个基本的规则是对于UDP套接字来说,除非套接字已连接(connect)否则异步错误不会返回给套接字
udp程序总结
客户端调用sendto的时候,目的IP地址和端口是给定的,但是一般来说可以不确定本机的源IP地址和端口号,在这种情况下,sendto会在第一次调用的时候确定端口号,但是源IP地址可以跟着每个UDP数据报而变动(因为数据链路不同),如果客户端绑定了IP地址,但是内核需要从另外的数据链路发出该数据报,那么IP数据报会包含一个不同于外出链路IP地址的源IP地址
服务器会想要从数据报中获取四个属性,源IP地址和端口号,目的IP地址和端口号。对于TCP来说,这四个属性都较为容易获取,但是对于UDP来说,目的IP地址只能通过设置IP_RECVDSTADDR套接字选项并调用recvmsg(而不是recvfrom)来获取,udp的目的IP地址可以随着每个数据报而改变
UDP的connect函数
对于UDP套接字而言,connect函数的作用与tcp下完全不一样,connect会促使内核检查是否存在立即可知的错误(如显然不可达的目的地)记录对端的IP地址和端口(来自于connect参数),然后立刻返回
对于一个已连接的udp套接字,发生了三个变化:
- 我们不能指定输出IP地址和端口号,也就是不能使用sendto(或者可以使用但是必须指定IP地址为NULL),而是用write或者send,所有写给已连接套接字的内容会自动发送到connect指定的ip地址和端口号
- 我们不需要使用recvfrom,而是改用read,recv和recvmsg,已连接udp套接字上返回的数据报只会来自于connect指定的IP地址,所以不用担心出现之前的无法识别是否来自服务器的问题
- 已连接套接字引发的异步错误会返回给进程而udp套接字不接收任何异步错误
对于那些没有匹配的数据报,udp会丢弃并生成icmp端口不可达错误
注意的是,connect只能用于唯一对端通信,比方说如果DNS中使用一个服务器主机,那么可以使用connect,如果是多个,那么就不能调用connect
多次调用connect
我们可以在一个套接字上多次调用connect,一般来说可以用两种原因
- 指定新的IP地址和端口号
- 断开套接字,为此我们只需要再次调用的时候将地址族sin_family改回AF_UNSPEC即可,这样做会返回EAFNOSUPPORT错误,不过一般关系不大
性能
对于未连接的udp套接字而言,sendto可能会涉及”连接套接字–》发送数据报–》断开套接字–》连接套接字–》发送数据报–》断开套接字“等步骤,如果我们确定我们会多次发送数据报,我们可以用显示连接套接字,使用之后,调用write的步骤变为"连接套接字–》发送数据报–》发送数据报-",在这种情形下,内核只需要复制一次套接字地质结构,否则,我们需要复制多次
修订后的dgcli函数
void dgcli(FILE *fp, int sockfd, 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));//改为write,同时我们去掉了if判断
n = Read(sockfd, recvline, MAXLINE);
recvline[n] = 0;
Fputs(recvline, stdout);
}
}
在修改后,如果对端服务器未启动,那么Read将会返回ECONNREFUSED错误(ICMP返回给进程)
UDP缺乏流量控制
由于udp缺乏流量控制且不可靠,我们需要注意数据报丢失是很容易的事情
UDP外出接口的确定
当我们使用connect来显示指定外出ip地址的时候,connect会确定本地的IP地址和端口,确定的过程是通过为目的IP地址检索路由表得到外出接口,然后选用该接口的主IP地址来确定的,端口号也会在调用connect的时候指派
使用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_child(int);
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);
Setsocketopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
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_child);
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;
else
err_sys("select error");
}
if (FD_ISSET(listenfd, &rset))
{
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &len);
if ((childpid = Fork()) == 0)
{
Close(listenfd);
str_echo(connfd);
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);
}
}
}
在该版本中,我们加入了几个元素
- 我们用setsockopt设置了reuseaddr防止已有连接存在
- udp可以绑定在和tcp相同的端口上,tcp端口和udp端口是独立的