UDP客户/服务器

UDP客户/服务器

转载:https://blog.csdn.net/yss28/article/details/54613893

  • 一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的(单个进程/线程就得处理所有客户)。
  • UDP套接字调用connect(不同于TCP):没有三路握手过程,内核只是检查是否存在错误,记录对端IP地址和端口号。

    • 未连接UDP套接字(unconnected UDP socket):新创建的UDP套接字默认如此。
    • 已连接UDP套接字(connected UDP socket):对创建的UDP套接字调用connect的结果。
  • 未连接UDP套接字常使用recvfromsendto,已连接UDP套接字常使用readwrite

  • 一个已连接UDP套接字能且仅能与一个对端IP地址交换数据报。
  • 已连接UDP套接字引发的异步错误会返回给它们的进程,而未连接UDP套接字不接收任何异步错误。
  • 对于TCP套接字,connect只能调用一次。对于UDP,再次调用connect可以指定新的IP地址和端口号或者断开套接字(把地址族设为AF_UNSPEC)。
  • TCP端口与UDP端口相互独立。
  • UDP缺乏流量控制,如果套接字缓冲区时会丢弃数据。

recvfrom和sendto

#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(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t *addrlen);

前三个参数:等同于read和write函数,而且sendto长度为0的数据报或者recvfrom返回0值都是可行的。 
flag参数:与recv/send类型函数相关。 
sendto的后两个参数:指向一个含有数据报接收者的协议地址(类似于connect)。 
recvfrom的后两个参数:指向一个数据报发送者的协议地址(类似于accept)。 
返回值:所接收数据报中的用户数据量大小。

未连接UDP套接字客户/迭代服务器

使用UDP重写上篇文章” TCP客户/服务器“中的回射服务例子如下: 
udpcli.c

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SERV_PORT 8755
#define MAXLINE 32
#define error_exit(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

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

    preply_addr = 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);
        printf("reply from %s:%d : ", 
            inet_ntop(AF_INET, &((struct sockaddr_in *)preply_addr)->sin_addr, buf, sizeof(buf)),
            ntohs(((struct sockaddr_in *)preply_addr)->sin_port));      
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) 
            printf("(ignored)");

        recvline[n] = 0;
        fputs(recvline, stdout);
    }
    free(preply_addr);
}       

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        error_exit("usage: udpcli <IPaddress>");

    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, (struct sockaddr *)&servaddr, sizeof(servaddr));

    exit(0);
}

udpserv.c

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SERV_PORT 8755
#define MAXLINE 32
#define error_exit(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)   

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;
    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); // 通配IP地址
    servaddr.sin_port        = htons(SERV_PORT);
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    exit(0);
}

结果: 
1 
2

  1. 因为recvfrom调用没有设置超时,如果数据报没有到达服务器,或者服务器的应答没有回到客户,客户将永远阻塞于dg_cli函数中的recvfrom调用。
  2. 如果服务器运行在一个只有单个IP地址的主机上,客户工作正常。然而如果服务器主机是多宿的,客户recvfrom返回的IP地址可能不是客服sendto发送数据报的目的IP地址。
  3. 如果在不启动服务器的前提下启动客户,客户sento时,服务器主机响应一个”port unreachable(端口不可达)“的ICMP消息(异步错误),不过这个ICMP消息不返回给客户进程,所以sento将成功返回。一个基本规则是:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接。

使用SIGALRM为recvfrom设置超时

改写上述udpcli.c中的dg_cli函数如下:

#include <signal.h>

void* Signal(int signo, void (*func)(int)) {
    struct sigaction act, oact;

    act.sa_handler = func; 
    sigemptyset(&act.sa_mask); 
    act.sa_flags = 0;
#ifdef SA_INTERRUPT 
    if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART 
    if (signo != SIGALRM) act.sa_flags |= SA_RESTART; 
#endif
    if (sigaction(signo, &act, &oact) < 0) 
        return SIG_ERR;
    return oact.sa_handler; // 返回信号的旧行为
}

