Linux下的socket编程实践(十一) 基本UDP编程细节

在我的这两篇博客中,简单介绍并实现了基于UDP(TCP)的windows(UNIX下流程基本一致)下的服务端和客户端的程序,本文继续探讨关于UDP编程的一些细节。

http://blog.csdn.net/nk_test/article/details/47733307

http://blog.csdn.net/nk_test/article/details/47756381

下图是一个简单的UDP客户/服务器模型:


我在这里也实现了一个简单的UDP回射服务器/客户端:


[cpp]  view plain  copy
  1. /**实践: 实现一个基于UDP的echo回声server/client**/    
  2. //server端代码    
  3. void echoServer(int sockfd);    
  4. int main()    
  5. {    
  6.     int sockfd = socket(AF_INET, SOCK_DGRAM, 0);    
  7.     if (sockfd == -1)    
  8.         err_exit("socket error");    
  9.     
  10.     struct sockaddr_in servAddr;    
  11.     servAddr.sin_family = AF_INET;    
  12.     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);    
  13.     servAddr.sin_port = htons(8001);    
  14.     if (bind(sockfd, (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)    
  15.         err_exit("bind error");    
  16.     
  17.     echoServer(sockfd);    
  18. }    
  19. void echoServer(int sockfd)    
  20. {    
  21.     char buf[BUFSIZ];    
  22.     ssize_t recvBytes = 0;    
  23.     struct sockaddr_in clientAddr;    
  24.     socklen_t addrLen;    
  25.     while (true)    
  26.     {    
  27.         memset(buf, 0, sizeof(buf));    
  28.         addrLen = sizeof(clientAddr);    
  29.         memset(&clientAddr, 0, addrLen);    
  30.         recvBytes = recvfrom(sockfd, buf, sizeof(buf), 0,    
  31.                              (struct sockaddr *)&clientAddr, &addrLen);    
  32.         //如果recvBytes=0, 并不代表对端连接关闭, 因为UDP是无连接的    
  33.         if (recvBytes < 0)    
  34.         {    
  35.             if (errno == EINTR)    
  36.                 continue;    
  37.             else    
  38.                 err_exit("recvfrom error");    
  39.         }    
  40.     
  41.         cout << buf ;    
  42.         if (sendto(sockfd, buf, recvBytes, 0,    
  43.                    (const struct sockaddr *)&clientAddr, addrLen) == -1)    
  44.             err_exit("sendto error");    
  45.     }    
  46. }    
[csharp]  view plain  copy
  1. /**client端代码**/    
  2. void echoClient(int sockfd);    
  3. int main()    
  4. {    
  5.     int sockfd = socket(AF_INET, SOCK_DGRAM, 0);    
  6.     if (sockfd == -1)    
  7.         err_exit("socket error");    
  8.     echoClient(sockfd);    
  9.     cout << "Client exiting..." << endl;    
  10. }    
  11. void echoClient(int sockfd)    
  12. {    
  13.     struct sockaddr_in servAddr;    
  14.     servAddr.sin_family = AF_INET;    
  15.     servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");    
  16.     servAddr.sin_port = htons(8001);    
  17.     char buf[BUFSIZ] = {0};    
  18.     while (fgets(buf, sizeof(buf), stdin) != NULL)    
  19.     {    
  20.         if (sendto(sockfd, buf, strlen(buf), 0,    
  21.                    (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)    
  22.             err_exit("sendto error");    
  23.         memset(buf, 0, sizeof(buf));    
  24.         int recvBytes = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);    
  25.         if (recvBytes == -1)    
  26.         {    
  27.             if (errno == EINTR)    
  28.                 continue;    
  29.             else    
  30.                 err_exit("recvfrom error");    
  31.         }    
  32.         cout << buf ;    
  33.         memset(buf, 0, sizeof(buf));    
  34.     }    
  35. }    
UDP协议并不是像TCP一样是一对一的通信,UDP可以实现广播通信,并且由于是无连接的, 只要知道对等方地址(ip和port) 都可以主动发数据。关闭server,再连接上,还可以进行通信。

UDP编程的注意事项和细节:

1.UDP不存在粘包问题,因为不是基于流的传输(基于消息)。

2.UDP报文可能会丢失、重复、乱序问题。处理丢失可以采用超时处理机制,计时器超时重传;处理重复、乱序可以靠维护数据报之间的序号解决。

3.UDP缺乏流量控制:当缓冲区写满以后,由于UDP没有流量控制机制,因此会覆盖缓冲区。可以通过模拟TCP的滑动窗口协议解决。

4.数据报截断:如果对端发送的UDP数据报大于本地接收缓冲区,报文可能被截断,后面的部分会丢失(而不是像我们想象的下一次能够接收到);并且如果我们使用sendto发送"ABCD"4个字节,接受的recv函数采用循环的方式每次接收一个字符,然而结果却是我们只能接受到A。剩下的不会存在于缓冲区,这也可以形成判断依据: 会丢失报式套接口(UDP)不是流式套接口(TCP)。

5.recvfrom返回0:不代表连接关闭,因为UDP是无连接的。

6.ICMP异步错误(重点)

服务器或者客户端只有一方打开的时候,会发生这个错误,并且这是TCP/IP协议栈产生的一个ICMP应答错误,recv的时候才能收到。因为根本得不到通知,所以称作异步,不会返回给未连接的套接字。

解决方法UDP调用connect

当增加上connect后,send之后,如果对方处于未开启的话,那么recv会接受到ICMP错误。UDPconnect没有三次握手,仅仅是维护了一个信息,一个状态,并且为这个套接字不能发送给其他地址。connect后不指定sendto的目的地址也可以,因为连接的时候已经指定了,并且还可以用sendwrite的方法。

进一步说明:

    1)UDP发送报文的时,只把数据copy到发送缓冲区。在服务器没有起来的情况下,可以发送成功

    2)所谓ICMP异步错误是指:发送的报文的时候,没有错误,接受报文recvfrom的时候,回收到ICMP应答.

    3)异步的错误,无法返回未连接的套接字, 因此如果上例我们调用了connect, 是可以收到该异步ICMP报文的;

