UNIX网络编程笔记(6)—UDP网络编程

来源:互联网 发布:unity3d gui texture 编辑:程序博客网 时间:2024/05/17 22:59

基本UDP套接字编程

1. 概述

TCP和UDP的本质区别就在于:UDP是无连接不可靠的数据报协议,TCP是面向连接的可靠字节流。因此使用TCP和UDP编写的应用程序存在一些差异。使用UDP编写的一些常见的应用程序有:DNS(域名解析系统)、NFS(网络文件系统)和SNMP(简单网络管理协议)。


2. sendto和recvfrom函数

类似与标准的read和write函数:

#include <sys/socket.h>ssize_t recvfrom (int sockfd,void *buff,size_t nbytes,int flags,                struct sockaddr *from,socklen_t *addrlen);ssize_t sendto (inat sockfd,const void * buff,size_t nbytes,int flags,                const struct sockaddr*to,socklen_t addrlen);

参数说明:
回忆read和write函数,前三个参数分别是:fd,buf,nbytes分别表示:描述符,指向读入或写出缓冲区的指针和读写的字节数,跟我们上述的recvfrom和sendto就是对应的。

对于sendto来说,顾名思义,我们需要一个参数包含数据报接收者的协议地址(IP和端口号),上述 const struct sockaddr * to就是这样一个参数,它指向了接收者的协议地址,另外我们需要一个addrlen,防止内核读取指针地址越界,这个套路跟以前见过TCP套接字函数中的用法一样。

对于recvfrom来说,struct sockaddr * fromsocklen_t *addrlen是值-结果参数,返回发送数据者的协议地址结构,如果部关系发送者的协议地址,那么我们可以完全把这两个参数设定为NULL。


3. UDP回射服务器程序

最基本的UDP回射服务器程序。

#include <stdio.h>  #include <stdlib.h>  #include <unistd.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/socket.h>  #include <netinet/in.h>  #include <string.h>  #define SERV_PORT 1024#define MAXLEN 1024void dg_echo(int sockfd,struct sockaddr*pcliaddr,socklen_t clilen);int main(){    int sockfd;    struct sockaddr_in servaddr,cliaddr;    if((sockfd=socket(AF_INET,SOCK_DGRAM,0))<0)    {        printf("socket error\r\n");        return -1;    }    //服务器套接字结构    memset(&servaddr,0x00,sizeof(servaddr));    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);    servaddr.sin_port=htons(SERV_PORT);    servaddr.sin_family=AF_INET;    bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));    dg_echo(sockfd,(struct sockaddr *)&cliaddr,sizeof(cliaddr));    return 0;}void dg_echo(int sockfd ,struct sockaddr* pcliaddr,socklen_t clilen){    char buf[MAXLEN];    int n;    int len = clilen;    while(1)    {        if((n=recvfrom(sockfd,buf,MAXLEN,0,pcliaddr,&len))<=0)//阻塞        {            printf("recvfrom error\r\n");            return ;        }        sendto(sockfd,buf,n,0,pcliaddr,len);    }}

4. UDP回射客户端程序

最基本的UDP回射客户端程序。

#include <stdio.h>  #include <stdlib.h>  #include <unistd.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/socket.h>  #include <netinet/in.h>  #include <string.h>  #define SERV_PORT 1024#define MAXLEN 1024void dg_cli(FILE*,int ,const struct sockaddr*,socklen_t);int main(int argc, char ** argv){    int sockfd;    struct sockaddr_in servaddr;    if(argc!=2)    {        printf("usage: udpcli <IPaddress>\r\n");        return -1;    }    memset(&servaddr,0x00,sizeof(servaddr));    servaddr.sin_family=AF_INET;    servaddr.sin_port=htons(SERV_PORT);    if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)    {        printf("inet_pton error\r\n");        return -1;    }    sockfd = socket(AF_INET,SOCK_DGRAM,0);    dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));    return 0;}void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen){    int n;    char sendbuff[MAXLEN];    char recvbuff[MAXLEN+1];    while(fgets(sendbuff,MAXLEN,fp)!=NULL)    {        //指定服务器套接字结构直接sendto        sendto(sockfd,sendbuff,strlen(sendbuff),0,pservaddr,servlen);        if((n=recvfrom(sockfd,recvbuff,MAXLEN,0,NULL,NULL))<=0)        {            printf("recvfrom error\r\n");            return ;        }        recvbuff[n]='\0';//防止越界        fputs(recvbuff,stdout);//输出回射数据    }}

