UNP-UNIX网络编程 第八章:基本udp套接字编程

一.

使用UDP的场合:DNS(域名系统)、NFS(网络文件系统)、SNMP(简单网络管理协议)
UDP没有像TCP那样的连接,客户端直接使用sendto函数向某服务器发送数据报,服务器端一直recvfrom函数阻塞,以接收任何客户端发送的数据,把数据报再发送给客户协议地址。
两个新函数:sendto()和recvfrom();connect()在UDP套接字中的用法;异步错误。

int sendto(int sockfd, const void* buff, size_t nbytes, int flag, const struct sockaddr* to, socklen_taddrlen);  
int recvfrom(int sockfd, void* buff, size_t nbytes, int flag, struct sockaddr* from, socklen_t* addrlen);  
//成功返回字节数,失败返回-1;

(1) flag后面说,这里先置为0
(2) sendto的地址结构指明发送目的地的套接字地址。addrlen指明地址长度,为整数型。相当于TCP的connect中的套接字地址。
(3) recvfrom的地址结构指明发送此数据报的发送端的套接字地址。addrlen为此套接字地址,为整型地址。相当于TCP的accept中的套接字地址。
(4) 写一个长度为0的数据报是可行的,会形成一个只包含IP首部(20字节)和UDP首部(8字节)的IP数据报。所以recvfrom返回0,是可接受的。
而不是像TCP那样read返回0表示关闭连接。
(5) recvfrom的套接字地址参数可以是NULL,表示不关心数据是谁发的。此时addrlen也必须是NULL。
对应TCP:

int connect (int sockfd , const struct sockaddr *servaddr , socklen_t addrlen);
int accept (int sockfd , struct sockaddr *cliaddr , socklen_t* addrlen);    
ssize_t read(int fd,void *buf,size_t nbytes);
ssize_t write(int fd,const void *buf,size_t nbytes)
#include    "unp.h"//服务器
int main(int argc, char **argv)
{/*首先正常情况下,函数永不会终止。它不像TCP连接那样,还有终止连接的四次。*/
    int    sockfd;
    struct sockaddr_in  servaddr, cliaddr;

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);//SOCK_DGRAM为数据报套接字

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//服务器IPv4地址被指定为INADDR_ANY
    servaddr.sin_port        = htons(SERV_PORT);//指定端口

    Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));//bind()

    dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));//执行服务器处理工作
}

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{/*1.该函数永不终止;2.迭代服务器,非并发;3.协议无关,不查看协议相关结构(pcliaddr)的内容*/
    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套接字都会有一个接收缓冲区,类似于一个队列(FIFO)。
多个数据报到达UDP服务器,则会排队,调用recvfrom函数,从这个队列头取出数据报给进程。
而TCP是为每个客户一个连接fork一个子进程,并且每个连接一个套接字,每个套接字一个接收缓冲区,所以我们要并发监听每个接收缓冲区。
而UDP是任何客户发送的数据报放入一个接收缓冲区,所以根本无需什么并发服务器,也不可能做成并发的。

#include    "unp.h"//客户端
int main(int argc, char **argv)
{//协议相关
    int    sockfd;
    struct sockaddr_in  servaddr;

    if (argc != 2)
        err_quit("usage: udpcli <IPaddress>");
    //把服务器的IP地址和端口号填入一个IPv4的套接字地址结构
    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);
}

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//协议无关
    int     n;
    char    sendline[MAXLINE], recvline[MAXLINE + 1];
    socklen_t       len;//1
    struct sockaddr *preply_addr;//2
    preply_addr = Malloc(servlen);//3
    //此时,如果客户的数据报丢失,客户端就会永远阻塞于Recvfrom(),应该设置一个超时(见14.2);
    //但是!!!!!!!!!!!!如果真的是服务器还没有应答呢????见22.5
    while (Fgets(sendline, MAXLINE, fp) != NULL) //从标准输入读入一个文本行
    {   //Sendto将文本行发送给服务器,如果没有指定端口,就临时绑定一个
        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
        //4------------------------------------------------------------------------------
        len = servlen;
        n = Recvfrom(sockfd, recvline, MAXLINE, 0, (SA *) replyaddr, &len);//读服务器回射
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) 
        {
            printf("reply from %s (ignored)\n",Sock_ntop(preply_addr, len));
            continue;
        }
        //-------------------------------------------------------------------------------
        //n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
        //有危险,最后两个NULL:任何主机都会向客户发送数据报,并且被认为了是服务器应答(改进)
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);//把文本行显示到标准输出
    }
}