static void sig_alrm(int signo) {
    return; /* just interrupt the recvfrom() */
}

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

    Signal(SIGALRM, sig_alrm);
    while (fgets(sendline, MAXLINE, fp) != NULL) {
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        alarm(3); // 设置报警时钟
        if ((n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0) {
            if (errno == EINTR) // SIGALRM中断返回
                fprintf(stderr, "socket timeout\n");
            else
                error_exit("recvfrom error");
        } else {
            alarm(0); // 关闭报警时钟
            recvline[n] = 0;
            fputs(recvline, stdout);
        }
    }
}
扫描二维码关注公众号,回复: 3169626 查看本文章

结果:

$ ./udpcli 127.0.0.1 (不启动服务器udpserv时)
12345
socket timeout (3s后recvfrom返回)
123
123344
socket timeout
socket timeout

使用select为recvfrom设置超时

改写上述udpcli.c中的dg_cli函数如下:

#include <sys/select.h>

int readable_timeo(int fd, int sec) { // 本函数适用于任何类型的套接字
    fd_set rset;
    struct timeval tv;

    FD_ZERO(&rset);
    FD_SET(fd, &rset);

    tv.tv_sec = sec;
    tv.tv_usec = 0;
    return (select(fd+1, &rset, NULL, NULL, &tv)); // select等待该描述符变为可读,或者发生超时(返回0)
}

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

    while (fgets(sendline, MAXLINE, fp) != NULL) {
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        if (readable_timeo(sockfd, 5) == 0) {
            fprintf(stderr, "socket timeout\n");
        } else {
            n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
            recvline[n] = 0; /* null terminate */
            fputs(recvline, stdout);
        }
    }
}

结果:

$ ./udpcli 127.0.0.1 (不启动服务器udpserv时)
12345
socket timeout (3s后recvfrom返回)
abc
socket timeout (3s后recvfrom返回)

使用SO_RCVTIMEO套接字选项为recvfrom设置超时

SO_RCVTIMEO套接字选项选项一旦设置到某个描述符,其超时设置将应用于该描述符上的所有读操作,类似的SO_SNDTIMEO选项则仅仅应用于写操作,两者都不能用于为connect设置超时。改写上述udpcli.c中的dg_cli函数如下:

void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) {
    int n;
    char sendline[MAXLINE], recvline[MAXLINE+1];
    struct timeval tv;

    tv.tv_sec = 3;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    while (fgets(sendline, MAXLINE, fp) != NULL) {
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

        n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
        if (n < 0) {
            if (errno == EWOULDBLOCK) {
                fprintf(stderr, "socket timeout\n");
                continue;
            } else
                error_exit("recvfrom error");
        }

        recvline[n] = 0;    /* null terminate */
        fputs(recvline, stdout);
    }
}

结果同上。

已连接UDP套接字客户/迭代服务器

将上述未连接UDP套接字客户程序改成已连接UDP套接字客户程序只需变动如下代码:

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

    connect(sockfd, (struct sockaddr *)pservaddr, servlen);

    len = sizeof(cliaddr);
    getsockname(sockfd, (struct sockaddr *)&cliaddr, &len);
    printf("local address %s:%d\n", 
        inet_ntop(AF_INET, &cliaddr.sin_addr, buf, sizeof(buf)),
        ntohs(cliaddr.sin_port));

    while (fgets(sendline, MAXLINE, fp) != NULL) {
        write(sockfd, sendline, strlen(sendline));

        n = read(sockfd, recvline, MAXLINE);

        recvline[n] = 0;
        fputs(recvline, stdout);
    }
}
  1. 客户connect服务器时,如果没有相匹配的套接字,UDP将丢弃它们并生成相应的ICMP端口不可达错误。
  2. 客户connect服务器后,可调用getsockname得到内核临时分配的本地IP地址和端口号。

使用select函数的TCP和UDP回射服务器

