UNIX网络编程笔记—UDP网络编程

Posted NearXDU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UNIX网络编程笔记—UDP网络编程相关的知识,希望对你有一定的参考价值。

基本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 1024

void 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 1024

void 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 connect
void 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 1024
void 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)才能接收到这些错误。

以上是关于UNIX网络编程笔记—UDP网络编程的主要内容,如果未能解决你的问题,请参考以下文章

UNIX网络编程笔记—传输层协议

java网络编程TCP/UDP笔记

读书笔记《Unix编程艺术》六

读书笔记《Unix编程艺术》六

Golang 网络编程丝绸之路 - TCP/UDP 地址解析

unix domain socket 的 UDP 通信