改进1.试着获取recvfrom的套接字地址和sendto发送的套接字地址是否一致,来决定此消息是否是来自对端服务器。就是比较servlen的大小和套接字地址结构本身。
如果服务器是多宿主机,即两个IP地址,如Ip1,Ip2。由于我们在写服务器端程序时,bind函数的参数是通配IP,所以当我们sendto时,是使用Ip1,而服务器回射时,内核自动选择了Ip2,则这会让我们客户端误判该回射消息不是来自服务器端。
两个解决办法:
1>recvfrom得到IP后,查询DNS获得主机的域名,以判断消息是否来自该主机。
2>服务器端为每个IP创建一个套接字,使用bind得到每个IP地址。然后使用select监听这些套接字,等待其中一个变为可读,
说明客户端使用的是这个IP,则服务器使用这个IP套接字回射就可以了。22.6

二. 服务器未运行

当我们先启动客户端,不启动服务器端进程时,发生了什么:
我们从控制台输入一行数据回车,然后客户端将永远阻塞在recvfrom函数上。

1. 底层机制:

(1)数据发送到服务器主机上,发送主机的目的端口并没有开启,所以返回一个端口不可达的ICMP(异步错误)消息,
这个消息是不会返回客户端进程的(为何?)。所以客户端用于阻塞在recvfrom上。
异步错误:本例中错误由sendto函数引起,但是sendto成功返回(只要在接口输出队列中有存放ip数据报的空间,就返回成功),
ICMP到后来才返回错误(几秒后),这就是异步错误。
(2)一个基本规则:对于一个UDP套接字,由它引发的异步错误不返回给它,除非它已经连接。(注意UDP也有connect函数)。
为何ICMP消息不会返回客户端进程?Unix这样设计的道理是什么?
假设我们使用客户端连续发送3个消息,2个消息的目的服务器正常,最后一个服务器未启动,则会有一个ICMP消息,
假设这个消息被recvfrom获取,recvfrom返回一个负值表示错误,rrno为错误类型,但是他不反悔错误的IP和端口号
此时客户端并不知道哪个目的套接字出错,内核无法告知进程,因为recvfrom此时返回的信息只是errno值!!

2. 上面提到未连接UDP套接字发生的异步错误,不会返回给进程,这里我们可以使用connect对一个UDP套接字进行连接。

Connect函数的调用和TCP一样,参数指定目的服务器的套接字地址。注意没有三次握手,只是检查对端是否存在立即可知的错误。
已连接UDP套接字和未连接UDP套接字的不同:
(1) 已连接套接字,直接使用send,发送数据报给connect的目的服务器套接字。而不使用sendto。
(2) 已连接套接字,直接使用write,只接收来自connect目的服务器套接字的数据报,
也就意味着,已连接套接字只能和一个对端进行通信,而未连接套接字显然可以和任何多个对端通信。
(3)已连接套接字发生异步消息会返回给进程,因为此时已经知道目的套接字。而未连接套接字不会返回给进程。

注意1:一般对客户端的UDP套接字进行connect,而服务器端还是sendto和recvfrom,connect只会影响本地套接字。

注意2:我们可以对一个已连接的UDP套接字指向多次connect,而TCP不可以。以下情况多次connect
(1) 指定新的IP地址和端口号
(2) 断开套接字。此时把套接字地址结构的地址族(IPv4的sin_family)设为AF_UNSPEC。

3. 当我们对一个未连接的UDP套接字连续sendto两次,看看具体步骤:

连接套接字、发送第一个数据报、断开套接字、连接套接字、发送第二个数据报、断开套接字。
如果两个数据报是同一个目的套接字,则我们应该使用显然connect,之后会提高效率。因为这样只需要一个连接和断开。
注:Unix中一个连接需要耗费一次UDP传输的三分之一的开销。

4. 重新对dg_cli改进

(上面那个改进是面向无连接的,这个是有链接,所以不知道谁发来的这种错误就不存在了,这里是改进接受到错误进程)

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//协议无关
    int     n;
    char    sendline[MAXLINE], recvline[MAXLINE + 1];
    Connect(socket,(SA*)pservaddr,servlen);//1.联结
    //此时,如果客户的数据报丢失,客户端就会永远阻塞于Recvfrom(),应该设置一个超时(见14.2);
    //但是!!!!!!!!!!!!如果真的是服务器还没有应答呢????见22.5
    while (Fgets(sendline, MAXLINE, fp) != NULL) //从标准输入读入一个文本行
    {   //Sendto将文本行发送给服务器,如果没有指定端口,就临时绑定一个
        Write(sockfd, sendline, strlen(sendline));//2改为write()
        n = Read(sockfd, recvline, MAXLINE);//3.读服务器回射改为read()
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);//把文本行显示到标准输出
    }
}

