UDP套接字编程

UDP编程简介

  1. TCP和UDP在传输层区别
    UDP是无连接不可靠的数据报协议。TCP提供面向连接的可靠字节流。

  2. 使用UDP常见应用
    DNS(域名系统),NFS(网络文件系统),SNMP(简单网络管理协议)

典型的UDP的Client和Server

(1)客户端不和服务器建立连接,只是使用sendto给服务器发送数据,必须指定服务器地址作为参数。
(2)服务器不接受来自客户端的连接,而只是使用recvfrom,等待来自客户端的数据达到。recvfrom同时返回
客户端的协议地址,用于服务器给客户端响应。


        UDP Client                         UDP服务器

                                           socket()
                                              |      
                                              |
                                         bind()众所周知端口
                                              |    
                                              |
        socket()                         recvfrom() <---------- 
           |                                  |               |
           |          数据请求                |                |
    --->sendto()  ----------------->    阻塞,直到收到数据       |  
    |      |                                  |               |
    |      |                                  |               |
    |      |                               处理请求            |
    |      |                                  |               |
    |      |                                  |               |
    |      |           数据应答                |               |
    |---recvfrom() <-----------------     sendto() ------------
           |
           |
           |
         close()

重要函数recvfrom和sendto

recvfrom函数:

    #include 
    ssize_t recvfrom(int sockfd,void* buff,size_t nbytes,int flags,struct sockaddr* from,socklen_t *addrlen);
    参数:sockfd  : 套接字描述符
          buff    : 用于存放数据的缓冲区
          nbytes  : 缓冲区大小
          flags   : 暂时总设置为0
          from    : 用于存放UDP对端的套接字协议地址(输出参数)
          addrlen : UDP对端的套接字协议地址字节大小(输出参数)

    注意:最后两个参数from和addrlen可以得知该UDP数据是谁发送过来的。
          如果设置from和addrlen为NULL,表示我们忽略对端信息。

    返回值:
        成功返回读取到的字节数,出错返回-1。返回值0是被允许的,不同于TCP中read返回0表示对端已经关闭。

sendto函数:

    #include 
    ssize_t sendto(int sockfd,const void* buff,size_t nbytes,int flags,const struct sockaddr* to,socklen_t *addrlen);
    参数:sockfd  : 套接字描述符
          buff    : 要发送的数据内存
          nbytes  : 数据大小
          flags   : 暂时总设置为0
          to      : 指向接收者的套接字地址结构(输入参数)
          addrlen : 上述套接字地址结构to的字节大小(输入参数)

    注意:最后两个参数to和addrlen告知该数据要发给谁。

    返回值:成功返回写入的字节数,出错返回-1
        写入字节长度为0是被允许的,在UDP下,会形成一个只包含20字节的IP首部和一个8字节的UDP首部,没
        有数据的IP数据报。

UDP的回射实例

        fgets               sendto                      recvfrom
stdin---------->            ----------------------------------->
                  UDP客户端                                     UDP服务器
stdout<---------             <----------------------------------
         fputs               recvfrom                     sendto


说明:
(1)通过socket指定参数SOCK_DGRAM创建UDP套接字。
(2)bind指定server端本地地址为INADDR_ANY(0),端口为众所周知。
(3)UDP中没有TCP中的EOF,因此recvfrom不会终止。
(4)recvfrom是阻塞的,因此提供的是一个迭代服务器,而不是并发服务器。
**//UDPserver代码**
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define MAXLINE     1024
#define SERV_PORT   29988

void dg_echo(int sockfd,struct sockaddr* 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);
    }
}