可将上篇文章” TCP客户/服务器“中的并发TCP回射服务器和本篇上述的迭代UDP回射服务器程序组合成单个使用select来复用TCP和UDP套接字的服务器程序,将”多进程并发TCP回射服务器”改写成如下即可:

int main(int argc, char **argv) {
    int                 listenfd, connfd;
    pid_t               childpid;
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;
    char                buff[MAXLINE];  
    int                 udpfd, nready, maxfdp1;
    char                mesg[MAXLINE];
    fd_set              rset;
    ssize_t             n;
    const int           on = 1;

    /* TCP服务器 */
    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)); // SO_REUSEADDR选项允许重用本地地址(以防该端口上已有连接存在)
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    listen(listenfd, 5);

    /* 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); // TCP端口与UDP端口相互独立
    bind(udpfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    /* 捕获SIGCHLD信号(当fork子进程时必须) */
    Signal(SIGCHLD, sig_chld);

    /* 准备调用select */
    FD_ZERO(&rset);
    maxfdp1 = MAX(listenfd, udpfd) + 1; 
    for ( ; ; ) {
        /* 调用select */
        FD_SET(listenfd, &rset);
        FD_SET(udpfd, &rset);
        if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
            if (errno == EINTR)
                continue;
            else
                error_exit("select");
        }   

        /* 处理新的TCP连接 */     
        if (FD_ISSET(listenfd, &rset)) {
            clilen = sizeof(cliaddr);
            if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
                if (errno == EINTR) continue; // 被信号中断的系统调用
                else if (errno == ECONNABORTED) continue; // 当accept返回前连接被客户终止(RST)
                else error_exit("accept");
            }
            printf("connection from %s, port %d\n", 
                inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), 
                ntohs(cliaddr.sin_port));   

            if ((childpid = fork()) == 0) { // 子进程
                close(listenfd);
                doit(connfd); // 子进程处理TCP客户的请求
                exit(0);
            } 
            close(connfd); // 父进程
        }

        /* 处理UDP数据报的到达 */   
        if (FD_ISSET(udpfd, &rset)) {
            clilen = sizeof(cliaddr);
            n = recvfrom(udpfd, mesg, MAXLINE, 0, (struct sockaddr *)&cliaddr, &clilen);
            sendto(udpfd, mesg, n, 0, (struct sockaddr *)&cliaddr, clilen);
        }
    }

    return 0;
}

结果:

使用信号驱动式I/O的UDP回射服务器

信号驱动式I/O就是让内核在套接字上发生“某事”时使用SIGIO信号通知进程。

#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>

#define SERV_PORT 8755
#define error_exit(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define QSIZE 8     
#define MAXDG 4096

typedef struct {
    void *dg_data; 
    size_t dg_len; 
    struct sockaddr *dg_sa;
    socklen_t dg_salen; 
} DG;

static DG dg[QSIZE];           // 已收取数据报队列(环形缓冲区)
static long cntread[QSIZE+1];  // 诊断用计数器
static int iget;               // 主循环将处理的下一个数组元素的下标
static int iput;               // 信号处理函数将存放到的下一个数组元素的下标
static int nqueue;             // 队列中供主循环处理的数据报的总数
static socklen_t g_clilen;
static int g_sockfd;

void* Signal(int signo, void (*func)(int)) {
    struct sigaction act, oact;

    act.sa_handler = func; 
    sigemptyset(&act.sa_mask); 
    act.sa_flags = 0;
#ifdef SA_INTERRUPT 
    if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART 
    if (signo != SIGALRM) act.sa_flags |= SA_RESTART; 
#endif
    if (sigaction(signo, &act, &oact) < 0) 
        return SIG_ERR;
    return oact.sa_handler; // 返回信号的旧行为
}   

static void sig_hup(int signo) {
    int i;
    for (i = 0; i <= QSIZE; i++)
        printf("cntread[%d] = %ld\n", i, cntread[i]);
}       

