Shunlqing's Blog

触动、反思,然后重新出发


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

基本UDP编程

发表于 2017-12-01 | 分类于 技术笔记

UDP 用户数据报协议

TCP的通信流程好比“打电话”,拨通号码,需要等对方接起电话(建立了通话渠道),才能进行交流。UDP的通信流程就好比“写信件”,写好信,填好对方的地址和名字,就可以寄出去了。如果没有告知对方,对方并不会知道他将会收到你的一封信。可能因为某些原因(信在运输被弄掉了等等),对方收不到你的信,你也不知道对方收到了没有。更糟糕的是,信可能到对方家里了,但是他不在,或者没有从邮箱里拿出你的信。类似信件的通信行为,UDP把用户进程每次传过来的数据不加拆解、完整的打包成udp数据包交给IP协议层。

UDP协议面向非连接。“非连接”怎么理解?就是说,UDP不管通信对方是否准备好接收消息,甚至不管对方是不是存在。直接把数据打包交给IP层去发送就是了。不像TCP,需要建立起一个连接,才能发送数据。

UDP协议是不可靠的。适用于一次传输数据量较少的通信。

UDP头部

UDP基本通信模型

请求——应答模式

recvfrom和sendto函数

这两个函数是UDP下的读写函数,类似于标准的read和write函数。可以说,这两个函数就是为UDP而生的。为什么这样说呢?

因为UDP是不建立连接的数据传输,这里隐含:对于一个udp套接字,它可以一会儿发给a服务器,一会发给b服务器。同样,它可以接受来自不同IP主机的数据包。(只要知道其地址和端口号就行了)。也就是说,每次发送/接收的行为都是独立的,那么怎么区分数据包呢?解决方法就是在read/write函数加两个参数:地址结构体及其长度。这也就演变出两个函数:recvfrom和sendto.

TCP是面向连接的,单向的。recvfrom/sendto没有必要使用在TCP上。

1
2
3
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buff, size_t hbytes, int flags, struct sockaddr* from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void* buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);

针对这两个函数,注意几点:

  • recvfrom的地址结构及其长度参数都是指针类型,即“值——结果”参数。sendto的地址结构长度是“值”类型。
  • recvfrom返回值为0是允许的,且不像TCP,返回0就代表对端关闭连接。对于UDP来说,写一个长度为0的数据报是允许的。
  • recvfrom的地址结构指针是NULL,那么其长度也必须为NULL,表示我们不关心数据发送者的协议地址。

各种小问题

数据报丢了。。。

客户端sendto一个数据报之后,就兴高采烈转入recvfrom中阻塞等待接收。凡是都有万一,万一数据报在路上被路由器丢弃了,或者服务器根本不存在,又或者服务器返回应答了但是应答丢了。。。会出现什么情况:客户端永久阻塞在recvfrom调用。

一般的解决方法是:给recvfrom调用设置一个超时。

数据报不请自来——验证响应

客户端或服务器本来在等待A端的数据包,但是却接收到了非A的数据报。如果应用进程不想与之通信,就需要有机制来忽略或处理这样的情况。

解决方法:通过recvfrom传回来的地址结构来判断是否是我们想要的响应。

服务器进程未运行

客户端在未获知服务器是否启动的情况下,就像服务器发送数据包,会发生这样的情况,即服务器进程还没有启动时,客户端就sendto一个数据包,然后直接转入到recvfrom中,那么什么也不会发生。客户端将永远阻塞在recvfrom。为什么是这样的?

针对IP协议来说,如果目标进程不可达,服务器端将会返回一个“端口不可达”ICMP错误(异步错误)。但是在默认情况(未连接)下,这个错误是不会被发送进程感知到的(原因后解)。

sendto成功返回仅仅表示在接口输出队列中具有足够的空间存放本次发送形成的IP数据报。也就不能说明对方已经收到数据报。

一个基本规则就是:对于一个UDP套接字,由他引发的异步错误并不返回给套接字本身,除非该UDP套接字已经连接。为什么这样做呢?原因其实很简单,想象一个场景,一个未连接UDP套接字同时发给多个不同的主机,其中有个主机响应了ICMP错误,如果将此异步错误传递给UDP套接字,那么它也判断不了是哪个通信方出现问题。已连接的UDP套接字可以感知到ICMP错误,就是因为它仅仅只有一个对端。

一言:UDP套接字进程感知不了ICMP错误,除非它是已连接的。

UDP的connect行为

与TCP的connect操作有三次握手过程不同,UDP的connect仅仅是检查是否存在立即可知的错误(地址是不是错误的),把套接字的目标地址和目标端口提前填好而已。

设置好目的IP和目的端口的UDP套接字称为“已连接UDP套接字”。它与未连接UDP套接字有三处不同:

  • 设置好目的IP和目的端口号,也就不用每次调用sendto和recvfrom这样需要填地址结构的函数了,可以像TCP一样,调用write/send。
  • 同样的,recvfrom也不必使用,而改用read、recv或recvmsg。
  • 由已连接UDP套接字引发的异步错误将返回它们所在的进程,而未连接UDP套接字不接受任何异步错误。

