转载,请加上原文链接;
目录
socket
对象的创建时间
这里需要一点TCP的知识, TCP状态分析请看 ——> TCP转态转换讲解(可点击)
当客户端想要与服务器通信的时候,客户端就会 准备 创建一个 socket
对象, 注意,是准备创建,并不是立刻就创建出来;因为在正确创建 socket 对象之前,需要先进行三次握手,如果三次握手失败,那么客户端创建 socket
对象就会失败,抛出一个 IOException
异常 ;只有当三次握手成功,客户端处于 ESTABLISHED
状态,才会正确的创建出 socket
对象 ;
在服务器端,服务器会事先就创建一个 ServerSocket
对象, 这个对象一般随着服务器的启动,就会得到创建,这里没有三次握手; ServerSocket
对象里面指定了 监听端口
和 监听地址
; ServerSocket
对象里面有2个数据列表,一个是 未完成连接数据列表
、一个是 已完成连接数据列表
;
其中 监听端口
,就是监听指定端口的请求,比如监听服务器的8080
端口,那么只有访问8080
端口的请求,我们的这个 ServerSocket
对象才会处理 ,这里面的端口号 需要我们自己写;
而 监听地址
,则是因位 一个服务器,可以设置多个 IP
地址 ,我们要是想只对访问其中某一个IP
地址的请求进行处理,那么就在 监听地址
里面,写上我们要监听的那个 IP
地址 ;一般情况下,我们写上 通配符 *
,即监听所有IP
地址 ;
当客户端向服务器发起请求连接的时候(注意:这个请求连接只是客户端发送一个 SYN
,是 第一次握手
),服务器端的 ServerSocket
对象 ,会根据请求,创建一个 套接字数据结构
,注意,这时候,服务端的socket对象,并没有完全
创建 (没有完全创建,类似于构造器得到执行,但是还没有返回实例);并且把这个 套接字数据结构
放到 未完成连接数据列表
里面 ; 只有当与客户端完成三次握手以后,服务器也进入 ESTABLISHED
状态,才会返回 socket
对象 ,并将 socket 对象对应的 套接字数据结构
从 未完成连接数据列表
里面转移到 已完成连接数据列表
里面 ;
(其中,在
已完成连接数据列表
中的每一个套接字数据结构
都代表着一个TCP
连接)
结论
:当客户端向服务器发起请求的时候(发送 SYN
),客户端是未完全创建出 socket
实例的;在服务器端,当服务器接收到客户端的请求的时候(收到 SYN
),也是未完全创建出 socket
实例的 ;
只有当两人完成 三次握手
以后,才会完全的创建出 socket
实例 ;
socket
通信可能会造成死锁
分析这个问题之前,我们需要知道 socket
对象里面有三个缓冲区;
SendQ
:缓存已经写入到输入流里面,但是接收到还未成功接收的字节;RecvQ
:缓存接收到的字节,但是还没有被分配到具体的应用程序 ;Delivered
:缓存着从RecvQ
中已经被具体程序读取了的字符 ;
这里我们需要知道,当发送的的 SendQ
满了以后,发送端就进入阻塞状态,不可以再发信息,这时候 TCP协议
负责将发送端 SendQ
中的内容转移到 接收端的 RecvQ
中,这样接收端的 SendQ
就又有了空间,可以继续对外发送信息 ;
上面是 TCP控制机制
,这样做的目的是,为了协调双方的压力,让双方的压力不要太大;否则,发送端一个劲的写数据,写了几个G的数据,接收端一次性接收几个G的数据,双方的压力都不小 ;
正是由于 TCP控制机制
的存在,才会导致了死锁的发生;(成也萧何败萧何)
我们想一下,当通信双方(A方、B方),都向对方写数据,并且数据量还不小(比 SendQ + RecvQ 两个缓存区大就可以造成死锁了,这两个缓冲区大小也就是几十 KB
,具体的忘记了,反正不大)
当 A 把自己的 SendQ
写满了,TCP
发现以后,就把里面的数据转移到 B 的 RecvQ
里面,正常情况下,B 发现自己的 RecvQ
里面有数据,就会去读数据;
但是现在,B也在写数据,我们知道一个线程,同时只能做一件事的; B 把自己的 SendQ
写满了,TCP
发现以后,就把里面的数据转移到 A 的 RecvQ
里面,正常情况下,A 发现自己的 RecvQ
里面有数据,就会去读数据,但是现在 A 也在写数据哦,也就是双方都在写数据,没有人去读数据;
好了,现在只要他们把各自的 SendQ
和 对方的 RecvQ
写满了,就会陷入 阻塞
转态,等待对方读数据,以便继续写数据;但是现在的情况的是,双方都阻塞了;完犊子了,就一直阻塞下去了;
我们回头分析下,这个问题产生的根本原因是啥?两边同时写大量的数据?其实这不是本质原因,本质原因是我们之前学的IO
都是 BIO
即 阻塞IO
,我们必须要等待 读写
操作,完成以后,才可以继续下面的操作;
一旦 读写
的动作未完成,就会进入阻塞状态,等待它完成,才可以执行下面的代码;
因此,这个问题解决的根源在于:使用 NIO(即非阻塞IO );