您的位置:首页 > 理论基础 > 计算机网络

《unix网络编程》(18)基本UDP套接字 简单客户服务器回射程序及改进

2015-04-14 19:44 316 查看
基本TCP套接字参考《unix网络编程》(8)基本TCP套接字

套接字函数



#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *to, socklen_t addrlen);
//如成功返回读或写的字节数,若出错返回-1
/*
参数:
     flags待讨论recv、send、recvmsg和sendmsg等函数再介绍,这里简单的程序中总将其置为0
     sendto中to指向一个含有数据报接收者的协议地址(如IP以及端口)的套接字地址结构,其大小由addrlen指定。
      recvfrom中from指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,而填写的字节数放在addrlen中返回给调用者。
*/
对于数据报协议:写一个长度为0的数据报是可行的。recvfrom返回0是可接受的:并不像TCP套接字上read返回0那样表示对端关闭连接。UDP是无连接的,没有UDP连接关闭的事情。如果recvfrom的from指针为空,那么相应的addrlen也要为空指针,表示不关心数据发送者的协议地址。

recvfrom、sendto都可以用于TCP,尽管通常没理由这么做。

TCP和UDP的客户服务器程序对比



简单的客户服务器回射程序

完整源码及Makefile下载

http://download.csdn.net/download/u013074465/8595079

服务器端,udpsrv01.c

#include "myheader.h"

//该函数时一个简单的循环,使用recvfrom读入下一个到达服务器端口的数据报,再使用sendto发送给发送者
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;
  
  //这里的第二个参数指定为SOCK_DGRAM(IPv4的数据报套接字)
  sockfd = 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(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
  
  dg_echo(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
}


这里的处理函数提供的是一个迭代服务器。大多数TCP服务器是并发的,大多数UDP服务器是迭代的

本套接字隐含一个排队的发生。每个UDP套接字有一个接收缓冲区,到达该套接字的每个数据报都会进入该缓冲区。当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO顺序返回给进程。这样,多个数据报达到该套接字,那么相继到达的数据报仅仅加到该套接字缓冲区中。然而这个缓冲区有限,在文章《unix网络编程》(17)套接字选项中已经提到通过SO_RCVBUF套接字以及如何改变。

客户端,udpcli01.c

#include "myheader.h"

//使用fgets从标准输入读入一行文本,使用sendto将文本发送到服务器,
//使用recvfrom读回服务器的回射,使用fputs把回射内容显示在标准输出。 
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);
    
    n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
    
    recvline
 = 0;
    Fputs(recvline, stdout);
  }
}

int main(int argc, char **argv) {
  int sockfd;
  struct sockaddr_in servaddr;
  
  if (argc != 2)
    err_exit("usage: udpcli <IPaddress>");
  
  bzero(&servaddr, sizeof(servaddr));
  //memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(SERV_PORT);
    printf("tesdddt\n");
  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    printf("test\n");
  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
  
  dg_cli(stdin, sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
  
  exit(0);
}
注意

调用recvfrom其第五个和第六个参数为NULL。这告知内核不关心应答数据报是谁发送的。这样存在一个风险:任何进程不论是在与本客户进程相同的主机还是不同的主机上,都可以向本地IP地址和端口发送数据报,这些数据报被客户读入并认为是服务器的应答,之后会解决该问题。

数据报的丢失

上面的UDP服务器/客户端例程是不可靠的。

如果一个客户的数据报丢失,客户永远阻塞于dg_cli的recvfrom调用,等待一个永远不能到达的服务器应答。类似的,客户数据报到达服务器了,但是服务器的应答丢了,那么客户同样阻塞于recvfrom。可以为客户的recvfrom设置一个超时

仅仅通过设置超时并不是完整解决方法。因为无法判断是数据未到达服务器还是服务器应答没回到客户。

验证收到的响应

介绍客户端代码时提到,知道客户临时端口号的任何进程都可以给客户发数据。解决方法是修改客户端处理程序的recvfrom调用以返回数据报发送者的IP地址和端口号,保留来自数据报所发往服务器的应答,而忽略其他的数据报。

重写dg_cli函数以分配一个套接字地址结构用于存放由recvfrom返回的结构:

#include	"unp.h"

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

        //malloc分配另一个套接字地址结构。
	preply_addr = Malloc(servlen);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		len = servlen;
       
                //在recvfrom调用中,我们通知内核返回数据报发送者的地址。我们首先比较
                //由recvfrom在值-结果参数中返回的长度,然后用memcmp比较套接字地址结构本身
                n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
		if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {
			printf("reply from %s (ignored)\n",
					Sock_ntop(preply_addr, len));
			continue;
		}

		recvline
 = 0;	/* null terminate */
		Fputs(recvline, stdout);
	}
}


如果服务器运行在一个只有单个IP地址的主机上,那么这个版本客户可以正常工作。然而如果服务器主机是多宿的,那么客户就可能失败。我们指定发送数据报到达的接口的服务器的某个非主IP地址,而服务器主机应答时内核选定的外出接口是其主IP地址,这样的话,上边的客户程序就会测试失败。