此时我们connect时,并没有发生错误,直到我们发送一个消息时才返回错误。而如果此时TCP的话,在connect时就会发生错误。
因为UDP的connect不会触发三次握手,而TCP的connect会触发三次握手,发现目的端口不可达,则服务器会返回RST分组。

5. UDP是没有流量控制的

假设一个客户端连续发送大量的数据,则服务器端使用套接字接收缓冲区排队接收这些数据,
但当发送来的数据超出套接字接收缓冲区时,服务器端就会自动丢弃到来的数据报,而此时客户端和服务器端不会有任何的错误。

6. UDP中的IP地址和端口号

(1)未连接的UDP套接字,
如果我们没有bind,则当sendto时,内核选择一个本地IP地址和端口号,所以同一主机上两次连续的sendto,
客户端:两个消息的源IP地址和端口号可能都不一样。而且,服务器端:接收recvfrom后,回射消息,sendto时,
可能造成回射消息的源IP地址和端口号和recvfrom消息的目的IP地址和端口号不一样(收发可能不是一个)。
(2)已连接UDP套接字,
客户端:如果没有bind,多次connect同一个目的IP端口号时,已连接套接字的本地IP和端口号可能都是不一样的。
我们可以使用getsockname来获取已连接UDP套接字的本地IP和端口号;recvmsg来获取未连接的UDP套接字的本地IP和端口号。

7. 使用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);

/* -----------------------4create listening TCP socket --------------------*/
//1.创建监听TCP套接字
//创建一个监听TCP套接字并捆绑服务器的众所周知端口,设置SO_REUSEADDR套接字选项以防该端口上已有连接存在。
    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);
/*-------------------------------------------------------------------------*/
/* --------------------------4create UDP socket ---------------------------*/
//2.创建一个UDP套接字并捆绑与TCP套接字相同的端口。这里无需在调用bind之前设置SO_REUSEADDR套接字选项,
//因为TCP端口是独立于UDP端口的
    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---------------------------- */
    Signal(SIGCHLD, sig_chld);//3.给SIGCHLD建立信号处理程序,因为TCP连接将
    //由某个子进程处理。我们已在图5-11中给出了这个信号处理函数。
        /* must call waitpid() */
//4.准备select,我们给select初始化一个描述符集,并计算出我们等待的两个描述符的较大者。
    FD_ZERO(&rset);
    maxfdp1 = max(listenfd, udpfd) + 1;

    for ( ; ; ) 
    {
/*5.调用select只是为了等待监听TCP套接字的可读条件或UDP套接字的可读条件。
既然我们的sig_chld信号处理函数可能中断我们对select的调用,我们于是需要处理EINTR错误。 */
        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");
        }
/*------------------6.处理新的客户连接//这与第5章中采取的步骤相同。------------------*/
//当监听TCP套接字可读时,我们accept一个新的客户连接,fork一个子进程,并在子进程中调用str_echo函数。

        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 */
        }
/*------------------6.处理数据报的到达----------------------------------------------*/
//如果UDP套接字可读,那么已有一个数据报到达。我们使用recvfrom读入它,再使用sendto把它发回给客户。
        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 */

注:
这里涉及select的I/O复用,信号处理,fork子进程,TCP服务器,UDP服务器,套接字选项。
这里的str_echo函数和第五章的相同,信号处理函数并没有实现,注意函数里要调用waitpid。
注意这里有意思的地方:就是我们只是使用select监听TCP的监听套接字和UDP套接字。对于已连接的TCP套接字使用子进程来处理。
即TCP的并行服务器和UDP的迭代服务器。
前面我们曾使用select来监听TCP监听套接字和已连接套接字,使得程序完全的单进程。这里并没有这么做,
因为那样太麻烦,这里只是展示了使用select同时监听TCP连接和UDP连接。很简洁,很有意思,这段代码仔细看。很多有意思的地方。
注意这里:同一台服务器,TCP套接字和UDP套接字使用同一个端口,这是可以的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值