系统IO原理以及TCP/IP协议

netstat -natp  查看内核里socket建立过程
tcpdump 抓取网络通信数据包
命令lsof  -op  $$  查看某个进程的文件描述符  

任何程序都有  这三个文件描述符  0:标准输入    1:标准输出    2:报错输出
/proc/proc/$$  当前bash的pid  $BASHPID/proc/$$/fd     命令lsof  -op  $$  查看某个进程的文件描述符   重定向操作符:不是命令,是机制 输入,输出  I/O<   >     管道   |
read a 0<io.txt    使用io进行文件描述符标准输入的替换 重定向操作符左边放的是某个文件描述符,右边放的是文件名,如果要在右边放文件描述符,需要在重定向操作符后加&,重定向操作符绑定是有序的,
ls ./  /sadasfasf 1>ls01.out 2> ls02.out   标准输出到ls01.out 错误输出到ls02.out
ls ./  /sadasfasf 1>ls02.out 2> ls02.out    两个文件描述符对文件是覆盖的,不会追加
ls ./  /sadasfasf 1> ls04.out  2>& 1      1先指向文件,再把2指向1指向的位置 如果要在右边放文件描述符,需要在重定向操作符后加&,重定向操作符绑定是有顺序的, 

head -8 test.txt |tail -1   读取第八行数据

 {a=9; echo "sss"; } |cat