小结

对于上述程序有几个问题需要注意:
1.最简单的UDP回射服务与客户端程序,在正常情况下,运行的很好。不过我们不知道数据报是否会在以下两种情况下丢失:1.客户数据->服务器方向 2.服务器应答->客户端,请求丢失和应答丢失都有可能造成客户端程序在recvfrom函数的阻塞。
2.如果不启动服务器程序,直接运行客户端,当我们输入数据之后(sendto正常返回),然而没有相应的服务器进行回射,客户端会阻塞在recvfrom函数,经过tcpdump工具分析,服务器主机响应一个port unreachable的ICMP消息。不过这个ICMP消息不返回给客户进程,称之为ICMP异步错误。
3.如果某个进程直到客户端进程的临时端口号,该进程也可以向客户端进程发送数据报,这些数据报就会跟服务器应答混淆,解决的办法就是客户端程序通过recvfrom返回发送者的套接字结构与服务器对比。


5. UDP调用connect

上述提到的ICMP异步错误不会返回到UDP套接字,通过connect函数可以解决。这个connect与TCP的connect还是有区别的,因为毕竟UDP,至少时不需要经过三路握手的过程,不过可以检测出是否存在立即可知的错误,例如一个显然不可打的目的地,记录对端的IP地址和端口号,立即返回到客户端进程。

因为调用connect,UDP程序也发生了细微的变化:

1.UDP套接字分为已连接套接字(调用connect成功后),和未连接套接字(默认)。
2.不能使用sendto来指定输出操作的ip地址和端口号了,需要改用send或write,这些数据报将发送到由connect指定的协议地址上。
3.不使用recvfrom来获得数据报的发送者,改用read或recv,在已连接的UDP套接字上,输入操作返回的数据报来自connect指定的协议地址。
4.异步错误会返回给已连接UDP套接字所在进程,未连接UDP套接字不会收到。

一句话总结就是,应用进程调用connect指定对端的IP地址和端口号,然后使用read和write与对端进程进行数据交换。

5.1 UDP套接字多次调用connect

对于TCP套接字来说,connect只能调用一次,不过对于UDP套接字可以调用多次,一般处于两个目的:

1.指定新的IP地址和端口号。
2.断开套接字。

对于第二个目的来说,为了断开一个UDP套接字连接,我们再次调用connect时把套接字地址结构的地址簇成员设置为AF_UNSPEC。这么做可能返回一个EAFNOSUPPORT错误,不过没有关系。使套接字断开连接的是在已连接UDP套接字上调用connect的进程。

5.2 性能

那么现在问题来了,调用 connect和不调用connect的UDP套接字到底哪个效率高呢?
答:当应用进程知道自己要给同一目的的地址发送多个数据报时,显示连接套接字效率更高。临时连接未连接的UDP套接字大约会耗费每个UDP传输三分之一的开销。

5.3 使用connect的UDP客户程序

这里的调用跟TCP调用connect类似,客户程序指定服务器套接字结构。