对1)的进一步分析:

如果服务器没启动,客户端sendto发送一个数据,结果会是什么呢?

结论是,sendto成功返回,如果客户端还同时调用了recvfrom,则将永远堵塞在recvfrom函数(当然可以设超时),同时通过tcpdump还可以看到,服务端返回ICMP port unreachable错误消息,但是这个消息并没有通过sendto和recvfrom函数返回给用户进程,换句话说,用户并不知道服务端返回了ICMP错误。怎么办呢,udp的connet函数可以搞定这些。

对于udp socket调用connect,称之为已连接UDP socket,其与未连接UDP socket区别如下:

  1. 不使用sendto,而使用write或send,因为在connect中,已经指定目的端IP地址;
  2. 不应用recvfrom,而使用read或recv或recvmsg,注意,如果源地址不是connect连接的目的地址,是不会回馈到该套接字的,这正好由内核帮我们完成了验证接收到的响应;
  3. 由已连接的UDP套接字引发的异步错误,会返回给他们所在的进程,这样就很好的解决了上述问题;
  4. 在调用connect,确定目的IP和port之外,同时还会通过目的地址,查找路由表,确定本地地址,connect之后调用getsockname可以获取到。

总结:UDP客户或服务进程,仅在使用自己的UDP套接字与确定的唯一对端进行通信时,才会调用connect,当然,一般UDP客户端会用connect多一点。同时如果采用connect,其性能也会得到提升,因为对于sendto来讲,其发送数据前和后,需要连接套接字、断开套接字,如果connect之后,这两步就省下了。

7.UDP外出接口的确定:

 假设客户端有多个IP地址,由connect /sendto 函数提供的远程地址的参数,系统会选择一个合适的出口,比如Server的IP是192.168.2.10, 而客户端现在的IP有 192.168.1.32 和 192.168.2.75 那么会自动选择192.168.2.75 这个IP出去。(具体算法先略过...)


最后附上数据报截断的一个示例:

[cpp]  view plain  copy
  1. int main()    
  2. {    
  3.     int sockfd = socket(AF_INET, SOCK_DGRAM, 0);    
  4.     if (sockfd == -1)    
  5.         err_exit("socket error");    
  6.     
  7.     struct sockaddr_in servAddr;    
  8.     servAddr.sin_family = AF_INET;    
  9.     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);    
  10.     servAddr.sin_port = htons(8001);    
  11.     if (bind(sockfd, (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)    
  12.         err_exit("bind error");    
  13.     //给自己发送数据    
  14.     if (sendto(sockfd, "ABCDE", 5, 0,    
  15.                (const struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)    
  16.         err_exit("sendto error");    
  17.     
  18.     for (int i = 0; i < 5; ++i)    
  19.     {    
  20.         char ch;    
  21.         int recvBytes =  recvfrom(sockfd, &ch, 1, MSG_DONTWAIT, NULL, NULL);    
  22.         if (recvBytes == -1)    
  23.         {    
  24.             if (errno == EINTR)    
  25.                 continue;    
  26.             else if (errno == EAGAIN)    
  27.                 err_exit("recvfrom error");    
  28.         }    
  29.         else    
  30.             cout << "char = " << ch << ", recvBytes = " << recvBytes << endl;    
  31.     }    
  32. }    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值