bash: 未预期的符号 `}' 附近有语法错误
[root@localhost test]# echo $a
1

[root@localhost test]# echo $$ | cat     $$优先级高于管道
1914
[root@localhost test]# echo $BASHPID |cat  $BASHPID优先级低于管道执行
1931 

pagecache 4k小格子
buffer缓冲流相比于普通流,效率更高,因为在jvm里默认有8kb的字节数组,8kb满了,才会进行系统调用将它写到内存pagecache,减少了系统调用次数(用户态和内核态切换次数)
内存不够时使用淘汰算法 lru算法   淘汰前先判pagecache是否是脏页(脏页是刚在内存是创建,或者被修改过),脏页要先写入磁盘,进行磁盘同步,才能进行淘汰
传输控制层协议      tcp是面向连接的传输协议,可靠 要先有服务端,客户端才能建立连接     udp不可靠   服务端客户端启动不分先后
TCP 要建立连接  经三次握手,客户端和服务端在自己的内存里开辟内存空间建立资源,线程等    客户端发一个包,服务端回复确认,客户端再发一个包给服务端,之后建立连接  客户端在第三次握手时,可以带着数据传过去,开始通信
 四次分手 断开连接   客户端发断开请求,服务端确一条消息认收到,服务端再发一条消息,客户端回复确认收到 之前过一段时间断开连接 如果超时,也会最终断开连接,销毁资源
 
  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报文多数情况下都是分开发送的。
四次分手时,关闭连接的发起方(client或server端),会出现 TIME_WAIT 的状态,有可能最后一次发送的ACK包由于网络因,没有被接收到,需要接收ACK的一方会进入重试,再次发FIN包给发起方,
所以,发起方会有一个CLOSE_WAIT等待关闭的过程,有可能最后的ACK没有到达,所以自己多留一会儿资源
在TIME_WAIT没有结束前,内核中的socket的四元组被占用,相同的对端不能使用这个资源建立新的连接浪费的是名额!站着茅坑不拉屎这个不是DDOS

关于端口被占用的原因 作为服务端listen状态的serverSocket只能启动65535个,因为如果有数据包进来,如果有两个Socket占用同一个端口进行监听,这个数据包不知道该交给哪个进程
sererSocket.accept 获得的socket对象相当于获得FD文件描述符,socket类型的文件描述符 如果想要动数据的话,通过FD找到内核的缓冲区进行数据的读写
 窗口机制,解决拥塞问题 
 客户端tcp连接过来,向服务端接收队列里发数据,如果一直发,但是应用程序没有从队列里及时取出来,队列一旦满了,就会将后面放不下的数据丢弃,可能会丢失数据
IPV4_FAILURE_FATAL="no"
IPV6INIT="yes"
IPV6_AUTOCONF="yes"
IPV6_DEFROUTE="yes"
IPV6_FAILURE_FATAL="no"
IPV6_ADDR_GEN_MODE="stable-privacy"
NAME="ens33"
UUID="6367ba0b-30ec-449c-9fc0-b1cff8ee7214"
DEVICE="ens33"
ONBOOT="yes"
IPADDR=192.168.33.104
GATEWAY=192.168.32.2
NETMASK=255.255.255.0
DNS2=8.8.8.8
IPADDR 192.168.32.2 和 NETMASK 255.255.255.0 掩码做按位与运算  得到 192.168.32是机器所在的网络地址,192.168.32.2 的最后一位2是主机在这个网络里的位置  
DNS 是解析域名和ip地址的映射
arp 解释ip地址和网卡硬件地址的映射

HTTP是在应用层,tcp是传输控制层, 不同层,http建立在tcp之上

BIO之所以慢,是因为accept是系统调用,循环阻塞主线程,每来一个客户端,接受到客户端socket,创建线程处理完才能循环回到主线程,再接收下一个连接。且线程太多以后,读取数据时,调度时,切换线程太耗时
弊端:阻塞,accept,read
一个程序监听的socket只有一个,连接的socket可以有多个
NIO比BIO更节省资源,更快 单线程,接收和读取都非阻塞 ,也可以使用一个或几个线程,每个线程负责处理一部分连接的遍历
NIO优势,通过一个或几个线程来处理多个连接,弊端: 无效的系统调用太多,是O(n)复杂度,如果有一万个连接,每循环一次,都会有一万次的recv系统调用,去读取是否有数据发送过来了,
可能只有几个连接发数据了,大部分系统调用都是无意义的,浪费资源 需要全量遍历用户态和内核态的切换才能实现
多路复用器通过一次系统调用,返回所有IO的状态,由程序自己对有状态的IO进行读写 只要程序自己进行读写,那么IO模型就是同步的  
同步IO模型:app需要自己进行R/W  异步IO:kernel完成R/W,写到buffer,程序从缓冲区里读     同异步只关注IO,不关注从IO读写完后面的操作
同步阻塞:程序自己读取,调用系统,等待结果返回
同步非阻塞:程序自己读,调用方法,立刻返回结果(程序自己决定后面什么时候再读)
异步:目前linux还没有通用内核的异步处理方案 异步非阻塞   异步阻塞没意义
多路复用器:通过一次系统调用,获得多个IO的状态,给程序返回有状态的IO,但是还需要程序自己去进行系统调用读取内容  select    poll  epoll 在同步非阻塞IO模型下使用  
select 复杂度O(1) ,调用一次就知道哪些IO可以读写,对具体m个IO进行recv系统调用 O(m),而不是全量的,系统调用次数少很多,文件描述符个数有1024的限制 windows linux unix都有select
select 就是把文件描述符传到内核,由内核根据描述符去遍历所有的IO询问状态(一次系统调用),而不是像NIO,由程序一次次调用内核,遍历的成本在于经历多次用户态和内核态的切换

其实:无论NIO,SELECT,POLL都是要遍历所有与程序有关的IO,询问状态,只不过:NIO:      这个遍历的过程成本在用户态内核态切换(多路复用器前期)select,poll: 
 这个遍历的过程触发了一次系统调用,用户态内核态的切换,过程中,把fds传递给内核,内核重新根据用户这次调用传过来的fds,遍历,修改状态

多路复用器:select poll的弊端,问题1.每次都要重新,重复传递fds  (内核开辟空间) 2.每次,内核被调了之后,针对这次调用,触发一个遍历fds全量的复杂度
epoll 
原理 :首先程序server端得到一个listen监听状态的的文件描述符fd4,再系统调用epoll_create,在内核中开辟空间 存放红黑树,返回一个文件描述符给程序,比如fd6,代表开启的空间,再调用
epoll_ctl,将监听状态的文件描述符添加到红黑树里中,关注的是fd4的accept事件,当有客户端发数据从网卡到内核对应fd4的缓冲区时, 拿着fd4文件描述符,去红黑树里找fd4文件描述符,
并把它拷贝到链表里,这个拷贝的过程是基于中断发生
epoll_create  返回一个文件描述符 只会调用一次 开辟空间 存放红黑树

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 
epoll_wait  从链表里返回有状态(有数据到达)的文件描述符,是从红黑树里拷贝来的
内核中,程序在红黑树中放过一些FD,那么伴随内核基于中断处理完fd的buffer,状态呀,之后,继续把有状态的fd,copy到链表中 所以程序只要调用wait,能及时取走有状态fd的结果集
select ,poll 它们是在jvm内部开启了一个数组,来存放监听和连接的文件描述符,当进行系统调用Selector.select方法时,会一次传递所有的文件描述符,仅系统调用一次,
把有连接建立或IO事件到达的文件描述符返回给程序。而EPOLL是在此基础上做了优化,Selector.open是在内核空间开启了一个红黑树结构的空间,把注册监听客户端连接和IO事件的文件描述符存入,
当有事件到达后,从红黑树里找到对应的文件描述符,把它拷贝到一个链表里去,当在java里调用Selector.select方法时,其实只是取出链表里的文件描述符集合,效率比select ,poll更高

    //1.select ()
                /* //select ()不传参数,是阻塞的 如果在对一些文件描述符调起select()时,
                如果其它线程在这个selector里加入了一些文件描述符,但这里是阻塞的,selectKeys()方法还是原来的那些keys
                可能永远阻塞下去,不能往下走
                 */
      int nums = selector.select(); //返回有事件的fd个数  将有事件的fd集合放到jVm的直接空间  是内核到jvm直接空间 fd集合进行增量的过程
              //  Thread.sleep(1000);
              //  System.out.println(Thread.currentThread().getName() +":after selec():"+selector.keys().size());

                //2.selectedKeys();   //返回注册在selector中等待IO操作(及有事件发生)channel的selectionKey。
                if (nums > 0) { //如果selecto里来事件了,进行处理
                    Set<SelectionKey> keys = selector.selectedKeys();  //把jVm的直接空间里fds拷贝到堆空间,在jvm堆空间给你一个事件集
                    Iterator<SelectionKey> iterator = keys.iterator();
                    while (iterator.hasNext()) {  //线程内部拿到keys是线性处理
                        SelectionKey key = iterator.next();
                        //为啥要remove,是下一次循环 selector.selectedKeys()时,还会再次把这些有状态的keys取出来,处理就重复了,猜测这些keys应该是在链表里保存
                        //2.selector不会自己删除selectedKeys()集合中的selectionKey,那么如果不人工remove(),将导致下次select()的时候selectedKeys()中仍有上次轮询留下来的信息,这样必然会出现错误。
                        iterator.remove();  //将处理完事件的fd从jVm的直接空间移除,不移除那下一次调selector.selectedKeys(),再从jvm直接空间拷贝时,还会重复处理
                        if (key.isAcceptable()) {  //接收客户端
                            acceptHander(key);
                        }
                        if (key.isReadable()) {
                            readHandler(key);
                        }
                        if (key.isWritable()) {

                        }
                    }

                }

考虑资源利用,充分利用cpu核数考虑有一个fd执行耗时,在一个线性里会阻塞后续FD的处理当有N个fd有R/W处理的时候:将N个FD 分组,每一组一个selector,
将一个selector压到一个线程上最好的线程数量是:cpu  cpu*2其实但看一个线程:里面有一个selector,有一部分FD,且他们是线性的多个线程,他们在自己的cpu上执行,代表会有多个selector在并行,
且线程内是线性的,最终是并行的fd被处理但是,你得明白,还是一个selector中的fd要放到不同的线程并行,从而造成canel调用嘛?  不需要了!!!上边的逻辑其实就是分治,
我的程序如果有100W个连接,如果有4个线程(selector),每个线程处理 250000那么,可不可以拿出一个线程的selector就只关注accpet ,然后把接受的客户端的FD,分配给其他线程的selector


程序应该将IO线程和业务执行线程分开,尽可能快的去处理内核接收队列里的数据,在IO密集的情况下,由网卡过来的数据,在队列满了的时候,可能会被丢弃
socketChannel.configureBlocking(false);
必须设置通道为 非阻塞,才能向 Selector 注册。 


selector.selectedKeys()与selecot.keys区别
selector.keys 返回当前所有注册在selector中channel的selectionKey
selector.selectedKeys() 返回注册在selector中等待IO操作(及有事件发生)channel的selectionKey。
 2.selector不会自己删除selectedKeys()集合中的selectionKey,那么如果不人工remove(),将导致下次select()的时候selectedKeys()中仍有上次轮询留下来的信息,这样必然会出现错误。
 

 
 
 
Netty框架
netty的 byteBuf getbytes(new byte[100])不会移动指针 而 readBytes(new byte[100])会移动指针,到读的内容之后,下次读就不能再次读到
EventLoopGroup bossGroup = new NioEventLoopGroup();当我们没有传入参数时,默认调用的是参数为0的构造方法,这个参数是可执行任务的线程数
看下带有参数的构造方法。,如果传入的nThreads为0,就是默认值。这个默认是多少呢,看源码,它是NettyRuntime.availableProcessors()的两倍  NettyRuntime.availableProcessors()原来是CPU的核数
    bind(port)与.localAddress(new InetSocketAddress(port))区别  
两者并没有什么区别,最后都会调用AbstractBootstrap这个抽象类的bind()方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值