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套接字常使用
recvfrom
和sendto
,已连接UDP套接字常使用read
和write
。 - 一个已连接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);
}
结果:
- 因为recvfrom调用没有设置超时,如果数据报没有到达服务器,或者服务器的应答没有回到客户,客户将永远阻塞于dg_cli函数中的recvfrom调用。
- 如果服务器运行在一个只有单个IP地址的主机上,客户工作正常。然而如果服务器主机是多宿的,客户recvfrom返回的IP地址可能不是客服sendto发送数据报的目的IP地址。
- 如果在不启动服务器的前提下启动客户,客户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);
}
}
}
结果:
$ ./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);
}
}
- 客户connect服务器时,如果没有相匹配的套接字,UDP将丢弃它们并生成相应的ICMP端口不可达错误。
- 客户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);
}