#include <stdio.h>  #include <stdlib.h>  #include <unistd.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/socket.h>  #include <netinet/in.h>  #include <string.h>  #define SERV_PORT 1024#define MAXLEN 1024//udp socket with connectvoid dg_cli(FILE*,int ,const struct sockaddr*,socklen_t);int main(int argc, char ** argv){    int sockfd;    struct sockaddr_in servaddr;    if(argc!=2)    {        printf("usage: udpcli <IPaddress>\r\n");        return -1;    }    memset(&servaddr,0x00,sizeof(servaddr));    servaddr.sin_family=AF_INET;    servaddr.sin_port=htons(SERV_PORT);    if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<0)    {        printf("inet_pton error\r\n");        return -1;    }    sockfd = socket(AF_INET,SOCK_DGRAM,0);    dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));    return 0;}void dg_cli(FILE*fp,int sockfd,const struct sockaddr*pservaddr,socklen_t servlen){    int n;    char sendbuff[MAXLEN];    char recvbuff[MAXLEN+1];    if(connect(sockfd,(struct sockaddr*)pservaddr,servlen)<0)    {        printf("connect error\r\n");        return ;    }    while(fgets(sendbuff,MAXLEN,fp)!=NULL)    {        write(sockfd,sendbuff,strlen(sendbuff));        if((n=read(sockfd,recvbuff,MAXLEN))==-1)        {            printf("read error!\r\n");            return ;        }        recvbuff[n]='\0';        fputs(recvbuff,stdout);    }}

6. 使用select的TCP+UDP回射服务器函数

1.分别创建TCP监听套接字和UDP套接字。
2.将监听套接字和UDP套接字分别加入select的描述符集。
3.当UDP套接字可读则FD_ISSET(udpfd,&rset)返回,直接回射。
4.当TCP监听套接字可读则FD_ISSET(listenfd,&rset)返回,创建子进程并对connfd已连接套接字进行读写。
5.除此之外,还需要注册一个信号处理函数,以处理客户进程中断导致子进程返回的情况,防止产生僵尸进程。

#include <stdio.h>  #include <stdlib.h>  #include <unistd.h>  #include <errno.h>  #include <sys/types.h>  #include <sys/wait.h>#include <sys/socket.h>  #include <netinet/in.h>  #include <string.h>  #include <signal.h>#define SERV_PORT 1024#define MAXLINE 1024void sig_chld(int);void str_echo(int);int max(int a,int b){    return a>b?a:b;}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;    /* 4create listening TCP socket */    if((listenfd = socket(AF_INET, SOCK_STREAM, 0))<0)    {        printf("socket error\r\n");        return -1;    }    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));    if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<0)    {        printf("bind error\r\n");        return -1;    }    if(listen(listenfd, 5)<0)    {        printf("listenfd error\r\n");        return -1;    }    /* 4create UDP socket */    if((udpfd = socket(AF_INET, SOCK_DGRAM, 0))<0)    {        printf("socket error\r\n");        return -1;    }    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family      = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port        = htons(SERV_PORT);    if(bind(udpfd, (struct sockaddr *) &servaddr, sizeof(servaddr))<0)    {        printf("bind error\r\n");        return -1;    }    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                printf("select error\r\n");        }        if (FD_ISSET(listenfd, &rset))        {            len = sizeof(cliaddr);            connfd = accept(listenfd, (struct sockaddr *) &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 */        }        if (FD_ISSET(udpfd, &rset))        {            len = sizeof(cliaddr);            n = recvfrom(udpfd, mesg, MAXLINE, 0, (struct sockaddr *) &cliaddr, &len);            sendto(udpfd, mesg, n, 0, (struct sockaddr *) &cliaddr, len);        }    }}void str_echo(int connfd){    ssize_t nread;    char readbuff[MAXLINE];    memset(readbuff,0x00,sizeof(readbuff));    while((nread=read(connfd,readbuff,MAXLINE))>0)    {        write(connfd,readbuff,strlen(readbuff));        memset(readbuff,0x00,sizeof(readbuff));    }}void sig_chld(int signo){    pid_t pid;    int stat;#if 1     while((pid=waitpid(-1,&stat,WNOHANG))>0)    printf("waitpid:child terminated,pid=%d\r\n",pid);#endif    return ;}

7. UDP总结

由于有了TCP的基础,这部分相对简单,不过简单的代价就是TCP提供的很多功能没有了,例如:检测丢失的分组并重传,验证相应是否来自正确的对端等等。
另外,UDP没有流量控制,所以一般UDP不用与传送大量数据;UDP套接字还可能产生ICMP异步错误,这可以通过tcpdump来查看这些错误,只有已连接的UDP套接字(connect)才能接收到这些错误。

1 0