TCP协议格式格式如下:
TCP报头当中各个字段的含义如下:
TCP报头当中的6位标志位:
如何分离?
当TCP从下次获取到报文时,尽管此时TCP并不知道报头的长度,但是前20字节是报文的基本报头是确定的,这20字节中包含了4位首部长度,因此就可以使用如下方式分离报头与有效荷载:
TCP报头当中的4位首部长度描述的基本单位是4字节,这也恰好是报文的宽度。4为首部长度的取值范围是0000 ~ 1111,因此TCP报头最大长度为15 × 4 = 60字节,因为基本报头的长度是20字节,所以报头中选项字段的长度最多是40字节。
如果TCP报头当中不携带选项字段,那么TCP报头的长度就是20字节,此时报头当中的4位首部长度的值就为20 ÷ 4 = 5,也就是0101。
如何交付?
其实交付的方式跟UDP一样的,因为每一个进程都会绑定一个端口号,客户端由操作系统动态进行绑定,服务端需要显示进行绑定,TCP协议中报头涵盖了目的端口号,我们可以读取目的端口号,进而找到相应的进程。
比如此时有两个人小花和小红,两个人在面对面进行交流,对方说话两个人都能听见,但是两个人相隔50米以后,此时再说话,对方也就无法听见了,这也就造成消息的不可靠性,其实本质上就是距离变长了,因此,要进行设备间的通信,就必须引入可靠性,如果要进行通信的各个设备相隔千里,,传输数据时出现错误的概率也会大大增高,此时要保证传输到对端的数据无误。
在网络中,并不存在100%可靠的协议,我们并不能保证发出去的消息被对方接收到了,但是在局部上我们却可以保证,只要对方对我们所发送到消息做出应答,我就能确保对方一定是收到了,而这就是TCP的确认应答机制,只要一个报文收到了对方的,我们就那保证发出的数据对方已经收到了,因此,TCP协议是一种可靠的协议。
TCP协议已经是可靠的了,为什么还会存在UDP协议呢?
因为网络通信具体采用TCP还是UDP取决于上层的场景,我们会发现不可靠和可靠是两个中性词,它所描述的是协议的特点。
我们最终选择何种协议,往往取决于上层的应用场景,如果场景严格要求数据传输的可靠性,就需要使用TCP协议,如果允许出现少量丢包等问题,就使用UDP协议。
32位序号
如果双方在进行通信的过程中,一方只有接收到了上一次发送数据的响应后才会在进行下一次数据的发送,就表明双方此时是串行的,效率也就非常低了。
因此双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了,此时也就能保证这些报文被对方收到了。
但在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的。但报文有序也是可靠性的一种,因此TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的。
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。
此时接收端收到了这三个TCP报文后,就可以根据TCP报头当中的32位序列号对这三个报文进行顺序重排(该动作在传输层进行),重排后将其放到TCP的接收缓冲区当中,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了。
注意:接收端在进行报文重排时,可以根据当前报文的32位序号与其有效载荷的字节数,进而确定下一个报文对应的序号。
32位确认序号
TCP报头当中的32位确认序号是告诉对端,我当前已经收到了哪些数据,你的数据下一次应该从哪里开始发。
当服务端收到客户端发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是服务端发给客户端的响应数据的报头当中的32位确认序号的值就会填成1001。
注意:
如果过程中出现报文丢失怎么办?
我们假设客户端向服务端发送了三个报文数据,32位报文序号分别是1,1001,2001,如果这三个报文在网络传输过程中出现了丢包,最终只有序号为1和2001的报文被客户端收到了,此时服务端在对客户端进行响应时,其响应报头当中的32位确认序号填的就是1001,告诉主机A下次向我发送数据时应该从序列号为1001的字节数据开始进行发送。
所以此时服务端向客户端发送的确认序号不可以是3001,因为如果是3001,就表明序号为1,1001,2001的报文都被收到了,但是此时序号为1001的报文并没有被收到,所以此时服务端只能给客户端响应1001,当客户端收到该确认序号后就会判定序号为1001的报文丢包了,此时客户端就可以选择进行数据重传。
为什么会存在序号和确认序号两套序号机制?
如果通信双方是半双工,一方发送数据,一方接收数据,一套序号机制是足够的:
TCP作为全双工通信,双方就可能会同时给对方发消息:
我们就可以总结出来序号与确认序号的作用是:
发送缓冲区和接收缓冲区
我们在学习UDP过程中接触到了接收缓冲区,UDP并不存在真正的发送缓冲区,但是对于TCP来说,是存在发送缓冲区和接收缓冲区的。
服务端与客户端之间的通信并不是直接发送至网络中的,而是通过对应发送和接收缓冲区保存起来,然后在进行数据的交互的。
TCP的发送缓冲区和接收缓冲区存在的意义
窗口大小
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被打满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。
因此TCP报头当中就有了16位的窗口大小,这个16位窗口大小当中填的是自身接收缓冲区中剩余空间的大小,也就是当前主机接收数据的能力。
接收端在对发送端发来的数据进行响应时,就可以通过16位窗口大小告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度。
此时我们也就可以理解,调用write/send/read/recv会阻塞的原因了:
标记为存在的原因
TCP的报文种类是多种多样的,存在常规报文,建立连接报文,断开连接报文,确认报文,其他类型报文。在接收到不同的报文就需要执行相应的动作,不同种类的报文对应的是不同的处理逻辑,所以我们要能够区分报文的种类。而TCP就是使用报头当中的六个标志字段来进行区分的,这六个标志位都只占用一个比特位,为0表示假,为1表示真。
SYN
该报文是一个连接请求报文,只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置。
FIN
该报文是一个断开连接的请求报文,只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置。
ACK
确认应答标志位,凡是该报文具有应答特征,该标志位会被设置为1。
RST
对连接进行重置,通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的RST标志位就会被置1,表示要求对方重新建立连接。如果通信中途发现之前建立好的连接出现了异常也会要求重新建立连接。
PSH
报文当中的PSH被设置为1,是在告诉对方尽快将你的接收缓冲区当中的数据交付给上层。
我们的接收缓冲区和发送缓冲区是存在一个水准线的概念的,当从接收缓冲区中recv/read数据时,只有当接收缓冲区中的数据到达这个水准线时才会read/recv数据,而当PSH被设置为1时,就是告诉操作系统,尽快将接收缓冲区内的数据交付给上层,尽管此时接收缓冲区中的数据并没达到我们的水准线,这也就是为什么我们平时recv/read期望的数据值与实际读取数据值不匹配的原因。
URG
URG是紧急标志位,由于双方进行网络通信过程中,数据被分成若干个TCP报文进行传输,由于32位序号的存在,保证了发送端发送的数据时有序的,接收端接收到的数据依然是有序的,但是有时候发送端发送了一些“紧急数据”,这些数据需要被上层应用马上读取,我们又该怎么办呢?
此时就需要我们的紧急标志位URG和16位紧急指针了:
双方通信的过程中,必定会存在大量的客户端去连接服务端,而操作系统对这些连接的管理方法就是“先描述在组织”,我们所说的连接,其实本质就是内核中的一种数据结构类型,建立连接成功的时候,就是在内存中创建对应的连接对象,最终再将这些连接以某种数据结构组织起来,此时操作系统对连接的管理就变成了对该数据结构的增删查改。
三次握手的过程:
双方在进行TCP通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手。
客户端想要与服务端之间进行通信时,需要与服务端先建立连接,此时客户端作为主动方会先向服务器发送连接建立请求,然后双方TCP在底层会自动进行三次握手。
注意:TCP作为全双工通信,三次握手对客户端服务端均会起效。
为什么是三次握手?不是一次,两次,四次呢?
建立连接的过程不一定是百分百成功的,通信双方前两次的握手是可以保证对方收到的,因为前两次握手都有对应的下一次握手对其进行响应,但第三次握手是没有对应的响应报文的,如果第三次握手时客户端发送的ACK报文丢失了,那么连接建立就会失败。
客户端再发起第三次握手就会认为自己已经完成了握手,但是如果此时服务器并没有收到客户端发来的第三次握手,此时服务端就不会建立对应的连接,所以建立连接时不管采用几次握手,最后一次握手的可靠性都是不能保证的。
既然连接的建立都不是百分之百成功的,因此建立连接时具体采用几次握手的依据,实际是看几次握手时的优点更多。
首先我们来看一次握手,以客户端向服务端发起连接请求为例,一次握手就表明客户端只向服务端发起建立连接的请求,服务端并不会进行响应,也就说明客户端一旦发出建立连接的请求就已经认为自己连接成功了,就会继续进行下一次连接,最终就会导致服务端被连接请求挤满,这种现象就叫做“SYN洪流”。显然,一次握手并不可取。
同样,两次握手也是,尽管第一次握手会发生响应,但是第二次握手并没有响应,而且此时的异常连接是挂在服务端的,服务端就需要进行维护,因为我们可能是多个服务器之间进行通信,所以服务端的维护就需要大量的成本。这也是不行的。
而我们的三次握手正好解决了上面的问题:
同时,三次握手也验证了TCP是全双工通信:
三次握手就保证连接建立时的异常连接挂在客户端,同时也验证了TCP是全双工通信的,是验证双方通信信道的最小次数,所以我们可以用三次握手来解决的事情,也就没必要进行对此验证了。
三次握手时的状态变化
套接字和三次握手之间的关系
四次挥手的过程
当服务端与客户端完成通信以后,就会结束通信,结束通信就会四次挥手。
四次挥手结束后连接方可断开成功。
为什么是四次挥手?
四次挥手时的状态变化
套接字和四次挥手之间的关系
CLOSE_WAIT
TIME_WAIT
虽然四次挥手已经完成,但是主动断开连接的一方会维持一段时间的TIME_WAIT状态,在该状态下,其实连接已经释放,但是ip,port依然被占用。
TIME_WAIT状态存在的必要性:
TIME_WAIT的等待时长是多少?
TIME_WAIT的等待时长既不能太长也不能太短。
TIME_WAIT的等待时长设置为两个MSL的原因:
解决TIME_WAIT状态引起的bind失败的方法
使用setsockopt()
设置socket
描述符的选项SO_REUSEADDR
为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符;
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
确认应答机制就是由TCP报头当中的,32位序号和32位确认序号来保证的。确认应答机制不是保证双方通信的全部消息的可靠性,而是通过收到对方的应答消息,来保证自己曾经发送给对方的某一条消息被对方可靠的收到了。
如何理解缓冲区?
TCP是面向字节流的,我们可以将TCP的发送缓冲区和接收缓冲区都想象成一个字符数组。
双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制。
需要注意的是,TCP保证双方通信的可靠性,一部分是通过TCP的协议报头体现出来的,还有一部分是通过实现TCP的代码逻辑体现出来的。
比如超时重传机制实际就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传,这就是通过TCP的代码逻辑实现的,而在TCP报头当中是体现不出来的。
丢包两种情况
丢包的一种情况就是发送端的数据报文丢失了,发送端在一定时间内收不到对应的响应报文,会进行超时空重传。
丢包的另一种情况是对方发过来的响应报文丢失了,此时发送端也会因为收不到对应的响应报文,而进行超时重传。
超时空重传时间如何设置?
超时空重传时间时间不能设置的太长也不能设置的太短:
因此超时重传的时间一定要是合理的,最理想的情况就是找到一个最小的时间,保证“确认应答一定能在这个时间内返回”。但这个时间的长短,是与网络环境有关的。网好的时候重传的时间可以设置的短一点,网卡的时候重传的时间可以设置的长一点,也就是说超时重传设置的等待时间一定是上下浮动的,因此这个时间不可能是固定的某个值。
TCP为了保证无论在任何环境下都能有比较高性能的通信,因此会动态计算这个最大超时时间。
TCP支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制(Flow Control)。
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,此时发送端继续发送数据,就会造成丢包,进而引起丢包重传等一系列连锁反应。
因此接收端可以将自己接收数据的能力告知发送端,让发送端控制自己发送数据的速度。
当发送端得知接收端接收数据的能力为0时会停止发送数据,此时发送端会通过以下两种方式来得知何时可以继续发送数据。
16为数字最大表示65535,那TCP窗口最大就是65535吗?
理论上确实是这样的,但实际上TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位得到的。
第一次向对方发送数据时如何得知对方的窗口大小?
双方在进行TCP通信之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力,所以双方在发送数据时是不会出现缓冲区溢出的问题的。
连续发送多个数据
双方在进行TCP通信时可以一次向对方发送多条数据,这样可以将等待多个响应的时间重叠起来,进而提高数据通信的效率。
虽然双方在进行TCP通信时可以一次向对方发送大量的报文,但不能将自己发送缓冲区当中的数据全部打包发送给对端,在发送数据时还要考虑对方的接收能力。
发送方可以一次发送多个报文给对方,此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的。
发送缓冲区当中的数据分为三部分:
这里发送缓冲区的第二部分就叫做滑动窗口。
滑动窗口描述的就是,发送方不用等待ACK一次所能发送的数据最大量。
滑动窗口存在的最大意义就是可以提高发送数据的效率:
当发送方发送出去的数据段陆陆续续收到对应的ACK时,就可以将收到ACK的数据段归置到滑动窗口的左侧,并根据当前滑动窗口的大小来决定,是否需要将滑动窗口右侧的数据归置到滑动窗口当中。
TCP的重传机制要求暂时保存发出但未收到确认的数据,而这部分数据实际就位于滑动窗口当中,只有滑动窗口左侧的数据才是可以被覆盖或删除的,因为这部分数据才是发送并被对方可靠的收到了,所以滑动窗口除了限定不收到ACK而可以直接发送的数据之外,滑动窗口也可以支持TCP的重传机制。
滑动窗口一定会整体右移吗?
滑动窗口不一定是整体右移的,比如收到2001这个ACK以后,就表明1001-2001这段数据已经被对方收到,但是对方上层一直不从接收缓冲区中读取数据,此时滑动窗口的大小就由4000变为了3000。
当发送端收到响应ACK2001以后,就会将1001-2000这个数据段置为滑动窗口左侧,但是由于此时对方的接收能力已经变为了3000,1001-2000这个数据段置为滑动窗口左侧以后窗口大小刚好为3000,此时滑动窗口就不能继续右移了。
如何实现滑动窗口
TCP接收和发送缓冲区都看作一个字符数组,而滑动窗口实际就可以看作是两个指针限定的一个范围,比如我们用start指向滑动窗口的左侧,end指向的是滑动窗口的右侧,此时在start和end区间范围内的就可以叫做滑动窗口。
当发送方收到对方响应时,我们假设确认序号为x,窗口大小为win,此时start就变为x,end就变为了start+win。
丢包问题
发送端一次发送多个报文数据时,此时的丢包情况也可以分为两种。
上面这种部分确认ACK丢失并没多大问题,我们可以通过后序的ACK来进行确认,比如1-1001,2001-3000这两个数据段对应ACK丢失了,但是发送方最终接收到3001-4001这个数据段的响应,此时发送端就会认为4001之前的数据段都收到了,确认序号4001就表示1-4000的字节数据都收到了,下一次应该从序号4001开始发送数据。
这种机制被称为“高速重发控制”,也叫做“快重传”。
需要注意的是,快重传需要在大量的数据重传和个别的数据重传之间做平衡,实际这个例子当中发送端并不知道是1001-2000这个数据包丢了,当发送端重复收到确认序号为1001的响应报文时,理论上发送端应该将1001-4000的数据全部进行重传,但这样可能会导致大量数据被重复传送,所以发送端可以尝试先把1001-2000的数据包进行重发,然后根据重发后的得到的确认序号继续决定是否需要重发其它数据包。
滑动窗口中的数据一定都没有被对方收到吗?
滑动窗口的数据并都是对方没有收到的数据,因为可能某一个数据段在传输的过程中出现丢包了,但是仅仅只有这个数据段的数据对端并没有收到,其他数据段的数据已经收到了,比如滑动窗口最左端出现了丢包,后面的数据都接收到了,但是并没有响应而已。
例如1001-2000数据段在传输过程中丢包了,此时2001-5000的数据虽然对方收到了,但是并没有响应,发送方接收到的确认序号依然是1001,当发送端对1001-2000数据段进行重发以后,确认序号就变为了5001,此时缓冲区当中1001-5000的数据就会被立马移动至滑动窗口左侧。
快重传 VS 超时重传
为什么会有拥塞控制?
双方进行通信的过程中,出现少量丢包的情况是允许的,此时触发快重传机制和超时空重传机制即可,但是如果出现大量丢包的情况,这种情况就不正常了。
TCP进行通信的过程,不仅仅考虑了双端主机的问题,还考虑了网络的问题。
如果出现大量的丢包情况,此时TCP就不会再推测是否是双方发送或者是接收数据出了问题,而是判断双方通信网络出现了拥塞问题。
如何解决网络拥塞问题?
当网络出现大面积瘫痪时,影响到的并不是一两台主机,几乎大部分主机都会被影响到,同样,造成网络大面积瘫痪,也不是一两台主机可以做到的,是大部分主机共同作用的结果。
拥塞控制
虽然滑动窗口能够高效可靠的发送大量的数据,但如果在刚开始阶段就发送大量的数据,就可能会引发某些问题。因为网络上有很多的计算机,有可能当前的网络状态就已经比较拥塞了,因此在不清楚当前网络状态的情况下,贸然发送大量的数据,就可能会引起网络拥塞问题。
因此TCP引入了慢启动机制,在刚开始通信时先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
我们会发现,拥塞窗口是以指数级方式增长的,原因就是慢启动实际上只是初始的时候比较慢,我们是为了探测网络拥塞的阈值,当网络拥塞恢复以后,我们就需要尽快的恢复通信的过程,所以就需要加快速度。
但是我们又不能一直以指数方式进行增长,可能又会导致网络拥塞:
如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。
延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。
捎带应答其实是TCP通信时最常规的一种方式,就好比主机A给主机B发送了一条消息,当主机B收到这条消息后需要对其进行ACK应答,但如果主机B此时正好也要给主机A发生消息,此时这个ACK就可以搭顺风车,而不用单独发送一个ACK应答,此时主机B发送的这个报文既发送了数据,又完成了对收到数据的响应,这就叫做捎带应答。
捎带应答最直观的角度实际也是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了。
此外,由于捎带应答的报文携带了有效数据,因此对方收到该报文后会对其进行响应,当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了,同时也能确保捎带的ACK应答也被对方可靠的收到了。
当创建一个TCP的socket时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
什么是粘包?
如何解决粘包问题
要解决粘包问题,本质就是要明确报文和报文之间的边界。
UDP是否存在粘包问题?
UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
进程终止
当一个进程退出,该进程打开的文件描述符会自动关闭,也就是说,客户端进程退出以后,就会调用close函数关闭对应的文件描述符,双方在底层也就会完成四次挥手,断开双方链接,也就是说,进程终止和进程正常退出没什么区别。
机器重启
当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。
机器掉电/网线断开
当客户端掉线后,服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。
TCP协议这么复杂就是因为TCP既要保证可靠性,同时又尽可能的提高性能。
可靠性:
提高性能:
需要注意的是,TCP的这些机制有些能够通过TCP报头体现出来的,但还有一些是通过代码逻辑体现出来的。
当然, 也包括你自己写TCP程序时自定义的应用层协议
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单,绝对的进行比较
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。
参考TCP的可靠性机制, 在应用层实现类似的逻辑。
例如:
我们在前面学习套接字的过程中,对于listen的第二个参数并没有详细的讲解,今天我们就来重新认识一下它,我们以一段代码进行测试:
Sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
class Sock
{
private:
const static int gbacklog = 1;
public:
Sock()
{
}
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(ERROR, "create socket error:%d:%s", errno, strerror(errno));
exit(0);
}
logMessage(NORMAL, "create socket success, listensock:%d", listensock);
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return listensock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(ERROR, "bind error:%d:%s", errno, strerror(errno));
exit(1);
}
}
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(ERROR, "listen error:%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init server success...");
}
int Accept(int listensock, uint16_t *port, std::string *ip)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error:%d:%s", errno, strerror);
exit(3);
}
if (port)
*port = htons(src.sin_port);
if (ip)
*ip = inet_ntoa(src.sin_addr);
return servicesock;
}
bool Connect(int sock, const uint16_t &server_port, const std::string &server_ip)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
socklen_t len = sizeof(server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if (connect(sock, (struct sockaddr *)&server, len) == 0)
return true;
else
return false;
}
~Sock()
{
}
};
main.cc
#include
#include "Sock.hpp"
int main()
{
Sock sock;
int listensock = sock.Socket();
sock.Bind(listensock, 8080);
sock.Listen(listensock);
while (true)
{
sleep(1);
}
return 0;
}
我们将listen第二个参数设置为1,运行程序,调用netstat命令,我们可以发现,启动 2个客户端同时连接服务器,查看服务器状态,一切正常。但是启动第3个客户端时, 发现服务器对于第3个连接的状态存在问题了,此时变为了SYN_RECV状态。
客户端状态正常,但是服务器端出现了 SYN_RECV 状态,而不是 ESTABLISHED 状态。
这是因为,Linux内核协议栈为一个tcp连接管理使用两个队列:
而全连接队列的长度会受到 listen 第二个参数的影响,全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了。这个队列的长度通过上述实验可知,是 listen 的第二个参数 + 1。
服务器本身就会维护连接队列,这个连接队列不能没有也不能太长,他就和listen的第二个函数存在关系。