解决方法:

(1)得到recvfrom返回的IP地址后,客户通过DNS中查找服务器主机的名字来验证该主机的域名(而不是验证IP地址)。

(2)UDP服务器为每个IP地址创建一个套接字,用bind绑定每个IP地址到各自套接字,然后再所有这些套接字上使用select(等待其中一个变为可读),再从可读的套接字给出应答;既然用于给出应答的套接字上绑定过的IP地址就是客户请求的目的IP(否则该数据报不会被投到该套接字),这就保证了应答的源地址与请求的目的地址相同。

服务进程未运行

如果服务器进程未运行时启动客户,那么此时在客户端键入一行文本,似乎什么也没发生。客户永远阻塞于recvfrom等待服务器应答。

但其实,服务器主机响应一个“port unreachable”(端口不可达)ICMP消息。不过这个错误不会返回给客户进程。称这个错误为异步错误。该错误由sendto引起,但是sendto本身却成功返回。从UDP输出操作成功返回仅仅表示在接口输出队列中具有存放所形成IP数据报的空间(参考文章《unix网络编程》(4)缓冲区大小及限制)。该错误直到之后才返回,所以称为异步错误。

一个基本规则是:对于一个UDP套接字,由它引发的异步错误不会返回给它,除非它是已连接的

为什么不返回给客户异步错误?试想单个UDP套接字上接连发送三个数据报给三个不同服务器(即三个不同IP)的UDP客户:该客户调用一个recvfrom循环接收应答。如果某个服务器进程没运行,那么这个服务器发出异步错误,这个ICMP错误消息包含引起错误的IP首部和UDP首部,但是客户并不知道是哪个数据报的发送引起了该错误。但内核通知客户进程时,recvfrom返回消息仅有errno值没办法返回数据报的目的IP和UDP端口号。因此,仅在已连接的UDP套接字上异步错误才返回给进程。

UDP的connect函数

为UDP套接字调用connect函数,其结果与TCP连接大相径庭:没有三次握手。内核只是检查是否存在立即可知的错误(如显然不可达的目的地),记录对端IP和端口(取自传递给connect的套接字的地址结构),然后立即返回到调用进程。

这样的话必须区分:未连接的UDP套接字和已连接的UDP套接字。

对于已连接的UDP套接字,发生了三个变化

(1)再也不能给输出操作指定目的IP和端口,就是说不能使用sendto,而用write或send。写到已连接UDP套接字的任何内容都发送到connect指定的协议地址(IP和端口)。

其实,也可以继续使用sendto,只是sendto第五个参数必须为空指针,第六个参数必须为0。

(2)不必使用recvfrom获取数据报发送者,而改用read、recv或recvmsg。已连接UDP套接字限定了其能且仅能与一个对端交换数据报。确切说,已连接UDP套接字仅仅与一个IP地址交换数据报,因为connect到多播或广播地址是可能的。

(3)会返回异步错误给客户进程。



一个UDP套接字多次调用connect

一个已连接套接字再次调用connect有以下原因:

(1)指定新IP和端口(指定新对端);而TCP套接字只能调用一次connect。

(2)断开套接字。为了端口一个已连接套接字,再次调用connect时将套接字地址结构的地址族成员(IPv4的sin_family,IPv6的sin6_family)设置为AF_UNSPEC;这可能返回一个EAFNOSUPPORT错误。使套接字断开连接的是在已连接UDP套接字上调用connect的进程。

性能

当应用进程在未连接套接字上调用sendto,源于Berkeley的内核暂时连接该套接字,发送数据报,然后断开连接。那么发送三个数据报就要连接三次、三次调用sendto发送数据、断开三次套接字。

在已连接套接字上发送三个数据报效率更高:connect后,调用三次write,依次输出三个数据报。这种情况下,内核只复制一个含有目的IP和端口号的套接字地址结构。相反,调用三次sendto会三次复制。

研究证明:临时连接未连接的UDP套接字大约耗费每个UDP传输三分之一的开销。

调用connect的dg_cli函数

调用connect,并且以read和write调用代替sendto和recvfrom调用。

那么此时在服务器进程未启动时,如果客户与服务器主机连接UDP套接字,那么在客户第一次发送数据报后服务器会给客户发送一个“read error: Connection refused”。这是由于客户发送的数据报引发的来自服务器的ICMP错误。然而TCP客户connect,指定一个不运行服务器进程的服务器主机时,connect同样返回错误,这是因为connect调用会造成TCP三次握手,而其中第一个分节引发服务器的TCP返回RST。

UDP缺少流量控制

客户端通过for循环连续为UDP服务器发送4000个二千字节大小的数据报。服务器结束到数据报统计收到的个数。可以发现,发出的4000个数据报只有少数被服务器接收。服务器一能用进程或客户应用进程都没有给出任何提示说这些数据报丢失。因此UDP没有流量控制且是不可靠的。此例表明UDP发送端淹没其接收端是轻而易举的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