网络传输
bio、nio、aio
bio
阻塞式io,服务端每次只能处理一个请求,处理完成后才能继续接受客户端的请求。
优化:每次收到请求后,开启一个线程去专门处理。
缺点:若是并发量很大,便会开启很多的线程,每个线程都占用一定的内存空间,最后导致内存占满。
nio
非阻塞式io,客户端发送的请求会注册到服务端的多路复用器,多路复用器监听有事件发生的文件描述符,服务器内核再对这些文件描述符进行处理。
aio
异步非阻塞式io,目前应用较少。
java nio三大组件channel、buffer、selector
buffer
内存缓冲区,本质上是一个数组,分为各种类型(如:bytebuffer,intbuffer,shortbuffer,longbuffer等,主要使用bytebuffer),用于暂存channel中读取的数据,以及即将写入channel的数据。
channel
数据传输的双向通道,可以从channel将数据读入buffer,也可以将buffer的数据写入channel。常见的channel有:
- FileChannel
- DatagramChannel(常用于UDP)
- SocketChannel(常用于TCP)
- ServerSocketChannel(常用于TCP)
selector
多路复用器,每次有新的客户端连接都会创建一个新的NIO channel,channel会注册到selector。每当有请求发生,selector就能监听到对应的channel,然后对这些channel提供服务。
select、poll、epoll、fd
select - O(n)
每次都是无差别地轮询所有的channel。本质上是有一个记录了当前连接请求fd的数组,包括fd的数量,每次有请求发生,都会遍历此数组。有最大连接数的限制。
poll - O(n)
与select差不多,也是轮询所有的channel。但其将数据结构改为链表,无最大连接数的限制。
epoll - O(1)
监听有事件发生的channel,然后只对有事件发生的channel进行处理。
fd
fd,文件描述符,本质上是进程打开文件的一个索引。linux系统中一切皆文件,文件描述符用来记录系统已经打开的文件的索引,从0开始分配,当进程开启时,会打开3个默认的文件描述符,stdin(键盘),stdout(显示器),stderr(显示器),以后打开的文件描述符从3开始分配。
每个进程都有一个指针 *files 保存在进程控制块PCB中 , 它指向一张表files_struct,该表包含一个指针数组,每个元素都是一个指向打开文件的指针。本质上,文件描述符就是该数组的下标。所以,只要获取到文件描述符,就可以找到对应的文件。
操作系统维护了三张表,进程级别的文件描述符表,系统级别的打开文件表,系统级别的i-node表。文件描述符表即进程打开的文件,每一项都指向系统级的打开文件表。打开文件表中offset即文件中的偏移量,status指操作文件的权限状态(可读,可写等),ptr指向真正的文件地址。系统中任何进程打开任何文件都会在打开文件表中添加一个记录项,按照一般情况下来说两个不同的进程打开相同的文件也会在表中创建两个不同的表项,因此两个进程对同一个文件可以有不同的状态标志以及文件当前偏移量。I-node表存放着文件的相关信息。
粘包与拆包
TCP、UDP是传输层的协议,TCP会发生粘包,UDP不会发生粘包。
UDP不会发生粘包现象。UDP是基于报文发送的,从UDP的报文结构可以看出,在UDP首部采用了16bit来只是UDP数据报文的长度,因此在应用层能够很好地将不同的数据报文区分开,从而避免粘包和拆包的问题。
TCP是基于字节流的瑞然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或则拆包现象发生的可能。
拆包
- 应用程序写入的数据大于TCP发送缓冲区剩余缓空间大小,这将会发生拆包;
- 待发送的数据大于MSS(最大报文长度),当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
粘包
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
- 接受数据段的应用层没有及时读取接受缓存区中的数据,将发生粘包。
解决
- 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了;
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接受缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来;
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。