TCP网络编程MSS细节

来源:互联网 发布:淘宝代购太平鸟真假 编辑:程序博客网 时间:2024/05/21 04:18

转自于http://blog.csdn.net/phunxm/article/details/5085869(有全文)

 

8I/O通信

I/O的角度来看,套接字也是文件,它提供了同文件读写(fread()/fwrite())对应的收发数据操作接口:send()/recv()

8.1 发送数据

8.1.1 send

// The send function sends data on a connected socket.

int send(

SOCKETs,// [in] Descriptor identifying a connected socket.

const char FAR *buf,// [in] Buffer containing the data to be transmitted.

int len,// [in] Length of the data in buf.

int flags// [in] Indicator specifying the way in which the call is made.

);

send()函数在一个已连接的套接字s上执行数据发送操作。对于客户机而言,发送的目标地址即connect()调用时所指定的地址;对于服务器而言,发送的目标地址即accept()调用所返回的地址。发送的内容为保存在缓冲区buf中,发送的内容长度为len。最后一个参数flags,通常情况下填0

send()函数只是将欲发送的内容从用户缓冲区拷贝到系统缓冲区(TCP Send Socket Buffer),系统的默认socket发送缓冲区(SO_SNDBUF)的大小为8K,我们可以调用setsockopt()将其更改,理论上最大为64KThe maximum congestion window is related to the amount of buffer space that the kernel allocates for each socket)。

只要系统缓冲区足够大,send()执行完拷贝立即返回实际拷贝的字节数。如果系统缓冲区不够大,例如在网络拥塞或带宽下降的情况下,用户大量地投递send()操作导致TCP Send Socket Buffer迅速充满,此时再调用send()操作,可能返回的值(即实际拷贝字节数)要小于我们传入的期待发送数量(len),在超时不得受理的情况下,返回SOCKET_ERRORWSAGetLastError()=WSAETIMEDOUT故大块的数据可能不能一次性发送完毕,通常需要检测send()返回值,多次调用send()直到发送完毕,可参考CSocket::Send()实现。关于发送超时限制(send timeout),可以调用setsockopt()SOL_SOCKET级别设置SO_SNDTIMEO选项值,以毫秒为单位。建议最多两分钟,因为TCP的MSL(Maximum Segment Lifetime)即为两分钟。

需要注意的是,用户可能短时间内需要发送多个小数据包,在TCP/IP中,Nagle算法要求主机等待数据积累到一定数量后或超过预定时间才发送。默认情况下实施Nagle算法,通信方会在向对方发送确认(ACK)信息之前,花费一定的时间来等待要传入的数据,这样,主机的就不必发送一个只有确认信息的数据报。发送小的数据包不仅没有多少意义,而且徒增错误检查和确认的开销。如果不想是使用Nagle算法,以保留发送边界,用户可调用setsockopt()函数在IPPROTO_TCP选项级别设置TCP_NODELAYTRUE。例如一次独立的HTTP GET请求往往希望保留发送边界,服务器的HTTP Response Header往往希望保留发送边界以区分后续的HTTP Response Content。体现在TCP层,即开启“PSH”选项。

具体的发送工作交由系统的传输层驱动程序完成。因为TCP提供可靠有序的传输机制,故我们总是很放心地认为它会将我们的数据发送到目的端。至于TCP分多少次将数据发送至对方,由协商的MSSMax Segment Size)和接收方的TCP Window决定。

8.1.2 sendto

// The sendto function sends data to a specific destination.

int sendto(

   SOCKETs,

   const char FAR *buf,

   int len,

   int flags,

   const struct sockaddr FAR *to,// [in] Optional pointer to the address of the target socket.

   int tolen// [in] Size of the address in to.

   );

sendto()函数只是比send()函数多出了一个目的地址信息参数,主要用于面向无连接的UDP通信。TCP套接字在建立连接(connect-accept)时,便知晓对方地址信息,而UDP套接字通信之前不建立连接,需要通信时,调用sendto()将消息发送给目的地址(to)。无论对方是否在指定端口监听sendto总是把数据发出去,要知道UDP是没有回应确认的。

注释中,sendto()函数的目标地址是“optional”,当我们忽略最后两个参数时,完全可以替换send()函数使用。实际上,这很方便我们在编程接口上提供统一。例如live555writeSocket接口针对TCPUDP套接字统一使用sendto()

由于UDP协议基本上只是在IP协议上做了简单的封装(Source Port+Destination Port+Length+Checksum),其没有做可靠性传输保障,故对UDP套接字一次sendto()的数据量不宜过大,最好以MTU为基准。使用UDP套接字往发送大数据块,往往因为IP分片等原因丢包,考虑异构网络及设备的MTU不同,一般一次发送512字节左右比较合适。

我们在一个UDP套接字上执行connect()操作,并未真正建立连接,而是执行一种目的地址绑定,事后我们可以使用send()函数替换sendto()函数。要取消UDP套接字与目的地址的关联,唯一的办法是在这个套接字上以INADDR_ANY为目标地址调用connect()

8.2 接收数据

8.2.1 recv

