UNIX网络编程(UNP) 第八章学习笔记

概述

image-20191216134708501

可以看到udp和tcp不一样的在于,udp不需要建立连接,没有connect和listen这一步,而且使用sendtorecvfrom函数来完成基本读取

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,一般来说可以用两种原因

  1. 指定新的IP地址和端口号
  2. 断开套接字,为此我们只需要再次调用的时候将地址族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);
        }
    }
}

在该版本中,我们加入了几个元素

  1. 我们用setsockopt设置了reuseaddr防止已有连接存在
  2. udp可以绑定在和tcp相同的端口上,tcp端口和udp端口是独立的
发布了31 篇原创文章 · 获赞 32 · 访问量 742

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/103573457
今日推荐