网络编程(一)

TCP / IP

TCP/IP协议,三次握手(建立连接),四次挥手(断开连接),都是从Client(客户端)角度来看待三次握手与四次挥手的,都是客户端发起第一次握手或者第一次挥手。

三次握手(建立连接)

建立Client与Server的关系,需要证明Client和Server的接和收都没有问题,也就是需要证明四个点。
一次握手(SYN),客户端发送一个带有 SYN(同步)标志的数据包给服务器,请求建立连接。此时客户端进入 SYN_SENT 状态。客户端发送数据,服务端接收数据,证明服务端接收数据正常(证明了一个点)。
二次握手(SYN + ACK),服务器收到客户端的请求后,会发送一个带有 SYN 和 ACK(确认)标志的数据包作为响应。此时服务器进入 SYN_RECV 状态,服务端收到客服端发送的消息,服务端发送消息,客户端接收消息,证明客户端发送和接收消息是正常(证明了三个点)
三次握手(ACK),客户端收到服务器的响应后,会发送一个带有 ACK 标志的数据包给服务器。服务器和客户端都知道连接已经建立。此时客户端进入 ESTABLISHED 状态,服务器也进入 ESTABLISHED 状态,客户端接收服务端发送的数据并发送接收凭证,服务端收到接收凭证,证明服务端发送数据没有问题(证明了四个点)

四次挥手(断开连接)

第一次挥手(FIN):Client完成数据传输后,发送带有FIN标志的数据包Server,表示Client已经完成数据发送,但仍然可以接收数据。Client进入FIN_WAIT_1
第二次挥手(ACK):Server收到Client的FIN的数据包,然后会发送一个带有 ACK 标志的数据包作为确认,表示服务器收到了客户端的结束请求。Server进入CLOSE_WAIT,客户端仍在等待服务器的结束请求。
第三次挥手(FIN):当Server也完成数据传输后,会向Client发送一个带有 FIN 标志的数据包,表示服务器已经完成数据发送。Server进入LAST_ACK
 第四次挥手(ACK):Client收到Server的FIN数据包后,发送一个带有 ACK 标志的数据包作为确认。此时Client进入TIME_WAIT 状态,等待一段时间以确保Server收到了确认,Server收到确认后,进入CLOSED 状态,连接终止。

IO

I/O模型

  • 同步:线程自己去获取结果(一个线程),自己亲自干,不让别人插手
  • 异步:线程自己不去获取结果,而是由其他线程发送结果(至少两个线程),让别人干,我在这等着结果
  • 阻塞:针对同步来说的,在一个线程里面,我就一直在哪等着,其他事情也不干,就一直再等数据准备好
  • 非阻塞:针对同步来说,在一个线程里面,我自己时不时的去问东西准备好没,但是我也没真正干事情,一遍又一遍的问,数据准备好了没,好了的话,我就去干事了

同步阻塞,同步非阻塞,多路复用(同步请求),异步阻塞,异步非阻塞,多路复用,信号驱动

阻塞I/O,以发起read事件为例,这个时候会从用户态切换为操作系统内核态,用户read会一直阻塞,直到数据返回
非阻塞I/O,频繁切换用户态和内核态,导致CPU空转,某钟角度来说,比阻塞I/O差一点

Java的I/O不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的,一次I/O请求,可以分解为下面5个步骤
磁盘 -》 内核缓冲区(内核态) -》 用户缓冲区(用户态) -》 socket缓冲区(内核态) -》 网卡

Channel(通道)

Channel是一种支持读和写的双向通道,在Java中有这几种常见的Channel,FileChannel / ServerSocketChannel / SocketChannel,其中ServerSocketChannel / SocketChannel重点提一下,在单线程下分为阻塞和非阻塞两种模式

  • 单线程阻塞情况下处理多个连接,每个连接会相互影响

        ServerSocketChannel.configureBlocking(true); 默认为true,阻塞模式
        SocketChannel.configureBlocking(true); 默认为true,阻塞模式

  •  单线程非阻塞情况下处理多个连接,每个连接在建立时,是互相独立的,每个连接的建立是独立的,每个连接读写数据是独立的,每个连接的建立和读写数据都是无限循环实现的

        ServerSocketChannel.configureBlocking(false);  无限循环,直到有新的连接建立
        SocketChannel.configureBlocking(false); 无限循环,直到有新的读写数据操作发生
        缺点:循环空转,导致CPU升高,这个问题可以通过Selector来解决,所谓得Selector就是在ServerSocketChannel和SocketChannel上面注册一个Selector事件,以后不管哪个Channel发生了accept/read/write操作,Selector都会自主监听到这些操作得发生,然后可以使用Selector相关api进行accept/read/write操作,以达到单线程模式下高效处理网络I/O的目的。

stream VS channel

  • stream不会自动缓冲数据,channel会利用系统提供的发送缓冲区/接收缓冲区(更会底层)
  • stream仅支持阻塞API,channel同时支持阻塞/非阻塞API,网络channel可配合selector实现多路复用,但是像FileChannel就不能使用selector,二则均为双工,即读写可以同时进行

   当调用一次channel.read或stream.read,会切换至操作系统内核态来完成真正数据的读取,而读取又分为两个阶段,分别为:等待数据阶段,复制数据阶段

Selector(事件选择器)

