一.
使用UDP的场合:DNS(域名系统)、NFS(网络文件系统)、SNMP(简单网络管理协议)
UDP没有像TCP那样的连接,客户端直接使用sendto函数向某服务器发送数据报,服务器端一直recvfrom函数阻塞,以接收任何客户端发送的数据,把数据报再发送给客户协议地址。
两个新函数:sendto()和recvfrom();connect()在UDP套接字中的用法;异步错误。
int sendto(int sockfd, const void* buff, size_t nbytes, int flag, const struct sockaddr* to, socklen_taddrlen);
int recvfrom(int sockfd, void* buff, size_t nbytes, int flag, struct sockaddr* from, socklen_t* addrlen);
//成功返回字节数,失败返回-1;
(1) flag后面说,这里先置为0
(2) sendto的地址结构指明发送目的地的套接字地址。addrlen指明地址长度,为整数型。相当于TCP的connect中的套接字地址。
(3) recvfrom的地址结构指明发送此数据报的发送端的套接字地址。addrlen为此套接字地址,为整型地址。相当于TCP的accept中的套接字地址。
(4) 写一个长度为0的数据报是可行的,会形成一个只包含IP首部(20字节)和UDP首部(8字节)的IP数据报。所以recvfrom返回0,是可接受的。
而不是像TCP那样read返回0表示关闭连接。
(5) recvfrom的套接字地址参数可以是NULL,表示不关心数据是谁发的。此时addrlen也必须是NULL。
对应TCP:
int connect (int sockfd , const struct sockaddr *servaddr , socklen_t addrlen);
int accept (int sockfd , struct sockaddr *cliaddr , socklen_t* addrlen);
ssize_t read(int fd,void *buf,size_t nbytes);
ssize_t write(int fd,const void *buf,size_t nbytes)
#include "unp.h"//服务器
int main(int argc, char **argv)
{/*首先正常情况下,函数永不会终止。它不像TCP连接那样,还有终止连接的四次。*/
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);//SOCK_DGRAM为数据报套接字
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//服务器IPv4地址被指定为INADDR_ANY
servaddr.sin_port = htons(SERV_PORT);//指定端口
Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));//bind()
dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));//执行服务器处理工作
}
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{/*1.该函数永不终止;2.迭代服务器,非并发;3.协议无关,不查看协议相关结构(pcliaddr)的内容*/
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);
}
}
大多数TCP服务器并发,UDP服务器迭代!!!
每个UDP套接字都会有一个接收缓冲区,类似于一个队列(FIFO)。
多个数据报到达UDP服务器,则会排队,调用recvfrom函数,从这个队列头取出数据报给进程。
而TCP是为每个客户一个连接fork一个子进程,并且每个连接一个套接字,每个套接字一个接收缓冲区,所以我们要并发监听每个接收缓冲区。
而UDP是任何客户发送的数据报放入一个接收缓冲区,所以根本无需什么并发服务器,也不可能做成并发的。
#include "unp.h"//客户端
int main(int argc, char **argv)
{//协议相关
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: udpcli <IPaddress>");
//把服务器的IP地址和端口号填入一个IPv4的套接字地址结构
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, (SA *) &servaddr, sizeof(servaddr));//某个协议类型的套接字地址结构
exit(0);
}
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//协议无关
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
socklen_t len;//1
struct sockaddr *preply_addr;//2
preply_addr = Malloc(servlen);//3
//此时,如果客户的数据报丢失,客户端就会永远阻塞于Recvfrom(),应该设置一个超时(见14.2);
//但是!!!!!!!!!!!!如果真的是服务器还没有应答呢????见22.5
while (Fgets(sendline, MAXLINE, fp) != NULL) //从标准输入读入一个文本行
{ //Sendto将文本行发送给服务器,如果没有指定端口,就临时绑定一个
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
//4------------------------------------------------------------------------------
len = servlen;
n = Recvfrom(sockfd, recvline, MAXLINE, 0, (SA *) replyaddr, &len);//读服务器回射
if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0)
{
printf("reply from %s (ignored)\n",Sock_ntop(preply_addr, len));
continue;
}
//-------------------------------------------------------------------------------
//n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
//有危险,最后两个NULL:任何主机都会向客户发送数据报,并且被认为了是服务器应答(改进)
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);//把文本行显示到标准输出
}
}
改进1.试着获取recvfrom的套接字地址和sendto发送的套接字地址是否一致,来决定此消息是否是来自对端服务器。就是比较servlen的大小和套接字地址结构本身。
如果服务器是多宿主机,即两个IP地址,如Ip1,Ip2。由于我们在写服务器端程序时,bind函数的参数是通配IP,所以当我们sendto时,是使用Ip1,而服务器回射时,内核自动选择了Ip2,则这会让我们客户端误判该回射消息不是来自服务器端。
两个解决办法:
1>recvfrom得到IP后,查询DNS获得主机的域名,以判断消息是否来自该主机。
2>服务器端为每个IP创建一个套接字,使用bind得到每个IP地址。然后使用select监听这些套接字,等待其中一个变为可读,
说明客户端使用的是这个IP,则服务器使用这个IP套接字回射就可以了。22.6
二. 服务器未运行
当我们先启动客户端,不启动服务器端进程时,发生了什么:
我们从控制台输入一行数据回车,然后客户端将永远阻塞在recvfrom函数上。
1. 底层机制:
(1)数据发送到服务器主机上,发送主机的目的端口并没有开启,所以返回一个端口不可达的ICMP(异步错误)消息,
这个消息是不会返回客户端进程的(为何?)。所以客户端用于阻塞在recvfrom上。
异步错误:本例中错误由sendto函数引起,但是sendto成功返回(只要在接口输出队列中有存放ip数据报的空间,就返回成功),
ICMP到后来才返回错误(几秒后),这就是异步错误。
(2)一个基本规则:对于一个UDP套接字,由它引发的异步错误不返回给它,除非它已经连接。(注意UDP也有connect函数)。
为何ICMP消息不会返回客户端进程?Unix这样设计的道理是什么?
假设我们使用客户端连续发送3个消息,2个消息的目的服务器正常,最后一个服务器未启动,则会有一个ICMP消息,
假设这个消息被recvfrom获取,recvfrom返回一个负值表示错误,rrno为错误类型,但是他不反悔错误的IP和端口号
此时客户端并不知道哪个目的套接字出错,内核无法告知进程,因为recvfrom此时返回的信息只是errno值!!
2. 上面提到未连接UDP套接字发生的异步错误,不会返回给进程,这里我们可以使用connect对一个UDP套接字进行连接。
Connect函数的调用和TCP一样,参数指定目的服务器的套接字地址。注意没有三次握手,只是检查对端是否存在立即可知的错误。
已连接UDP套接字和未连接UDP套接字的不同:
(1) 已连接套接字,直接使用send,发送数据报给connect的目的服务器套接字。而不使用sendto。
(2) 已连接套接字,直接使用write,只接收来自connect目的服务器套接字的数据报,
也就意味着,已连接套接字只能和一个对端进行通信,而未连接套接字显然可以和任何多个对端通信。
(3)已连接套接字发生异步消息会返回给进程,因为此时已经知道目的套接字。而未连接套接字不会返回给进程。
注意1:一般对客户端的UDP套接字进行connect,而服务器端还是sendto和recvfrom,connect只会影响本地套接字。
注意2:我们可以对一个已连接的UDP套接字指向多次connect,而TCP不可以。以下情况多次connect
(1) 指定新的IP地址和端口号
(2) 断开套接字。此时把套接字地址结构的地址族(IPv4的sin_family)设为AF_UNSPEC。
3. 当我们对一个未连接的UDP套接字连续sendto两次,看看具体步骤:
连接套接字、发送第一个数据报、断开套接字、连接套接字、发送第二个数据报、断开套接字。
如果两个数据报是同一个目的套接字,则我们应该使用显然connect,之后会提高效率。因为这样只需要一个连接和断开。
注:Unix中一个连接需要耗费一次UDP传输的三分之一的开销。
4. 重新对dg_cli改进
(上面那个改进是面向无连接的,这个是有链接,所以不知道谁发来的这种错误就不存在了,这里是改进接受到错误进程)
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{//协议无关
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
Connect(socket,(SA*)pservaddr,servlen);//1.联结
//此时,如果客户的数据报丢失,客户端就会永远阻塞于Recvfrom(),应该设置一个超时(见14.2);
//但是!!!!!!!!!!!!如果真的是服务器还没有应答呢????见22.5
while (Fgets(sendline, MAXLINE, fp) != NULL) //从标准输入读入一个文本行
{ //Sendto将文本行发送给服务器,如果没有指定端口,就临时绑定一个
Write(sockfd, sendline, strlen(sendline));//2改为write()
n = Read(sockfd, recvline, MAXLINE);//3.读服务器回射改为read()
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);//把文本行显示到标准输出
}
}
此时我们connect时,并没有发生错误,直到我们发送一个消息时才返回错误。而如果此时TCP的话,在connect时就会发生错误。
因为UDP的connect不会触发三次握手,而TCP的connect会触发三次握手,发现目的端口不可达,则服务器会返回RST分组。
5. UDP是没有流量控制的
假设一个客户端连续发送大量的数据,则服务器端使用套接字接收缓冲区排队接收这些数据,
但当发送来的数据超出套接字接收缓冲区时,服务器端就会自动丢弃到来的数据报,而此时客户端和服务器端不会有任何的错误。
6. UDP中的IP地址和端口号
(1)未连接的UDP套接字,
如果我们没有bind,则当sendto时,内核选择一个本地IP地址和端口号,所以同一主机上两次连续的sendto,
客户端:两个消息的源IP地址和端口号可能都不一样。而且,服务器端:接收recvfrom后,回射消息,sendto时,
可能造成回射消息的源IP地址和端口号和recvfrom消息的目的IP地址和端口号不一样(收发可能不是一个)。
(2)已连接UDP套接字,
客户端:如果没有bind,多次connect同一个目的IP端口号时,已连接套接字的本地IP和端口号可能都是不一样的。
我们可以使用getsockname来获取已连接UDP套接字的本地IP和端口号;recvmsg来获取未连接的UDP套接字的本地IP和端口号。
7. 使用select的TCP和UDP来重写回射服务器
/* include udpservselect01 */
#include "unp.h"
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;
void sig_chld(int);
/* -----------------------4create listening TCP socket --------------------*/
//1.创建监听TCP套接字
//创建一个监听TCP套接字并捆绑服务器的众所周知端口,设置SO_REUSEADDR套接字选项以防该端口上已有连接存在。
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));
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
/*-------------------------------------------------------------------------*/
/* --------------------------4create UDP socket ---------------------------*/
//2.创建一个UDP套接字并捆绑与TCP套接字相同的端口。这里无需在调用bind之前设置SO_REUSEADDR套接字选项,
//因为TCP端口是独立于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);
Bind(udpfd, (SA *) &servaddr, sizeof(servaddr));
/*-----------------------end udpservselect01-------------------------------*/
/* --------------------include udpservselect02---------------------------- */
Signal(SIGCHLD, sig_chld);//3.给SIGCHLD建立信号处理程序,因为TCP连接将
//由某个子进程处理。我们已在图5-11中给出了这个信号处理函数。
/* must call waitpid() */
//4.准备select,我们给select初始化一个描述符集,并计算出我们等待的两个描述符的较大者。
FD_ZERO(&rset);
maxfdp1 = max(listenfd, udpfd) + 1;
for ( ; ; )
{
/*5.调用select只是为了等待监听TCP套接字的可读条件或UDP套接字的可读条件。
既然我们的sig_chld信号处理函数可能中断我们对select的调用,我们于是需要处理EINTR错误。 */
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
err_sys("select error");
}
/*------------------6.处理新的客户连接//这与第5章中采取的步骤相同。------------------*/
//当监听TCP套接字可读时,我们accept一个新的客户连接,fork一个子进程,并在子进程中调用str_echo函数。
if (FD_ISSET(listenfd, &rset))
{
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &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 */
}
/*------------------6.处理数据报的到达----------------------------------------------*/
//如果UDP套接字可读,那么已有一个数据报到达。我们使用recvfrom读入它,再使用sendto把它发回给客户。
if (FD_ISSET(udpfd, &rset))
{
len = sizeof(cliaddr);
n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);
Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len);
}
}
}
/* end udpservselect02 */
注:
这里涉及select的I/O复用,信号处理,fork子进程,TCP服务器,UDP服务器,套接字选项。
这里的str_echo函数和第五章的相同,信号处理函数并没有实现,注意函数里要调用waitpid。
注意这里有意思的地方:就是我们只是使用select监听TCP的监听套接字和UDP套接字。对于已连接的TCP套接字使用子进程来处理。
即TCP的并行服务器和UDP的迭代服务器。
前面我们曾使用select来监听TCP监听套接字和已连接套接字,使得程序完全的单进程。这里并没有这么做,
因为那样太麻烦,这里只是展示了使用select同时监听TCP连接和UDP连接。很简洁,很有意思,这段代码仔细看。很多有意思的地方。
注意这里:同一台服务器,TCP套接字和UDP套接字使用同一个端口,这是可以的。