已连接UDP套接字只是确定在当下要通信的对端,是可变的。改变目的IP和目的端口的方法就是再次调用connect。

已连接和未连接UDP套接字的性能

在一个未连接的UDP套接字上调用两次sendto函数,内核行为经历6个步骤:

  • 连接套接字;
  • 输出第一个数据报;
  • 断开套接字连接;
  • 连接套接字;
  • 输出第二个数据报;
  • 断开套接字连接;

在一个已连接的UDP套接字上调用两次sendto函数,内核行为经历6个步骤:

  • 连接套接字;
  • 输出第一个数据报;
  • 输出第二个数据报;
  • 断开套接字连接;

TIME_WAIT状态

发表于 2017-11-03 | 分类于 网络编程

本文关注内容:TCP连接的TIME_WAIT状态。TCP很重要的一个特性就是可靠性,这是在TCP协议的设计中考虑的重点。TIME_WAIT状态的引入,也是在为TCP可靠性做贡献。

为认识TIME_WAIT状态最基本的设计初衷,我们引一个场景:

在四次挥手阶段,主动关闭方接受被动关闭方的FIN包,然后发送给被动关闭方ACK。此时如果协议策略是直接关闭
连接,清理连接占用的资源。这样会出现问题:如果最后的ACK包丢失了,被动关闭方等待接受ACK超时,就要求重
传。此时主动关闭方已经没有了那个连接的信息,所以发送一个RST。被动关闭方收到RST,一脸茫然。。。

所以,主动关闭方在发给对端ACK包时,不能直接关闭连接,需要保证对端能够收到ACK包,必要时能够重传ACK包。此时主动关闭方所处的状态就是所谓的“TIME_WAIT”状态。

什么是TIME_WAIT状态

TIME_WAIT

  • 发生对象:TCP连接的主动关闭方(如果双方同时关闭,则都会进入TIME_WAIT状态)
  • 发生时机:四次挥手阶段,主动关闭方发出对被动关闭方FIN报文的ACK报文后。
  • 持续时长:2 * MSL (MSL:报文的最大生存时间,伯克利实现设置为30s)

TIME_WAIT的作用(用以解决什么问题)

文章开头我们提到一个TIME_WAIT的好处:(1)主动关闭方进入TIME_WAIT状态确保被动关闭方能够接收到FIN的ACK。除此之外,能够保证:(2)旧连接断开,一个一摸一样的新链接建立(源端口,源IP,目的端口,目的IP都一样),旧连接的数据包不会影响到新的连接。因为TIME_WAIT持续的时间,能够保证数据包在网络中消逝,而连接处于TIME_WAIT状态期间,主机不能使用当前的IP和端口组合建立连接。

TIME_WAIT有何负面影响

TIME_WAIT状态需要持续2*MSL,长达1分钟,甚至更长。在这期间,该连接的端口不能用于建立新的连接。如果短时间内,关闭大量的连接,就会造成大量的TIME_WAIT。
两方面的影响:

  • 大量处于TIME_WAIT的连接,消耗主机内存资源(内核表示tcp的tuple)。
  • TCP协议用于表示端口的字段为16bit,也就是说,理论上最多可使用65535个端口号,实际上可使用的更少。如果短时间内大量的TIME_WAIT,端口耗尽,就无法再发起新的连接了。

如何避免TIME_WAIT状态

解决:

  • TCP选项:TCP_REUSEADDR (强制可以重新使用!)
  • 参数:net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle(直接就不在TIME_WAIT状态呆着。。。)
  • 连接池(不着急关闭)

TCP_REUSEADDR : 处于TIME_WAIT状态的socket是占有连接的,即不允许另一个socket绑定相同的ip和端口。TCP_REUSEADDR的作用就是:”允许处理TIME_WAIT的连接所占有的IP+端口组合得到复用”。不管之前的socket是否设置了该选项,只要当前的socket设置即可生效。

net.ipv4.tcp_tw_reuse = 1允许在系统重用处于TIME_WAIT连接的端口,作用类似与TCP_REUSEADDR;net.ipv4.tcp_tw_recycle = 1是启用TIME_WAIT快速回收,意味直接就不进入TIME_WAIT状态,靠时间戳区分是旧连接还是新连接的数据包。该功能在NAT环境下可能会出现严重问题。

连接池: 顾名思义,我们使用池子收集空闲的连接,而不是关闭它。这样就能减少TIME_WAIT状态的产生。这种解决方法适合特定的场景,比如数据库连接池。


本文完。

123
shunlqing

shunlqing

22 日志
7 分类
12 标签
E-Mail
© 2017 — 2019 shunlqing
本站访问量: 次 丨 本站访客: 人