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 * from
和 socklen_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网络编程的主要内容,如果未能解决你的问题,请参考以下文章