Selector也就是我们常说的事件选择器,单个线程可以用配合Selector完成对多个Channel的多个事件的监控,这个就叫多路复用。多路复用仅针对网络IO,普通文件没办法利用多路复用,如果不用Selector的非阻塞模式,线程大部分时间都在做无用功,就是上面说的api空转,而Selector可以保证有可连接的时才去连接/有可读事件时才去读/有可写事件时才去写入,限于网络传输的限制,Channel未必时时可写,一旦channel可写,会触发Selector的可写事件

单线程版本Selector

  • 可以使单个线程能够处理更多的socket
  • Selector.select()方法,没有事件发生时,线程会阻塞,有事件,线程才会恢复运行,
  • Selector.select()方法在事件未处理时,它不会阻塞,事件发生后要么处理要么取消,不能置之不理,不然会一直空转
  • Selector.selectorKeys()内部包含了选择器所有的事件,可以通过这个方法处理你自己关心的事件,如果关心read事件,你就订阅read事件就好了

多线程版本Selector

  • 单线程版本的多路复用器存在一个问题,如果某个事件处理耗费时间过长,会影响其他事件的处理。多线程版本的核心思想是专职专责,一个线程与一个selector配合,然后可以只关心accept的事件,然后根据业务逻辑和CPU核心数,启动多个线程,每个线程绑定selector然后用来处理读写事件

Buffer(缓冲区)

与Channel配合使用,从Channel中读取的数据放到Buffer(缓冲区)里面,常用的是ByteBuffer。Buffer有读和写两种模式,flip()切换为读模式,clear()切换为写模式。Buffer初始化,有两种分配内存的方法,一个是直接使用JVM堆内存,一个是使用系统内存
Buffer中有三个重要的概念,capacity(容量)/ limit(读写限制) / position(读写位置)

  • capacity,缓冲区的最大容量
  • limit, 读和写的限制大小,调用flip()之后,limit就是本次读的位置限制,position变为0就是从头读
  • position, 类似位置指针,就是读和写所在的位置

黏包,半包,拆包

黏包

现象:发送abc def,接收abcdef,黏包多条数据黏在一起发送,导致接收端无法区分出一条完整的数据,半包是和黏包对应的概念,黏在一起的数据,可能会导致之后的数据缺失,就会导致数据缺失这个是半包。
原因:

  • 应用层:接收方ByteBuf设置过大(Netty默认设置为1024)
  • 滑动窗口:假设发送方发送256byte为一个完整的数据,但是由于接收方处理不及时且窗口大小足够大,这256byte就会缓冲在接收方的滑动窗口中,当滑动窗口缓冲了多个报文会造成黏包现象
  • Nagle 算法:会造成黏包
半包

现象:发送abcdef,接收abc edf

原因:

  • 应用层:接收方ByteBuf小于实际数据量大小
  • 滑动窗口: 假设接收方的窗口大小只剩了128 byte,发送的报文大小是256 byte,这时候接收方的缓冲区放不下256 byte,只能先发送前128 byte,等待ack后才能发送剩余的部分,这就造成了半包
  • MSS限制: 当发送的数据超过MSS限制后,会将数据切分发送,就会造成半包

MSS是传输层的报文载荷长度(不包含报头),

MTU是数据链路层最大载荷长度,其中MTU包含了MSS,MTU-IP头-TCP=MSS

拆包 

将黏包/半包的数据经过处理组合一条完整的数据,供接收端解析。

      Demo:
      "Hello, world\nI'm zhangsan\nHo"   --- 黏包
      "w are you?\n" --- 半包
      一条完整数据,可以通过\n标记拆除,完整数据的长度为length=i+1-position

 如何解决黏包半包
  • 短链接

可以解决server端的黏包问题,客户端每发送完一次完整数据,就主动断开链接,下一次发消息再重新建立链接,人为的制造消息边界,可以解决黏包的问题,但是解决不了半包问题。

  • 定长解码器

使用Netty中的定长解码器类(FixedLengthFrameDecoder),也是人为规定每次发送消息大小,这样接收方在接收数据的时候,可以使用FixedLengthFrameDecoder直接接收固定大小的数据。

需要注意的是需要分析业务中最大数据的byte来决定 定长 的大小,因为如果发送的数据过小可以补上默认值,虽然会浪费空间,但是如果发送的数据大小超过定长,那么还是会产生黏包和半包的现象。

  • 行解码器

可以使用Netty中的LineBasedFrameDecoder类,需要人为的将消息用默认的\n 或者 \r\n 或者自定义分隔进行消息分隔,这样接收端解析的时候可以用分隔符解析出一条完整的消息,但是这样会带来一个额外的解析开销,需要一个字节一个字节找到分隔符才行,效率较低。

  • LTC解码器

使用LengthFieldBasedFrameDecoder,是在消息体中加上本次消息的长度,这样接收方接收到数据之后可以根据消息长度,直接把完整的消息解析成功。LengthFieldBasedFrameDecoder有四个非常重要的属性

int maxFrameLength: 消息的最大长度

int lengthFieldOffset:表示长度的位置是从哪一个字节开始的

int lengthFieldLength:表示长度是用了多少个字节

int lengthAdjustment:

int initialBytesToStrip: 还差多少个字节到正文

Netty中的ByteBuf

ByteBuf是对NIO中的ByteByffer的增强和优化。

相比于NIO中ByteBuffer中优势如下:

  • 使用了池化技术,可以重用池中ByteBuf实例,更节约内存,减少内存溢出的可能性。
  • 使用了读写指针,不需要像ByteBuffer中一样读写模式切换
  • 可以自动扩容
  • 支持链式调用
  • 很多地方使用了零拷贝的思想,如slice、duplicate、CompsiteByteBuf等方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值