unix的哲学是一切皆文件,可以把socket看成是一种特殊的文件,而一些socket函数就是对其进行的操作api(读/写IO、打开、关闭)。我们知道普通文件的打开操作(open)返回一个文件描述字,与之类似,socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,
sockfd即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
在将一个地址绑定到socket的时候,需要先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过不少血案,谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
这里的主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
listen函数的第一个参数即为要监听的socket描述字,第二个参数为socket可以接受的排队的最大连接个数。listen函数表示等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数去接收请求,这样连接就建立好了(在connect之后就建立好了三次连接),之后就可以开始进行类似于普通文件的网络I/O操作了。
如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与客户的TCP连接。
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,类似于操作完打开的文件要调用fclose关闭打开的文件。
close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
socket中TCP的四次握手释放连接详解
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。一段时间之后,服务端调用close关闭它的socket。这导致它的TCP也发送一个FIN N;接收到这个FIN的源发送端TCP对它进行确认,这样每个方向上都有一个FIN和ACK。
为什么要三次握手
由于tcp连接是全双工的,存在着双向的读写通道,每个方向都必须单独进行关闭。当一方完成它的数据发送任务后就可以发送一个FIN来终止这个方向的连接。收到FIN只意味着这个方向上没有数据流动,但并不表示在另一个方向上没有读写,所以要双向的读写关闭需要四次握手,
3. time_wait状态如何避免?
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
1.客户端连接服务器的80服务,这时客户端会启用一个本地的端口访问服务器的80,访问完成后关闭此连接,立刻再次访问服务器的
80,这时客户端会启用另一个本地的端口,而不是刚才使用的那个本地端口。原因就是刚才的那个连接还处于TIME_WAIT状态。
2.客户端连接服务器的80服务,这时服务器关闭80端口,立即再次重启80端口的服务,这时可能不会成功启动,原因也是服务器的连
接还处于TIME_WAIT状态。
实战分析:
状态描述:
CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
ITMED_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT:另一边已初始化一个释放
LAST_ACK:等待所有分组死掉</pre>
命令解释:
如何尽量处理TIMEWAIT过多?
编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间</pre>
然后执行 /sbin/sysctl -p 让参数生效.
/etc/sysctl.conf是一个允许改变正在运行中的Linux系统的接口,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项,修改内核参数永久生效。
简单来说,就是打开系统的TIMEWAIT重用和快速回收。
本文主要讲述了socket的主要api,以及tcp的连接过程和其中各个阶段的连接状态,理解这些是更深入了解tcp的基础!
目录:
以前我也认为TCP是相当底层的东西,我永远不需要去了解它。虽然差不多是这样,但是实际生活中,你依然可能遇见和TCP算法相关的bug,这时候懂一些TCP的知识就至关重要了。( 本文也可以引申为,系统调用,操作系统这些都很重要,这个道理适用于很多东西 )
这里推荐一篇小短文, 人人都应该懂点TCP
使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数据结构,比如连接的状态、读写缓冲区、定时器等。当通信结束时,双方必须关闭连接以释放这些内核数据。TCP服务基于流,源源不断从一端流向另一端,发送端可以逐字节写入,接收端可以逐字节读出,无需分段。
需要注意的几点:
TCP状态(11种):
eg.
以上为TCP三次握手的状态变迁
以下为TCP四次挥手的状态变迁
服务器通过 listen 系统调用进入 LISTEN 状态,被动等待客户端连接,也就是所谓的被动打开。一旦监听到SYN(同步报文段)请求,就将该连接放入内核的等待队列,并向客户端发送带SYN的ACK(确认报文段),此时该连接处于 SYN_RECVD 状态。如果服务器收到客户端返回的ACK,则转到 ESTABLISHED 状态。这个状态就是连接双方能进行全双工数据传输的状态。
而当客户端主动关闭连接时,服务器收到FIN报文,通过返回ACK使连接进入 CLOSE_WAIT 状态。此状态表示——等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接之后,也会立即给客户端发送一个FIN来关闭连接,使连接转移到 LAST_ACK 状态,等待客户端对最后一个FIN结束报文段的最后一次确认,一旦确认完成,连接就彻底关闭了。
客户端通过 connect 系统调用主动与服务器建立连接。此系统调用会首先给服务器发一个SYN,使连接进入 SYN_SENT 状态。
connect 调用可能因为两种原因失败:1. 目标端口不存在(未被任何进程监听)护着该端口被 TIME_WAIT 状态的连接占用( 详见后文 )。2. 连接超时,在超时时间内未收到服务器的ACK。
如果 connect 调用失败,则连接返回初始的 CLOSED 状态,如果调用成功,则转到 ESTABLISHED 状态。
客户端执行主动关闭时,它会向服务器发送一个FIN,连接进入 TIME_WAIT_1 状态,如果收到服务器的ACK,进入 TIME_WAIT_2 状态。此时服务器处于 CLOSE_WAIT 状态,这一对状态是可能发生办关闭的状态(详见后文)。此时如果服务器发送FIN关闭连接,则客户端会发送ACK进行确认并进入 TIME_WAIT 状态。
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。 流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四种算法来进行拥塞控制: 慢开始、拥塞避免、快重传、快恢复。
在Linux下有多种实现,比如reno算法,vegas算法和cubic算法等。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢开始。
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
发送端的每个TCP报文都必须得到接收方的应答,才算传输成功。
TCP为每个TCP报文段都维护一个重传定时器。
发送端在发出一个TCP报文段之后就启动定时器,如果在定时时间类未收到应答,它就将重发该报文段并重置定时器。
因为TCP报文段最终在网络层是以IP数据报的形式发送,而IP数据报到达接收端可能是乱序或者重复的。TCP协议会对收到的TCP报文进行重排、整理,确保顺序正确。
TCP报文段所携带的应用程序数据按照长度分为两种: 交互数据 和 成块数据
对于什么是粘包、拆包问题,我想先举两个简单的应用场景:
对于第一种情况,服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理...。对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看 第二种情况 下客户端发送的两条消息递交到服务端有可能出现的情况:
第一种情况:
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。
第二种情况:
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。
第三种情况:
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。
我们知道tcp是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。换算成字节,也就是180多字节。
tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
发生TCP粘包、拆包主要是由于下面一些原因:
既然知道了tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:
写了一个简单的 golang 版的tcp服务器实例,仅供参考:
例子
参考和推荐阅读书目:
注释:
eg.
欢迎分享,转载请注明来源:夏雨云
评论列表(0条)