更新时间:2025 08 13 19:37:39 作者 :庆美网 围观 : 42次
大家好,感谢邀请,今天来为大家分享一下socketaccept-socketaccept阻塞的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!
2. 服务器独有的:(1)LISTEN (2)SYN_RCVD (3)CLOSE_WAIT (4)LAST_ACK 。
3. 共有的:(1)CLOSED (2)ESTABLISHED 。
推荐视频
学习地址:
LISTEN – 侦听来自远方TCP端口的连接请求;
SYN-SENT -在发送连接请求后等待匹配的连接请求;
SYN-RECEIVED – 在收到和发送一个连接请求后等待对连接请求的确认;
ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;
FIN-WAIT-1 – 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
FIN-WAIT-2 – 从远程TCP等待连接中断请求;
CLOSE-WAIT – 等待从本地用户发来的连接中断请求;
CLOSING -等待远程TCP对连接中断的确认;
LAST-ACK – 等待原来发向远程TCP的连接中断请求的确认;
TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;
CLOSED – 没有任何连接状态;
LISTENING :侦听来自远方的TCP端口的连接请求 .
首先服务端需要打开一个 socket 进行监听,状态为LISTEN。
有提供某种服务才会处于LISTENING状态, TCP状态变化就是某个端口的状态变化,提供一个服务就打开一个端口,例如:提供www服务默认开的是80端口,提供ftp服务默认的端口为21,当提供的服务没有被连接时就处于LISTENING状态。FTP服务启动后首先处于侦听(LISTENING)状态。处于侦听LISTENING状态时,该端口是开放的,等待连接,但还没有被连接。就像你房子的门已经敞开的,但还没有人进来。
看LISTENING状态最主要的是看本机开了哪些端口,这些端口都是哪个程序开的,关闭不必要的端口是保证安全的一个非常重要的方面,服务端口都对应一个服务(应用程序),停止该服务就关闭了该端口,例如要关闭21端口只要停止IIS服务中的FTP服务即可。关于这方面的知识请参阅其它文章。
如果你不幸中了服务端口的木马,木马也开个端口处于LISTENING状态。
SYN-SENT: 客户端SYN_SENT状态:
在客户端发送连接请求后,等待匹配的连接请求:
客户端通过应用程序调用connect进行active open.于是客户端tcp发送一个SYN以请求建立一个连接.之后状态置为SYN_SENT. /*The socket is actively attempting to establish a connection. 在发送连接请求后等待匹配的连接请求 */
当请求连接时客户端首先要发送同步信号给要访问的机器,此时状态为SYN_SENT,如果连接成功了就变为ESTABLISHED,正常情况下SYN_SENT状态非常短暂。例如要访问网站http://www.baidu.com,如果是正常连接的话,用TCPView观察 IEXPLORE .EXE(IE)建立的连接会发现很快从SYN_SENT变为ESTABLISHED,表示连接成功。SYN_SENT状态快的也许看不到。
如果发现有很多SYN_SENT出现,那一般有这么几种情况,一是你要访问的网站不存在或线路不好,二是用扫描软件扫描一个网段的机器,也会出出现很多SYN_SENT,另外就是可能中了病毒了,例如中了\”冲击波\”,病毒发作时会扫描其它机器,这样会有很多SYN_SENT出现。
SYN-RECEIVED: 服务器端状态SYN_RCVD
再收到和发送一个连接请求后等待对方对连接请求的确认。当服务器收到客户端发送的同步信号时,将标志位ACK和SYN置1发送给客户端,此时服务器端处于SYN_RCVD状态,如果连接成功了就变为ESTABLISHED,正常情况下SYN_RCVD状态非常短暂。如果发现有很多SYN_RCVD状态,那你的机器有可能被SYN Flood的DoS(拒绝服务攻击)攻击了。
SYN Flood的攻击原理是:
在进行三次握手时,攻击软件向被攻击的服务器发送SYN连接请求(握手的第一步),但是这个地址是伪造的,如攻击软件随机伪造了51.133.163.104、65.158.99.152等等地址。 服务器 在收到连接请求时将标志位 ACK和 SYN 置1发送给客户端(握手的第二步),但是这些客户端的IP地址都是伪造的,服务器根本找不到客户机,也就是说握手的第三步不可能完成。
这种情况下服务器端一般会重试(再次发送SYN+ACK给客户端)并等待一段时间后丢弃这个未完成的连接,这段时间的长度我们称为SYN Timeout,一般来说这个时间是分钟的数量级(大约为30秒-2分钟);一个用户出现异常导致服务器的一个线程等待1分钟并不是什么很大的问题,但如果有一个恶意的攻击者大量模拟这种情况,服务器端将为了维护一个非常大的半连接列表而消耗非常多的资源—-数以万计的半连接,即使是简单的保存并遍历也会消耗非常多的 CPU 时间和内存,何况还要不断对这个列表中的IP进行SYN+ACK的重试。此时从正常客户的角度看来,服务器失去响应,这种情况我们称做: 服务器端受到了SYN Flood攻击(SYN洪水攻击 )
ESTABLISHED:代表一个打开的连接。
ESTABLISHED状态是表示两台机器正在传输数据,观察这个状态最主要的就是看哪个程序正在处于ESTABLISHED状态。
服务器出现很多 ESTABLISHED状态: netstat -nat |grep 9502或者使用lsof -i:9502可以检测到。
当客户端未主动close的时候就断开连接:即客户端发送的FIN丢失或未发送。
这时候若客户端断开的时候发送了FIN包,则服务端将会处于CLOSE_WAIT状态;
这时候若客户端断开的时候未发送FIN包,则服务端处还是显示ESTABLISHED状态;
结果客户端重新连接服务器。
而新连接上来的客户端(也就是刚才断掉的重新连上来了)在服务端肯定是ESTABLISHED; 如果客户端重复的上演这种情况,那么服务端将会出现大量的假的ESTABLISHED连接和CLOSE_WAIT连接。
最终结果就是新的其他客户端无法连接上来,但是利用netstat还是能看到一条连接已经建立,并显示ESTABLISHED,但始终无法进入程序代码。
FIN-WAIT-1: 等待远程TCP连接中断请求,或先前的连接中断请求的确认
主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态./* The socket is closed, and the connection is shutting down. 等待远程TCP的连接中断请求,或先前的连接中断请求的确认 */
FIN-WAIT-2:从远程TCP等待连接中断请求
主动关闭端接到ACK后,就进入了FIN-WAIT-2 ./* Connection is closed, and the socket is waiting for a shutdown from the remote end. 从远程TCP等待连接中断请求 */
这就是著名的半关闭的状态了,这是在关闭连接时,客户端和服务器两次握手之后的状态。在这个状态下,应用程序还有接受数据的能力,但是已经无法发送数据,但是也有一种可能是,客户端一直处于FIN_WAIT_2状态,而服务器则一直处于WAIT_CLOSE状态,而直到应用层来决定关闭这个状态。
CLOSE-WAIT:等待从本地用户发来的连接中断请求
被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT. /* The remote end has shut down, waiting for the socket to close. 等待从本地用户发来的连接中断请求 */
CLOSING:等待远程TCP对连接中断的确认
比较少见./* Both sockets are shut down but we still don\’t have all our data sent. 等待远程TCP对连接中断的确认 */
LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认
被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个 FIN,等待对方的ACK.就进入了LAST-ACK . /* The remote end has shut down, and the socket is closed. Waiting for acknowledgement. 等待原来发向远程TCP的连接中断请求的确认 */
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认
在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态。/* The socket is waiting after close to handle packets still in the network.等待足够的时间以确保远程TCP接收到连接中断请求的确认 */
TIME_WAIT等待状态,这个状态又叫做2MSL状态,说的是在TIME_WAIT2发送了最后一个ACK数据报以后,要进入TIME_WAIT状态,这个状态是防止最后一次握手的数据报没有传送到对方那里而准备的(注意这不是四次握手,这是第四次握手的保险状态)。这个状态在很大程度上保证了双方都可以正常结束,但是,问题也来了。
由于插口的2MSL状态(插口是IP和端口对的意思,socket),使得应用程序在2MSL时间内是无法再次使用同一个插口的,对于客户程序还好一些,但是对于服务程序,例如httpd,它总是要使用同一个端口来进行服务,而在2MSL时间内,启动httpd就会出现错误(插口被使用)。为了避免这个错误,服务器给出了一个平静时间的概念,这是说在2MSL时间内,虽然可以重新启动服务器,但是这个服务器还是要平静的等待2MSL时间的过去才能进行下一次连接。
CLOSED:没有任何连接状态
被动关闭端在接受到ACK包后,就进入了closed的状态。连接结束./* The socket is not being used. 没有任何连接状态 */
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg,大厂面试题 等)
大家对netstat -a命令很熟悉,但是,你注意到STATE一栏没,基本上显示着established,time_wait,close_wait等,这些到底是 什么意思呢?
大家很明白TCP初始化连接三次握手吧:发SYN包,然后返回SYN/ACK包,再发ACK包,连接正式建立。但是这里有点出入,当请求者收到SYS /ACK包后,就开始建立连接了,而被请求者第三次握手结束后才建立连接。但是大家明白关闭连接的工作原理吗?关闭连接要四次握手:发FIN包,ACK 包,FIN包,ACK包,四次握手!!为什么呢,因为TCP连接是全双工,我关了你的连接,并不等于你关了我的连接。
客户端正常情况下TCP状态迁移:
服务器正常情况下TCP状态迁移:
当客户端开始连接时,服务器还处于LISTENING,
客户端发一个SYN包后,他就处于SYN_SENT状态,服务器就处于SYS收到状态,
然后互相确认进入连接状态ESTABLISHED.
当客户端请求关闭连接时,客户端发送一个FIN包后,客户端就进入FIN_WAIT_1状态,等待对方的确认包,
服务器发送一个ACK包给客户,客户端收到ACK包后结束FIN_WAIT_1状态,进入FIN_WAIT_2状态,等待服务器发过来的关闭请求,
服务器发一个FIN包后,进入CLOSE_WAIT状态,
当客户端收到服务器的FIN包,FIN_WAIT_2状态就结束,然后给服务器端的FIN包给以一个确认包,客户端这时进入TIME_WAIT,
当服务器收到确认包后,CLOSE_WAIT状态结束了,
这时候服务器端真正的关闭了连接.但是客户端还在TIME_WAIT状态下,
什么时候结束呢.我在这里再讲到一个新名词:2MSL等待状态,其实TIME_WAIT就是2MSL等待状态,
为什么要设置这个状态,原因是有足够的时间让ACK包到达服务器端,如果服务器端没收到ACK包,超时了,然后重新发一个FIN包,直到服务器收到ACK 包.
TIME_WAIT状态等待时间是在TCP重新启动后不连接任何请求的两倍.
大家有没有发现一个问题:如果对方在第三次握手的时候出问题,如发FIN包的时候,不知道什么原因丢了这个包,然而这边一直处在FIN_WAIT_2状 态,而且TCP/IP并没有设置这个状态的过期时间,那他一直会保留这个状态下去,越来越多的FIN_WAIT_2状态会导致系统崩溃.
上面我碰到的这个问题主要因为TCP的结束流程未走完,造成连接未释放。现设客户端主动断开连接,流程如下:
由于Server的Socket在客户端已经关闭时而没有调用关闭,造成服务器端的连接处在“挂起”状态,而客户端则处在等待应答的状态上。
此问题的典型特征是:一端处于FIN_WAIT2 ,而另一端处于CLOSE_WAIT。不过,根本问题还是程序写的不好,有待提高
—————————————————————————————————————-
CLOSE_WAIT,TCP的癌症,TCP的朋友。
CLOSE_WAIT状态的生成原因
首先我们知道,如果我们的服务器程序APACHE处于CLOSE_WAIT状态的话,说明套接字是被动关闭的!
因为如果是CLIENT端主动断掉当前连接的话,那么双方关闭这个TCP连接共需要四个packet:
Client —>FIN —>Server
Client<— ACK<— Server
这时候Client端处于FIN_WAIT_2状态;而Server 程序处于CLOSE_WAIT状态。
Client<— FIN<— Server
这时Server 发送FIN给Client,Server 就置为LAST_ACK状态。
Client —>ACK —>Server
Client回应了ACK,那么Server 的套接字才会真正置为CLOSED状态。
Server 程序处于CLOSE_WAIT状态,而不是LAST_ACK状态,说明还没有发FIN给Client,那么可能是在关闭连接之前还有许多数据要发送或者其 他事要做,导致没有发这个FIN packet。
通常来说,一个CLOSE_WAIT会维持至少2个小时的时间。如果有个流氓特地写了个程序,给你造成一堆的 CLOSE_WAIT,消耗你的资源,那么通常是等不到释放那一刻,系统就已经解决崩溃了。
只能通过修改一下TCP/IP的参数,来缩短这个时间:修改tcp_keepalive_*系列参数有助于解决这个 问题。
解决这个问题的方法是修改系统的参数,系统默认超时时间的是7200秒,也就是2小时, 这个太大了,可以修改如下几个参数:
然后,执行sysctl命令使修改生效。
连接进程是通过一系列状态表示的,这些状态有:
LISTEN,SYN-SENT,SYN-RECEIVED,ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,CLOSE- WAIT,CLOSING,LAST-ACK,TIME-WAIT和CLOSED
1、建立连接协议(三次握手)
(1)客户 端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标 志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通 讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3。
2、连接终止协议(四次握手)
由于TCP连 接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终 止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接 在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客 户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一 样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。
(4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处 于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报 文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手 过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文 后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报 文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况 下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点 数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报 文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标 志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发 送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报 文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报 文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一 个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一 个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文 给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话, 那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报 文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
最后有2个问题 的回答,我自己分析后的结论(不一定保证100%正确)
1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起 应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文 通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文 和FIN报文多数情况下都是分开发送的。
2、 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状 态?
这是因为:虽然双方 都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状 态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报 文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报 文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报 文,并保证于此。
断开连接的时候, 当发起主动关闭的左边这方发送一个FIN过去后,
右边被动关闭的这方要回应一个ACK,这个ACK是TCP回应的,而不是应用程序发送的,
此时,被动关闭的一方就处于CLOSE_WAIT状态了。
如果此时被动关闭的这一方不再继续调用closesocket,那么他就不会发送接下来的FIN,导致自己老是处于CLOSE_WAIT。
只有被动关闭的这一方调用了 closesocket,才会发送一个FIN给主动关闭的这一方,同时也使得自己的状态变迁为LAST_ACK。
比如被动关闭的是客户端
当对方调用closesocket的时候,你的程序正在
很多人就是忘记了那句closesocket,这种代码太常见了。
我的理解,
当主动关闭的一方发送FIN到被动关闭这边后,被动关闭这边的TCP马上回应一个ACK过去,同时向上面应用程序提交一个ERROR,
导致上面的SOCKET的send或者recv返回SOCKET_ERROR.
正常情况下,如果上面在返回SOCKET_ERROR后调用了closesocket, 那么被动关闭的者一方的TCP就会发送一个FIN过去,自己的状态就变迁到LAST_ACK.
服务器上出现大量的close_wait的例子和解决方法(例子从网上找的,基本差不多)
进程被kill时,会释放占用的所有链接句柄。
该问题的出现原因网上到处都是,也就是Socket的Client端出现异常没有Close就退出了。
上面的状态迁移图,基本上把TCP三次握手和四次握手的大致流程描述的非常清楚了,下面我们用文字将上面的过程描述一遍,并对异常情况进行分析:
三次握手概述:
三次握手和编程的关联:
三次握手时的异常:
第一次握手丢包:默认情况下,connect()是阻塞式的,如果请求无法发送到服务器,那么connect会进行一段很长时间的等待和重试(重传次数和时间间隔我们暂且不去深究),此时我们可以使用通过设置SO_SNDTIMEO来为connect设置超时以减少connect的等待时间
第二次握手丢包:对于客户来说,依然是connect超时,所以处理方式和第一次握手丢包是一样的。对于服务器来说,由于收不到第三次握手请求,所以会进行等待重传,直到多次重传失败后,关闭半连接。
第三次握手丢包:由于客户在发送第三次握手包后,不再等待确认,就直接进入了ESTABLISHED状态,所以一旦第三次握手失败,客户和服务器的状态就不同步了。当然,此时服务器会进行多次重发,一旦客户再次收到SYN+ACK(第二次握手请求),会再次确认。不过,如果第三次握手一直失败,则会出现,客户已经建立连接,而服务器关闭连接的情况。随后,一旦客户向服务器发送数据,则会收到一条RST回应,告诉用户连接已经重置,需要重新进行三次握手。
RST和SIGPIPE:有过网络编程经验的人都知道在写网络通信的时候,需要屏蔽SIGPIPE信号,否则的话,一旦收到PIPE信号会导致程序异常退出。其实这个SIGPIPE就是由于write()的时候,我们自己的状态是ESTABLISHED而对方的状态不是ESTABLISHED,那么对方就会给我们一个RST回应,收到这个回应之后,系统就会自动生成一个PIPE信号。
四次握手概述:
四次握手和编程的关联:
说明:
两个WAIT:
CLOSE_WAIT:CLOSE_WAIT的状态位于向对方确认FIN之后,向对方发送FIN之前,这段时间由于对方已经发送了FIN,也就表示不会再收到数据,但是这并不表示自己没有数据要发,毕竟只有在发送了FIN之后,才表示发送完毕。所以,CLOSE_WAIT这段时间主要的工作就是给对方发送必要的数据,对自己的数据进行收尾,所有工作结束之后,调用close(),发送FIN,等待LAST_ACK
TIME_WAIT:存在TIME_WAIT状态有如下两个理由:
四次握手的异常:
第一次握手丢包:FIN_WAIT1丢失会导致客户重传,如果多次重传失败,则客户超时关闭连接,而服务器依然保持ESTABLISHED状态。如果服务器主动发送数据,则会收到一个RST包,重置连接。设置KeepAlive道理相同,核心是要求服务器主动发数据。如果服务器永远不会主动发数据,那么就会一直保持这样一个“假连接”
第二次握手丢包:由于服务器第二次握手不会重发,所以即使丢包也不管,直接向对方发送FIN,此时客户执行”同时关闭“的流程(这个流程后面再说),等待TIME_WAIT时间后关闭。在客户进入TIME_WAIT之后,自己由于FIN没有相应,会重发,如果被客户TIME_WAIT收到并发送LAST-ACK,则流程正常结束,如果反复重发没有响应,那么超时关闭
第三次握手丢包:服务器会持续等待在LAST_ACK状态,而客户会持续等待在FIN_WAIT2状态,最后双方超时关闭
第四次握手丢包:客户端进入TIME_WAIT状态,等待2MSL,服务器由于收不到LAST-ACK则进行重发,如果多次重发失败,则超时关闭(这个流程和第二次握手丢包的后半段状态是一样的)
除了上面的顺序打开,和顺序关闭方式,TCP还有同时打开和同时关闭的流程:
同时打开流程:
两个应用程序同时执行主动打开的情况是可能的,虽然发生的可能性较低。每一端都发送一个SYN,并传递给对方,且每一端都使用对端所知的端口作为本地端口。
例如:
主机a中一应用程序使用7777作为本地端口,并连接到主机b 8888端口做主动打开。
主机b中一应用程序使用8888作为本地端口,并连接到主机a 7777端口做主动打开。
tcp协议在遇到这种情况时,只会打开一条连接。
这个连接的建立过程需要4次数据交换,而一个典型的连接建立只需要3次交换(即3次握手)
但多数伯克利版的tcp/ip实现并不支持同时打开
同时关闭流程:
如果应用程序同时发送FIN,则在发送后会首先进入FIN_WAIT_1状态。在收到对端的FIN后,回复一个ACK,会进入CLOSING状态。在收到对端的ACK后,进入TIME_WAIT状态。这种情况称为同时关闭。
同时关闭也需要有4次报文交换,与典型的关闭相同。
如果上面的顺序流程已经非常清楚的话,那么这两个同时打开、同时关闭的状态图就不难理解了……
大家可以通过这两张图来对应上面socket关闭流程中,“第二次握手失败”的解释,其实也就不难理解,为什么客户会进入同时关闭状态了。因为客户在发送了FIN之后,没有等到ACK,而是等到了服务器的FIN,自然符合同步关闭的流程。
如果TCP连接被对方正常关闭,也就是说,对方是正确地调用了closesocket(s)或者shutdown(s)的话,那么上面的Recv或Send调用就能马上返回,并且报错。这是由于close socket(s)或者shutdown(s)有个正常的关闭过程,会告诉对方“TCP连接已经关闭,你不需要再发送或者接受消息了”。
但是,如果意外断开,客户端(3g的移动设备)并没有正常关闭socket。双方并未按照协议上的四次挥手去断开连接。
那么这时候正在执行Recv或Send操作的一方就会因为没有任何连接中断的通知而一直等待下去,也就是会被长时间卡住。
像这种如果一方已经关闭或异常终止连接,而另一方却不知道,我们将这样的TCP连接称为半打开 的。
解决意外中断办法都是利用保活机制。而保活机制分又可以让底层实现也可自己实现。
简单的说也就是在自己的程序中加入一条线程,定时向对端发送数据包,查看是否有ACK,如果有则连接正常,没有的话则连接断开
一)双方拟定心跳(自实现)
一般由客户端发送心跳包,服务端并不回应心跳,只是定时轮询判断一下与上次的时间间隔是否超时(超时时间自己设定)。服务器并不主动发送是不想增添服务器的通信量,减少压力。
但这会出现三种情况:
情况1.
客户端由于某种网络延迟等原因很久后才发送心跳(它并没有断),这时服务器若利用自身设定的超时判断其已经断开,而后去关闭socket。若客户端有重连机制,则客户端会重新连接。若不确定这种方式是否关闭了原本正常的客户端,则在ShutDown的时候一定要选择send,表示关闭发送通道,服务器还可以接收一下,万一客户端正在发送比较重要的数据呢,是不?
情况2.
客户端很久没传心跳,确实是自身断掉了。在其重启之前,服务端已经判断出其超时,并主动close,则四次挥手成功交互。
情况3.
客户端很久没传心跳,确实是自身断掉了。在其重启之前,服务端的轮询还未判断出其超时,在未主动close的时候该客户端已经重新连接。
这时候若客户端断开的时候发送了FIN包,则服务端将会处于CLOSE_WAIT状态;
这时候若客户端断开的时候未发送FIN包,则服务端处还是显示ESTABLISHED状态;
而新连接上来的客户端(也就是刚才断掉的重新连上来了)在服务端肯定是ESTABLISHED;这时候就有个问题,若利用轮询还未检测出上条旧连接已经超时(这很正常,timer总有个间隔吧),而在这时,客户端又重复的上演情况3,那么服务端将会出现大量的假的ESTABLISHED连接和CLOSE_WAIT连接。
最终结果就是新的其他客户端无法连接上来,但是利用netstat还是能看到一条连接已经建立,并显示ESTABLISHED,但始终无法进入程序代码。个人最初感觉导致这种情况是因为假的ESTABLISHED连接和 CLOSE_WAIT连接会占用较大的系统资源,程序无法再次创建连接(因为每次我发现这个问题的时候我只连了10个左右客户端却已经有40多条无效连接)。而最近几天测试却发现有一次程序内只连接了2,3个设备,但是有8条左右的虚连接,此时已经连接不了新客户端了。这时候我就觉得我想错了,不可能这几条连接就占用了大量连接把,如果说几十条还有可能。但是能肯定的是,这个问题的产生绝对是设备在不停的重启,而服务器这边又是简单的轮询,并不能及时处理,暂时还未能解决。
其实keepalive的原理就是TCP内嵌的一个心跳包,
以服务器端为例,如果当前 server 端检测到超过一定时间(默认是 7,200,000 milliseconds ,也就是 2 个小时)没有数据传输,那么会向 client 端发送一个 keep-alive packet (该 keep-alive packet 就是 ACK和 当前 TCP 序列号减一的组合),此时 client 端应该为以下三种情况之一:
1. client 端仍然存在,网络连接状况良好。此时 client 端会返回一个 ACK 。server 端接收到 ACK 后重置计时器(复位存活定时器),在 2 小时后再发送探测。如果 2 小时内连接上有数据传输,那么在该时间基础上向后推延 2 个小时。
2. 客户端异常关闭,或是网络断开。在这两种情况下, client 端都不会响应。服务器没有收到对其发出探测的响应,并且在一定时间(系统默认为 1000 ms )后重复发送 keep-alive packet ,并且重复发送一定次数( 2000 XP 2003 系统默认为 5 次 , Vista 后的系统默认为 10 次)。
3. 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。对于应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启Keepalive功能并设置合理的Keepalive参数。
全局设置可更改 /etc/sysctl.conf ,加上:
net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_time = 60
在程序中设置如下:
在程序中表现为,当tcp检测到对端socket不再可用时(不能发出探测包,或探测包没有收到ACK的响应包),select会返回socket可读,并且在recv时返回-1,同时置上errno为ETIMEDOUT.
一、前言
最近公司在预研设备app端与服务端的交互方案,主要方案有:
虽然上面的一些成熟方案肯定更利于上生产环境,但它们通讯基础也都是socket长连接,所以本人主要是预研了一下socket长连接的交互,写了个简单demo,采用了BIO的多线程方案,集成了springboot,实现了自定义简单协议,心跳机制,socket客户端身份强制验证,socket客户端断线获知等功能,并暴露了一些接口,可通过接口简单实现客户端与服务端的socket交互。
Github源码:
二、IO通讯模型
1. IO通讯模型简介
IO通讯模型主要包括阻塞式同步IO(BIO),非阻塞式同步IO,多路复用IO以及异步IO。
该部分内容总结自专栏文章:
1.1 阻塞式同步IO
BIO就是:blocking IO。最容易理解、最容易实现的IO工作方式,应用程序向操作系统请求网络IO操作,这时应用程序会一直等待;另一方面,操作系统收到请求后,也会等待,直到网络上有数据传到监听端口;操作系统在收集数据后,会把数据发送给应用程序;最后应用程序受到数据,并解除等待状态。
BIO通讯示意图
1.2 非阻塞式同步IO
这种模式下,应用程序的线程不再一直等待操作系统的IO状态,而是在等待一段时间后,就解除阻塞。如果没有得到想要的结果,则再次进行相同的操作。这样的工作方式,暴增了应用程序的线程可以不会一直阻塞,而是可以进行一些其他工作。
非阻塞式IO示意图
1.3 多路复用IO(阻塞+非阻塞)
多路复用IO示意图
目前流程的多路复用IO实现主要包括四种:select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
1.4 异步IO
异步IO则是采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。
异步IO示意图
和同步IO一样,异步IO也是由操作系统进行支持的。微软的windows系统提供了一种异步IO技术:IOCP(I/O Completion Port,I/O完成端口);
Linux下由于没有这种异步IO技术,所以使用的是epoll(上文介绍过的一种多路复用IO技术的实现)对异步IO进行模拟。
2. Java对IO模型的支持
由于是要实现socket长连接的demo,主要关注其一些实现注意点及方案,所以本demo采用了BIO的多线程方案,该方案代码比较简单、直观,引入了多线程技术后,IO的处理吞吐量也大大提高了。下面是BIO多线程方案server端的简单实现:
三、注意点及实现方案
1. TCP粘包/拆包
1.1 问题说明
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。 1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包; 2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包; 3. 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包; 4. 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
1.2 解决思路
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下: 1. 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格; 2. 在包尾增加回车换行符进行分割,例如FTP协议; 3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度; 4. 更复杂的应用层协议。
1.3 demo方案
作为socket长连接的demo,使用了上述的解决思路2,即在包尾增加回车换行符进行数据的分割,同时整体数据使用约定的Json体进行作为消息的传输格式。
使用换行符进行数据分割,可如下进行数据的单行读取:
可如下进行数据的单行写入:
Json消息格式如下:
(1) 服务端接收消息实体类
(2) 服务端发送消息实体类
(3) 客户端发送消息实体类
2. 客户端或服务端掉线检测功能
2.1 实现思路
通过自定义心跳包来实现掉线检测功能,具体思路如下:
客户端连接上服务端后,在服务端会维护一个在线客户端列表。客户端每隔一段时间,向服务端发送一个心跳包,服务端受收到包以后,会更新客户端最近一次在线时间。一旦服务端超过规定时间没有接收到客户端发来的包,则视为掉线。
2.2 代码实现
维护一个客户端map,其中key代表用户的唯一id(用户唯一id的身份验证下面会说明),value代表用户对应的一个实体
其中Connection对象包含的信息如下:
主要关注其中的lastOnTime字段,每次服务端接收到标识是心跳数据,会更新当前的lastOnTime字段,代码如下:
额外会有一个监测进程,以一定频率来监测上述维护的map中的每一个Connection对象,如果当前时间与lastOnTime的时间间隔超过自定义的长度,则自动将其对应的socket连接关闭,代码如下:
在上面代码中,服务端收到标识是心跳数据的时候,除了更新该socket对应的lastOnTime,还会同样同样心跳类型的数据给客户端,客户端收到标识是心跳数据的时候也会更新自己的lastOnTime字段,同时也有一个心跳监测线程在监测当前的socket连接心跳是否超时
3. 客户端身份获知、强制身份验证
3.1 实现思路
通过代码socket = serverSocket.accept()获得的一个socket连接我们仅仅只能知道其客户端的ip以及端口号,并不能获知这个socket连接对应的到底是哪一个客户端,因此必须得先获得客户端的身份并且验证通过其身份才能让其正常连接。
具体的实现思路是:
自定义一个登陆处理接口,当server端受到标识是用户登陆的时候(此时会携带用户信息或者token,此处简化为用户id),调用用户的登陆验证,验证通过的话则将该socket连接与用户信息绑定,设置其为已登录,并且封装对应的对象放入前面提的客户端map中,由此可获得具体用户对应的哪一个socket连接。
为了实现socket连接的强制验证,在监测线程中,也会判断当前用户多长时间内没有实现登录态,若超时则认为该socket连接为非法连接,主动关闭该socket连接。
3.2 代码实现
自定义登陆处理接口,这边简单以userId来判断是否允许登陆:
收到客户端发来的数据时候的处理:
监测线程判断用户是否完成身份验证:
4. socket异常处理与垃圾线程回收
4.1 实现思路
socket在读取数据或者发送数据的时候会出现各种异常,比如客户端的socket已断开连接(正常断开或物理连接断开等),但是服务端还在发送数据或者还在接受数据的过程中,此时socket会抛出相关异常,对于该异常的处理需要将自身的socket连接关闭,避免资源的浪费,同时由于是多线程方案,还需将该socket对应的线程正常清理。
4.2 代码实现
下面以server端发送数据为例,该代码中加入了重试机制:
上述调用的this.connectionThread.stopRunning()代码如下:
上述代码中设置了线程对象的状态,下述代码在监测线程中执行,将没有运行的线程给清理掉
四、项目结构
由于使用了springboot框架来实现该demo,所以项目结构如下:
整体项目结构图
socket工具包目录如下:
socket工具包目录
pom文件主要添加了springboot的相关依赖,以及json工具和lombok工具等,依赖如下:
自己写的socket工具包的使用方式如下:
该demo中主要提供了以下几个接口进行测试:
具体的postman文件也放已在项目中,具体可点此链接获得
demo中还提供了一个简单压测函数,如下:
源码地址如下,仅供学习参考
github.com/DavidDingXu/springboot-socket-demo
五、参考
一:socket通信基本原理。
首先socket 通信是基于TCP/IP 网络层上的一种传送方式,我们通常把TCP和UDP称为传输层。
如上图,在七个层级关系中,我们将的socket属于传输层,其中UDP是一种面向无连接的传输层协议。UDP不关心对端是否真正收到了传送过去的数据。如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。
socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,对用户来说,只要通过一组简单的API就可以实现网络的连接。借用网络上一组socket通信图给大家进行详细讲解:
首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。
二:socket通信基本示例:
在对socket通信基本原理明白后,那我们就写一个最简单的示例,展示童鞋们常遇到的第一个问题:客户端发送消息后,服务端无法收到消息。
服务端:
客户端:
启动服务端:发现正常,等待客户端的的连接
启动客户端:发现客户端启动正常后,马上执行完后关闭。同时服务端控制台报错:
然后好多童鞋,就拷贝这个java.net.SocketException: Connection reset上王查异常,查询解决方案,搞了半天都不知道怎么回事。解决这个问题我们首先要明白,socket通信是阻塞的,他会在以下几个地方进行阻塞。第一个是accept方法,调用这个方法后,服务端一直阻塞在哪里,直到有客户端连接进来。第二个是read方法,调用read方法也会进行阻塞。通过上面的示例我们可以发现,该问题发生在read方法中。有朋友说是Client没有发送成功,其实不是的,我们可以通debug跟踪一下,发现客户端发送了,并且没有问题。而是发生在服务端中,当服务端调用read方法后,他一直阻塞在哪里,因为客户端没有给他一个标识,告诉是否消息发送完成,所以服务端还在一直等待接受客户端的数据,结果客户端此时已经关闭了,就是在服务端报错:java.net.SocketException: Connection reset
那么理解上面的原理后,我们就能明白,客户端发送完消息后,需要给服务端一个标识,告诉服务端,我已经发送完成了,服务端就可以将接受的消息打印出来。
通常大家会用以下方法进行进行结束:
socket.close() 或者调用socket.shutdownOutput();方法。调用这俩个方法,都会结束客户端socket。但是有本质的区别。socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。而socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。现在我们将上面的客户端示例修改一下啊,增加一个标识告诉流已经输出完毕:
客户端2:
console服务端正常输出了
服务端在接受到客户端关闭流的信息后,知道信息输入已经完毕,苏哦有就能正常读取到客户端传过来的数据。通过上面示例,我们可以基本了解socket通信原理,掌握了一些socket通信的基本api和方法,实际应用中,都是通过此处进行实现变通的。
三:while循环连续接受客户端信息:
上面的示例中scoket客户端和服务端固然可以通信,但是客户端每次发送信息后socket就需要关闭,下次如果需要发送信息,需要socket从新启动,这显然是无法适应生产环境的需要。比如在我们是实际应用中QQ,如果每次发送一条信息,就需要重新登陆QQ,我估计这程序不是给人设计的,那么如何让服务可以连续给服务端发送消息?下面我们通过while循环进行简单展示:
服务端:
客户端:
客户端console:
服务端console:
大家可以看到,通过一个while 循环,就可以实现客户端不间断的通过标准输入流读取来的消息,发送给服务端。而服务端在结束数据的时候,也通过这个标识进行判断,如果接受到这个标识,表明数据已经传入完成,那么服务端就可以将数据度入后显示出来。
在上面的示例中,客户端端在循环发送数据时候,每发送一行,添加一个换行标识“\\n”标识,在告诉服务端我数据已经发送完成了。而服务端在读取时,通过while ((str = bufferedReader.readLine())!=null)去判断是否读到了流的结尾,负责服务端将会一直阻塞在哪里,等待客户端的输入。
通过while方式,我们可以实现多个客户端和服务端进行聊天。但是,下面敲黑板,划重点。由于socket通信是阻塞式的,假设我现在有A和B俩个客户端同时连接到服务端的上,当客户端A发送信息给服务端后,那么服务端将一直阻塞在A的客户端上,不同的通过while循环从A客户端读取信息,此时如果B给服务端发送信息时,将进入阻塞队列,直到A客户端发送完毕,并且退出后,B才可以和服务端进行通信。简单地说,我们现在实现的功能,虽然可以让客户端不间断的和服务端进行通信,与其说是一对一的功能,因为只有当客户端A关闭后,客户端B才可以真正和服务端进行通信,这显然不是我们想要的。 下面我们通过多线程的方式给大家实现正常人类的思维。
四:多线程下socket编程
服务端:
客户端:
启动服务端
启动两个客户端A 、 B
客户端A console:
客户端B console:
服务端 console:
通过这里我们可以发现,客户端A和客户端B同时连接到服务端后,都可以和服务端进行通信,也不会出现前面讲到使用while(true)时候客户端A连接时客户端B不能与服务端进行交互的情况。在这里我们看到,主要是通过服务端的 new Thread(new Runnable() {}实现的,每一个客户端连接进来后,服务端都会单独起个一线程,与客户端进行数据交互,这样就保证了每个客户端处理的数据是单独的,不会出现相互阻塞的情况,这样就基本是实现了QQ程序的基本聊天原理。
但是实际生产环境中,这种写法对于客户端连接少的的情况下是没有问题,但是如果有大批量的客户端连接进行,那我们服务端估计就要歇菜了。假如有上万个socket连接进来,服务端就是新建这么多进程,反正楼主是不敢想,而且socket 的回收机制又不是很及时,这么多线程被new 出来,就发送一句话,然后就没有然后了,导致服务端被大量的无用线程暂用,对性能是非常大的消耗,在实际生产过程中,我们可以通过线程池技术,保证线程的复用,下面请看改良后的服务端程序。
改良后的服务端:
通过线程池技术,我们可以实现线程的复用。其实在这里executorService.submit在并发时,如果要求当前执行完毕的线程有返回结果时,这里面有一个大坑,在这里我就不一一详细说明.
在实际应用中,socket发送的数据并不是按照一行一行发送的,比如我们常见的报文,那么我们就不能要求每发送一次数据,都在增加一个“\\n”标识,这是及其不专业的,在实际应用中,通过是采用数据长度+类型+数据的方式,在我们常接触的热Redis就是采用这种方式,
五:socket 指定长度发送数据
在实际应用中,网络的数据在TCP/IP协议下的socket都是采用数据流的方式进行发送,那么在发送过程中就要求我们将数据流转出字节进行发送,读取的过程中也是采用字节缓存的方式结束。那么问题就来了,在socket通信时候,我们大多数发送的数据都是不定长的,所有接受方也不知道此次数据发送有多长,因此无法精确地创建一个缓冲区(字节数组)用来接收,在不定长通讯中,通常使用的方式时每次默认读取8*1024长度的字节,若输入流中仍有数据,则再次读取,一直到输入流没有数据为止。但是如果发送数据过大时,发送方会对数据进行分包发送,这种情况下或导致接收方判断错误,误以为数据传输完成,因而接收不全。在这种情况下就会引出一些问题,诸如半包,粘包,分包等问题,为了后续一些例子中好理解,我在这里直接将半包,粘包,分包概念性东西在写一下(引用度娘)
5.1 半包
接受方没有接受到一个完整的包,只接受了部分。
原因:TCP为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。
影响:长连接和短连接中都会出现
5.2 粘包
发送方发送的多个包数据到接收方接收时粘成一个包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
分类:一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包
出现粘包现象的原因是多方面的:
1)发送方粘包:由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
2)接收方粘包:接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
5.3分包
分包(1):在出现粘包的时候,我们的接收方要进行分包处理;
分包(2):一个数据包被分成了多次接收;
原因:1. IP分片传输导致的;2.传输过程中丢失部分包导致出现的半包;3.一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系)。
影响:粘包和分包在长连接中都会出现
那么如何解决半包和粘包的问题,就涉及一个一个数据发送如何标识结束的问题,通常有以下几种情况
固定长度:每次发送固定长度的数据;
特殊标示:以回车,换行作为特殊标示;获取到指定的标识时,说明包获取完整。
字节长度:包头+包长+包体的协议形式,当服务器端获取到指定的包长时才说明获取完整;
所以大部分情况下,双方使用socket通讯时都会约定一个定长头放在传输数据的最前端,用以标识数据体的长度,通常定长头有整型int,短整型short,字符串Strinng三种形式。
下面我们通过几个简单的小示例,演示发送接受定长数据,前面我们讲过通过特殊标识的方式,可是有什么我们发送的数据比较大,并且数据本身就会包含我们约定的特殊标识,那么我们在接受数据时,就会出现半包的情况,通过这种情况下,我们都是才有包头+包长+包体的协议模式,每次发送数据的时候,我们都会固定前4个字节为数据长度,那到数据长度后,我们就可以非常精确的创建一个数据缓存区用来接收数据。
那么下面就先通过包类型+包长度+消息内容定义一个socket通信对象,数据类型为byte类型,包长度为int类型,消息内容为byte类型。
首先我们创建服务端socket
在服务端创建后,我们通过DataInputStream 数据流进行数据获取,首先我们获取数据的类型,然后在获取数据的长度,因为数据实际有效长度是整个数据的长度减去5,(包括前个字节为数据类型,前二到五个字节为数据长度)。然后根据数据的实际有效长度创建数据缓存区,用户存放数据,这边确保每次接接受数据的完整性,不会出现半包与粘包的情况。在数据读取的时候,我们通过readFully()方法读取数据。
下面我们来创建socket的客户端:
客户端socket创建后,我们通过dataOutputStream输出流中的writeByte()方法,设置数据类型,writeInt()方法设置数据长度,然后通过write()方法将数据发送到服务端进行通信,发送完毕后,为了确保数据完全发送,通过调用flush()方法刷新缓冲区。
下面我们通过控制可以看到服务端接受数据的情况:
console客户端发送数据:
服务端console接受数据:
上面服务端分别接受到数据的类型,长度和详细内容,具体下面的错误异常是由于客户端发送一次后关闭,服务端任在接受数据,就会出现连接重置的错误,这是一个简单的通过数据类型+数据长度+数据内容的方法发送数据的一个小例子,让大家了解socket通信数据发送的原理,在实际应用中,原理不出其左右,只是在业务逻辑上完善而已。
六:socket 建立长连接
在了解socket长连接和短连接之前,我们先通过一个概念性的东西,理解一下什么叫长连接,什么叫短连接,长连接的原理和短连接的原理,
6.1 长连接
指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。整个通讯过程,客户端和服务端只用一个Socket对象,长期保持Socket的连接。
6.2 短连接
短连接服务是每次请求都建立链接,交互完之后关闭链接,
6.3 长连接与短连接的优势
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是短连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。(度娘)
在这章之前,你看到所有的例子,都是短连接,每次连接完毕后,都是自动断开,如果需要重新连接,则需要建立新的连接对象,比如像前一章我们看到的例子中,服务端有connection reset错误,就是短连接的一种。接下来,我们主要讲解一下长连接原理,在实际应用中,长连接他并不是真正意义上的长连接,(他不像我们打电话一样,电话通了之后一直不挂的这种连接)。他们是通过一种称之为心跳包或者叫做链路检测包,去定时检查socket 是否关闭,输入/输出流是否关闭。
在这里有个问题,也是好多初学者比较困惑的,也是好多初学socket时候,遇到的一个问题,那就是socket是通过流的方式通信的,既然关闭流,就是关闭socket,那么长连接不是很简单吗?就是我们读取流中的信息后,不关闭流,等下次使用时,直接往流中扔数据不就行了?
针对这个问题,我做个详细的解答,尽可能的描述清楚,首先我们socket是针对应用层与TCP/ip数据传输协议封装的一套方案,那么他的底层也是通过Tcp/Tcp/ip或则UDP通信的,所以说socket本身并不是一直通信协议,而是一套接口的封装。而tcp/IP协议组里面的应用层包括FTP、HTTP、TELNET、SMTP、DNS等协议,我们知道,http1.0是短连接,http1.1是长连接,我们在打开http通信协议里面在Response headers中可以看到这么一句Connection:keep-alive。他是干什么的,他就是表示长连接,但是他并不是一直保持的连接,他有一个时间段,如果我们想一直保持这个连接怎么办?那就是在制定的时间内让客户端和服务端进行一个请求,请求可以是服务端发起,也可以是客户端发起,通常我们是在客户端不定时的发送一个字节数据给服务端,这个就是我们称之为心跳包,想想心跳是怎么跳动的,是不是为了检测人活着,心会定时的跳动,就是这个原理。
嘿,亲爱的小伙伴们,今天我们要来聊一聊生活百科行业的热门话题——圣元优博敏佳奶粉!是不是听起来就很高大上?没错,它可是备受各位爸爸妈妈们青睐的宝宝奶粉哦!那么,究竟什么是圣元
嗨,亲爱的准妈妈们!今天我们来聊聊备受关注的孕妇奶粉——圣元优博孕妇奶粉。你可能已经听说过它,也可能对它的效果和口碑有所疑问。别担心,下面我会给你详细介绍它的成分、专家
喜欢宝宝的爸爸妈妈们,大家好!今天小编要为大家介绍的是备受关注的生活百科行业话题——“圣元优博婴幼儿奶粉的成分”。作为宝宝成长中必不可少的营养来源,奶粉的选择显得尤为
嘿,亲爱的宝爸宝妈们,今天小编要和大家聊一聊最近备受瞩目的话题——圣元优博奶粉!这款奶粉不仅在配方上与众不同,更是备受育儿界推崇的宝贝养育神器。接下来,让我们一起来探究一
想要给宝宝选择一款优质的奶粉,可不是件容易的事情哦!尤其是在如今市场上琳琅满目的各类奶粉品牌中,要做出最佳选择更是难上加难。而作为生活百科行业的一员,今天我们就来聊聊备
嘿,亲爱的读者们!今天我们要来聊一聊最近备受关注的生活百科行业话题——圣元优博奶粉的配方表。是不是有些小激动呢?毕竟,每个做爸妈的都希望给宝宝最好的营养,而奶粉配方表就是
用户评论
这篇文章讲得真明白!我之前经常在网络编程中遇到这个问题,每次 `socket_accept()` 都卡在那儿,不知道为什么,现在终于理解了是阻塞造成的。
有15位网友表示赞同!
对于新手来说,这个概念确实不太好理解,因为平时很少接触到进程的管理方式。希望博主以后可以多分享一些更具体的例子和解决方案!<i>
有18位网友表示赞同!
感觉这篇文章没说清重点,只讲了阻塞,但这真的解决不了问题吧?没有提到怎么进行非阻塞操作或者使用线程池来解决这个问题?
有11位网友表示赞同!
看了这篇文章才知道原来 `socket_accept()` 是阻塞式的!我以前就觉得这个函数有点怪怪的,每次执行都要等好久。以后明白了机制可以考虑其他方案了!
有12位网友表示赞同!
同意评论里说的,太理论化了! 想看一些实际应用案例比较好理解啊!比如在聊天服务器中如何使用非阻塞的方式处理客户端连接
有17位网友表示赞同!
我也是个小白,看了这篇文章终于稍微明白一点了。明白了是阻塞的意思就等下找点资料学习一下非阻塞是怎么实现的。
有9位网友表示赞同!
其实这个问题很简单啊,用线程池或者异步方式就可以避免阻塞了! 博主是不是有点故意写得复杂?
有12位网友表示赞同!
这篇文章比较详细地介绍了 `socket_accept()` 的工作原理,很适合用来入门学习。不过对于已经有一定了解网络编程基础的人来说,可能显得有些枯燥了。
有8位网友表示赞同!
阻塞的问题确实是一个难题,尤其是在高并发场景下。我之前一直采用单线程处理,导致服务器性能低下,现在打算尝试使用多线程或者事件驱动模式来提高效率。 这个文章的内容让我对这个话题有了更深的认识。
有13位网友表示赞同!
网上关于非阻塞套接字的资料太少了,希望博主以后能分享一些自己实践经验,包括使用 Threadpool 或异步框架的代码实现等细节!
有7位网友表示赞同!
我感觉这篇博客有点过于强调阻塞导致的问题,没有平衡地讨论一下阻塞方式的一些优点,比如它对于简单的程序来说可能更易于理解调试。
有15位网友表示赞同!
我一直以为套接字都是阻塞式的,原来还有非阻塞的解决方案! 这篇文章让我开阔了视野,我会好好学习一下 nonblocking 编程技巧!
有20位网友表示赞同!
写这篇文章的时候是不是没有考虑实际应用场景?在一些简单的项目中,使用阻塞可能更方便快捷啊。
有11位网友表示赞同!
我主要关注的是跨平台的套接字解决方案,这篇博客似乎只讲了单一平台的实现方法,希望博主未来可以涉及到更多的知识点!
有12位网友表示赞同!
有什么能解决 `socket_accept()` 的阻塞问题,例如是使用其他框架或者库吗? 这篇文章内容虽然有启发,但缺少实践指南。
有19位网友表示赞同!
终于找到一篇比较好的解释`socket_accept()`的问题了! 之前一直搞不清楚怎么处理并发连接的问题。要深入学习非阻塞编程才能解决这个问题!
有7位网友表示赞同!
`socket_accept()` 的阻塞确实会影响程序效率,但并不是所有情况都需要非阻塞的操作啊,比如一些简单的应用可以使用阻塞方式,省得费心额外配置繁琐的线程或异步框架
有13位网友表示赞同!