// The recv function receives data from a connected or bound socket.

int recv(

SOCKETs,// [in] Descriptor identifying a connected socket.

char FAR *buf,// [out] Buffer for the incoming data.

int len,// [in] Length of buf.

int flags// [in] Flag specifying the way in which the call is made.

);

recv()函数在一个已连接的套接字s上执行数据接收操作。对于客户机而言,数据的源地址即connect()调用时所指定的地址;对于服务器而言,数据的源地址即accept()调用所返回的地址。接收的内容为保存至长度为len的缓冲区buf,最后一个参数flags,通常情况下填0

recv()函数只是将TCP层当前接收到的数据流从系统缓冲区(TCP Receive Socket  Buffer)拷贝到用户缓冲区,系统的默认socket接收缓冲区(SO_RCVBUF)的大小为8K,我们可以调用setsockopt()将其更改,理论上最大为64KThe maximum congestion window is related to the amount of buffer space that the kernel allocates for each socket)。

recv()函数返回实际接收到的数据,可能小于缓冲区的长度len,可能当前到达的有效数据大于len,但最大返回len。在超时仍无数据到来的情况下,返回SOCKET_ERRORWSAGetLastError()=WSAETIMEDOUT关于接收超时限制(receive timeout),可以调用setsockopt()SOL_SOCKET级别设置SO_RCVTIMEO选项值,以毫秒为单位。建议最多两分钟,因为TCP的MSL(Maximum Segment Lifetime)即为两分钟。

如果对方不停发送数据,而本机过于繁忙疲于应付,则可能导致数据大量累积,一旦TCP Receive Socket BufferTCP Window充满,则可能产生数据溢出。TCP滑动窗口机制,由接收方建议性的控制发送量,即每一次确认回应(ACK)时都告知对方自己当前的接收能力(TCP窗口的大小),发送方据此有效地控制自己的发送行为,协调双方的通信步伐。

由于基于流的TCP协议,未保留消息边界(boundary)的概念,发送者发送的数据很快就会聚集在系统接收缓冲区(TCP堆栈)中。假设这样一种情景,客户端连接流媒体服务器(如IP摄像头)后,发送请求码流的请求,这以后服务器总是将连续不断地推送数据过来(如IP摄像头实时监控码流)。若客户端不执recv()拷贝操作而又尚未关闭连接,则服务器不断推送数据到客户端的TCP Stack,直至TCP window size=0

不管消息边界是否存在,接收端都会尽量地读取当前的有效数据。执行拷贝后,数据将立即从系统缓冲区删除,以释放部分TCP Window。因为流的无边界性,故用户投递了三个send(),可能接收端只需一次或两次recv()即接收完成。若客户三次send()的是结构化的数据,而接收端收到的是粘连在一起的一大坨数据或两块随机边界数据,这种情况即通常所说的TCP粘包问题。

具体的接收工作交由系统的传输层驱动程序完成。因为TCP提供可靠有序的传输机制,故我们总是很放心地认为它会将对方发送过来的数据正确的提交给我们。这里面的正确是指应用层面的报文结构及格式,即使TCP层面发生了偶然的丢包重传(retransmit out of order),但我们得到的仍然是对方提交的完整的报文。应用层协议就需要我们自己解析了。

粘包问题需要我们联合发送方,采取有效边界措施在应用层重组出正确的报文。例如,发送方往往在一个数据包的头4个字节告知对方接下来的数据有多少,这样接收方就能有效的执行接收,以保留边界和结构性。假设接收方得知发送方将发送32KB的数据过来,便投递一个32KB的缓冲区调用recv试图一次性接收完毕,这将以失败告终。实际上,发送方的TCP层将按MSS尺寸将TCP报文分解成很多个段(Segment)分多次发送给接收方。当然,它们往往具有相同的确认号(ack),以表示这些段是一个回应报文。这样,客户端才能识别出TCP segment of a reassembled PDU,以正确重组报文。可参考CSocket::Receive()实现。

8.2.2 recvfrom

// The recvfrom function receives a datagram and stores the source address.

int recvfrom(

SOCKETs,

char FAR*buf,

int len,

int flags,

struct sockaddr FAR *from,// [out] Optional pointer to a buffer that will hold the source address upon return.

int FAR *fromlen// [in, out] Optional pointer to the size of the from buffer.

);

recvfrom/recvsendto/send在行为学上同功,因为事先不知发送方为谁,故只要进来的通信,都将对方的地址保存在参数from中。值得注意的是,尽管UDP中没有TCP监听、连接等概念,但是作为接收方往往需要在本地某个端口上等待,这个端口必须是专用,约定用户预知的。故通常在调用recvfrom之前,必须显式调用bind()函数将UDP套接字关联到本地某个指定端口,进行监听

UDP通信是基于离散消息(message)的,故要么收到对方发送的消息包,要么整包丢失,接收方不得而知。如果整包丢失了,由于接收方不得而知,故没有反馈信息,也不会重发。这就是UDP通信的不可靠处。

live555中的readSocket接口针对TCPUDP套接字统一使用recvfrom

原创粉丝点击