int main(int argc , char** argv) {
    int sockfd = -1;
    struct sockaddr_in servaddr;
    struct sockaddr_in cliaddr;
    sockfd = socket(AF_INET , SOCK_DGRAM , 0);
    bzero(&servaddr , sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(0);
    servaddr.sin_port = htons(SERV_PORT);
    bind(sockfd , (struct sockaddr*)&servaddr , sizeof(servaddr));
    dg_echo(sockfd , (struct sockaddr*)&cliaddr , sizeof(cliaddr));
    return 0;
}
**//UDPClient代码**
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define MAXLINE     1024
#define SERV_PORT   29988

void dg_cli(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
    int n;
    char sendline[MAXLINE];
    char recvline[MAXLINE+1];
    socklen_t len;
    struct sockaddr* preply_addr;
    preply_addr = (struct sockaddr*)malloc(servlen);

    while(fgets(sendline,MAXLINE,fp)!=NULL){
        sendto(sockfd,sendline,strlen(sendline),0,pservaddr,servlen);
        len = servlen;
        n = recvfrom(sockfd,recvline,MAXLINE,0,preply_addr,&len);
        if(len!= servlen || memcmp(pservaddr,preply_addr,len)!=0){
            continue;
        }
        recvline[n] = 0;
        fputs(recvline,stdout);
    }
}

int main(int argc , char** argv) {
    int sockfd = -1;
    struct sockaddr_in servaddr;
    if(argc != 2){
        cout << "usage:udpcli " << endl;
        exit(-1);
    }
    sockfd = socket(AF_INET , SOCK_DGRAM , 0);
    bzero(&servaddr , sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET , argv[1],&servaddr.sin_addr);
    dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    return 0;
}

注意:上述代码可能出现问题:
(1)如果客户端sendto的数据丢失(比如网络问题),则recvfrom一直阻塞。
(2)如果客户端sendto数据达到,但是服务器发出数据没有达到客户端,则客户端也将一直处于阻塞。

UDP产生ICMP异步错误

做如下测试:
    (1)不启动UDPServer;
    (2)开启tcpdump抓包:sudo tcpdump -i lo
    (3)启动UDPClient:./UDPClient 127.0.0.1,发送数据。

结果:
    此时并没有发生回射,在tcpdump我们发现如下信息:
    15:41:39.535289 IP localhost.50872 > localhost.29988: UDP, length 7
    15:41:39.535300 IP localhost > localhost: ICMP localhost udp port 29988 
        unreachable, length 43

    第一条显示本地端口50872向本地端口29988服务器端口发送数据,长度为7字节UDP
    第二条显示回了一条ICMP包: udp port 29988 unreachable,端口不可达。

总结:
    服务并没有开启时会响应一个port unreachable的ICMP错误,但是该错误不会返回
    给客户端,我们称这样错误为异步错误。

产生异步错误原因:
    该异步错误由sendto引起,UDP输出操作sendto仅仅表示在接口输出队列中具有存放数据报的空间,并不
    代表发送成功。该ICMP错误是之后真正发送时才产生。

解决异步错误的方法:
    对于一个UDP套接字,由它引起的异步错误并不返回给它,除非它已经连接。在进程将其UDP套接字连接
    到恰恰一个对端后,这些异步错误才返回给进程。

UDP的connect函数

udp使用connect函数必要性:
    解决udp无法接受异步错误的问题。

tcp和udp使用connect区别:
    (1)没有tcp连接的三次握手协议
    (2)内核只是检查是否存在一个立即可知的错误(例如上述的ICMP错误)
    (3)内核记录对端的IP和port。
    (4)立即返回。

未连接的UDP套接字:使用socket创建UDP套接字默认如此。
已连接的UDP套接字:调用connect结果。

udp套接字调用connect影响:
    (1)发送数据时不能再指定IP和Port。即不能使用sendto,改用write和send
        任何写入该套接字的数据会自动发往coonect指定对端。
    (2)不必再使用recvfrom接收数据,改用read,recv,recvmsg。
    (3)引发的任何异步错误,会立即返回给当前进程。

对一个udp多次调用connect:
    (1)可以指定新的IP和Port
    (2)把套接字地址结构地址族成员指定为AF_UNSPEC,则断开套接字。如果此时返回一个FAFNOSUPPORT
    错误,可以忽略。

客户端connect接收异步错误

void dg_cli2(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
        int n;
        char sendline[MAXLINE];
        char recvline[MAXLINE+1];
        connect(sockfd,(SA*)pservaddr,servlen);

        while(fgets(sendline,MAXLINE,fp)!=NULL){
            write(sockfd,sendline,strlen(sendline));
            n = read(sockfd,recvline,MAXLINE);
            if(n<0){
                cout << strerror(errno) << endl;
                return ;
            }
            recvline[n] = 0;
            fputs(recvline,stdout);
        }
    }
调用connect指定对端IP和PORT;
使用read替换recvfrom,使用write替换sendto。

注意:如果服务器没有启动,在connect时并不会出错,当进行read时,则会返回错误:Connection refused。
    如果使用的是TCP,则会进行三次握手,Connection refused则会在connect时就返回给进程。

UDP缺乏流量控制

测试实例:客户端不间断的向server发送大量数据,服务器不间断的接收数据。
    //客户端发送数据
    #define NDG 2000
    #define DGLEN   1400
    void dg_cli(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
        int i;
        char sendline[DGLEN];
        for(i=0;i0,pservaddr,servlen);
        }
    }

    //服务器不间断接收数据并统计
    static int count;
    void dg_echo(int sockfd,SA* pcliaddr,socklen_t clilen) {
        socklen_t len;
        char mesg[MAXLINE];
        signal(SIGINT,recvfrom_int);
        for(;;){
            len = clilen;
            recvfrom(sockfd,mesg,MAXLINE,0,pcliaddr,&len);
            count++;
        }
    }
    static void recvfrom_int(int signo){
        cout<<"received " << count << " datagrams" << endl;
        exit(0);
    }
使用命令netstat -s -udp 可以查看系统网络数据包信息,如下:
    Udp:
        245306 packets received
        402051 packets to unknown port received.
        3712 packet receive errors
        651115 packets sent
        RcvbufErrors: 3712
        IgnoredMulti: 1
    可以查看接收到包,没有接收的包,接收错误的包,以及发送的包。

测试结果:
    Client不控制流量的发送,Server不间断的接收,有一定概率接收的包小于发送的包。
    也就是说:有包丢失了。

结论:
    发送或者接收UDP套接字有一定缓冲区,当缓冲区已经满时再发送数据,会导致数据的丢失。
    同时一般发送端速度较快,接收端较慢,UDP发送端淹没接收端是轻而易举的。

设置UDP缓冲区大小:
    int n = 220 * 1024;
    setsockopt(sockfd,SOL_SOCKET,SO_RECBUF,&n,sizeof(n));

你可能感兴趣的:(Unix网络编程)