/* SIGIO信号的信号处理函数 
 * 1.源自Berkeley的实现使用SIGIO信号支持套接字的信号驱动式I/O;
 * 2.UDP上SIGIO信号在如下情况发生:“数据报到达套接字” 和“套接字上发生异步错误”;
 * 3.TCP上SIGIO信号产生过于频繁,近乎无用。 */
static void sig_io(int signo) {
    ssize_t len;
    int nread;
    DG *ptr;

    for (nread = 0; ; ) {
        if (nqueue >= QSIZE) // 检查队列溢出 
            error_exit("receive overflow");

        ptr = &dg[iput];
        ptr->dg_salen = g_clilen;
        len = recvfrom(g_sockfd, ptr->dg_data, MAXDG, 0, ptr->dg_sa, &ptr->dg_salen);
        if (len < 0) {
            if (errno == EWOULDBLOCK)
                break; // all done; no more queued to read 
            else
                error_exit("recvfrom error");
        }
        ptr->dg_len = len;

        nread++;
        nqueue++;
        if (++iput >= QSIZE)
            iput = 0;
    }
    cntread[nread]++; // histogram of # datagrams read per signal 
}

void dg_echo(int sockfd_arg, struct sockaddr *pcliaddr, socklen_t clilen_arg) {
    int i;
    const int on = 1;
    sigset_t zeromask, newmask, oldmask;

    g_sockfd = sockfd_arg;
    g_clilen = clilen_arg;

    /* 初始化已接收数据队列 */
    for (i = 0; i < QSIZE; i++) {   /* init queue of buffers */
        dg[i].dg_data = malloc(MAXDG);
        dg[i].dg_sa = malloc(g_clilen);
        dg[i].dg_salen = g_clilen;
    }
    iget = iput = nqueue = 0;

    /* 建立信号处理函数并设置套接字标志 */
    Signal(SIGHUP, sig_hup); // SIGHUP信号用于诊断目的
    Signal(SIGIO, sig_io); // 1.为SIGIO信号建立的信号处理函数sig_io(SIGIO信号支持套接字的信号驱动式I/O)
    fcntl(g_sockfd, F_SETOWN, getpid()); // 2.设置套接字属主
    ioctl(g_sockfd, FIOASYNC, &on); // 3.开启该套接字的信号驱动式I/O
    ioctl(g_sockfd, FIONBIO, &on); // 既然信号是不排队的,开启信号驱动式I/O的描述符通常也设置为非阻塞式

    /* 初始化信号集 */
    sigemptyset(&zeromask);     /* init three signal sets */
    sigemptyset(&oldmask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGIO); /* signal we want to block */

    /* 阻塞SIGIO并等待有事可做 */
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 阻塞SIGIO
    for ( ; ; ) {
        /* sigsuspend进行一下处理过程:
         * 1.保存当前信号掩码;
         * 2.设置当前信号掩码为zeromask,暂停进程执行,直到收到信号为止;
         * 3.在进程捕获信号并且在该信号的处理函数返回之后才返回;
         * 4.sigsuspend在该信号的处理函数返回之后才返回,并且将当前信号掩码恢复为调用时刻的值。 */
        while (nqueue == 0)
            sigsuspend(&zeromask); 

        /* 解阻塞SIGIO并发送应答 */
        sigprocmask(SIG_SETMASK, &oldmask, NULL);

        sendto(g_sockfd, dg[iget].dg_data, dg[iget].dg_len, 0, dg[iget].dg_sa, dg[iget].dg_salen);
        if (++iget >= QSIZE)
            iget = 0;

        /* 阻塞SIGIO以修改主循环和信号处理函数共同使用的值nqueue */
        sigprocmask(SIG_BLOCK, &newmask, &oldmask);
        nqueue--;
    }
}   

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    const int on = 1;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 通配IP地址
    servaddr.sin_port = htons(SERV_PORT); 
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)); 
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    exit(0);
}

猜你喜欢

转载自blog.csdn.net/buhuiguowang/article/details/81561317