1. 计算机网络编程基础
##1.七层模型 七层模型(OSI,Open System Interconnection参考模型),是参考是国际标准化组织制定的一个用于计算机或通讯系统间互联的标准体系。它是一个七层抽象的模型,不只包括一系列抽象的术语和概念,也包括具体的协议。 经典的描述以下:web
简述每一层的含义:面试
- 物理层(Physical Layer):创建、维护、断开物理链接。
- 数据链路层 (Link):逻辑链接、进行硬件地址寻址、差错校验等。
- 网络层 (Network):进行逻辑寻址,实现不一样网络之间的路径选择。
- 传输层 (Transport):定义传输数据的协议端口号,及流控和差错校验。
- 会话层(Session Layer):创建、管理、终止会话。
- 表示层(Presentation Layer):数据的表示、安全、压缩。
- 应用层 (Application):网络服务与最终用户的一个接口
每一层利用下一层提供的服务与对等层通讯,每一层使用本身的协议。了解了这些,然并卵。可是,这一模型确实是绝大多数网络编程的基础,做为抽象类存在的,而TCP/IP协议栈只是这一模型的一个具体实现。编程
##2.TCP/IP协议模型 IP数据包结构: 
TCP数据包结构: segmentfault
###一个模型例子: 寻址过程:每台机子都有个物理地址MAC地址和逻辑地址IP地址,物理地址用于底层的硬件的通讯,逻辑地址用于上层的协议间的通讯。寻址过程会先使用ip地址进行路由寻址,在不一样网络中进行路由转发,到了同一个局域网时,再根据物理地址进行广播寻址,数据在以太网的局域网中都是以广播方式传输的,整个局域网中的全部节点都会收到该帧,只有目标MAC地址与本身的MAC地址相同的帧才会被接收。数组
创建可靠的链接:A向B传输一个文件时,若是文件中有部分数据丢失,就可能会形成在B上没法正常阅读或使用。 TCP协议就是创建了可靠的链接: TCP三次握手肯定了双方数据包的序号、最大接受数据的大小(window)以及MSS(Maximum Segment Size)安全
会话层用来创建、维护、管理应用程序之间的会话,主要功能是对话控制和同步,编程中所涉及的session是会话层的具体体现。表示层完成数据的解编码,加解密,压缩解压缩等。服务器
#2.Socket编程 在Linux世界,“一切皆文件”,操做系统把网络读写做为IO操做,就像读写文件那样,对外提供出来的编程接口就是Socket。因此,socket(套接字)是通讯的基石,是支持TCP/IP协议网络通讯的基本操做单元。socket实质上提供了进程通讯的端点。进程通讯以前,双方首先必须各自建立一个端点,不然是没有办法创建联系并相互通讯的。一个完整的socket有一个本地惟一的socket号,这是由操做系统分配的。网络
在许多操做系统中,Socket描述符和其余IO描述符是集成在一块儿的,操做系统把socket描述符实现为一个指针数组,这些指针指向内部数据结构。进程进行Socket操做时,也有着多种处理方式,如阻塞式IO,非阻塞式IO,多路复用(select/poll/epoll),AIO等等。 多路复用每每在提高性能方面有着重要的做用。 当前主流的Server侧Socket实现大都采用了epoll的方式,例如Nginx, 在配置文件能够显式地看到 use epoll。session
举个栗子 Java中Socket服务端的简单实现:基本思路就是一个大循环不断监听客户端请求,为了提升处理效率可使用线程池多个线程进行每一个链接的数据读取数据结构
public class BIOServer { private ServerSocket serverSocket; private ExecutorService executorService = Executors.newCachedThreadPool(); class Handler implements Runnable { Socket socket; public Handler(Socket socket) { this.socket = socket; } @Override public void run() { try { BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream())); String readData = buf.readLine(); while (readData != null) { readData = buf.readLine(); System.out.println(readData); } } catch (Exception e) { e.printStackTrace(); } } } public BIOServer(int port) { try { serverSocket = new ServerSocket(port); } catch (IOException e) { e.printStackTrace(); } } public void run() { try { Socket socket = serverSocket.accept(); executorService.submit(new Handler(socket)); } catch (Exception e) { } } }
客户端:创建socket链接、发起请求、读取响应
public class IOClient { public void start(String host, int port) { try { Socket s = new Socket("127.0.0.1",8888); InputStream is = s.getInputStream(); OutputStream os = s.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); bw.write("测试客户端和服务器通讯,服务器接收到消息返回到客户端\n"); bw.flush(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String mess = br.readLine(); System.out.println("服务器:"+mess); } catch (Exception e) { e.printStackTrace(); } } }
#3.IO模型
对于一次IO访问(以read举例),数据会先被拷贝到操做系统内核的缓冲区page cache中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个read操做发生时,它会经历两个阶段:
- 等待数据准备
- 将数据从内核拷贝到进程中
IO模型的分类有下:
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 异步 I/O(asynchronous IO)
BIO 阻塞 I/O
缺点:一个请求一个线程,浪费线程,且上下文切换开销大;
上面写的socket列子就是典型的BIO
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来讲,不少时候数据在一开始尚未到达。好比,尚未收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程须要等待,也就是说数据被拷贝到操做系统内核的缓冲区中是须要一个过程的。而在用户进程这边,整个进程会被阻塞(固然,是进程本身选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
NIO 非阻塞 I/O
当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error 。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
nonblocking IO的特色是用户进程须要不断的主动询问kernel数据好了没有。
I/O 多路复用
IO multiplexing就是咱们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。
机制:一个线程以阻塞的方式监听客户端请求;另外一个线程采用NIO的形式select已经接收到数据的channel信道,处理请求;
- select,poll,epoll模型 - 处理更多的链接
上面所说的多路复用的select,poll,epoll本质上都是同步IO,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,其实是指阻塞在select上面,必须等到读就绪、写就绪等网络事件。异步IO则无需本身负责进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
I/O 多路复用的特色是经过一种机制一个进程能同时等待多个文件描述符, 而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就能够返回。因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/ epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。
- 一个面试问题:select、poll、epoll的区别?
Java中的I/O 多路复用: Reactor模型
(主从Reactor模型)netty就是主从Reactor模型的实现,至关于这个模型在
对比与传统的I/O 多路复用,Reactor模型增长了事件分发器,基于事件驱动,可以将相应的读写事件分发给不一样的线程执行,真正实现了非阻塞I/O。
基于Reactor Pattern 处理模式中,定义如下三种角色
- Reactor将I/O事件分派给对应的Handler
- Acceptor处理客户端新链接,并分派请求处处理器链中
- Handlers执行非阻塞读/写 任务
举个栗子 回顾咱们上面写的代码,是否是每一个线程处理一个链接,显然在高并发状况下是不适用的,应该采用 IO多路复用 的思想,使得一个线程可以处理多个链接,而且不能阻塞读写操做,添加一个 选择器在buffer有数据的时候就开始写入用户空间.这里的多路是指N个链接,每个链接对应一个channel,或者说多路就是多个channel。复用,是指多个链接复用了一个线程或者少许线程
如今咱们来优化下上面的socket IO模型 ####优化后的IO模型: 实现一个最简单的Reactor模式:注册全部感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。流程就是不断轮询能够进行处理的事件,而后交给不一样的handler进行处理. 上面提到的主要是四个网络事件:有链接就绪,接收就绪,读就绪,写就绪。I/O复用主要是经过 Selector复用器来实现的,能够结合下面这个图理解上面的叙述
public class NIOServer { private ServerSocketChannel serverSocket; private Selector selector; private ReadHandler readHandler; private WriteHandler writeHandler; private ExecutorService executorService = Executors.newCachedThreadPool(); abstract class Handler { protected SelectionKey key; } class ReadHandler extends Handler implements Runnable { @Override public void run() { ///...读操做 } } class WriteHandler extends Handler implements Runnable { @Override public void run() { ///...写操做 } } public NIOServer(int port) { try { selector = Selector.open(); serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress(port)); serverSocket.register(this.selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void run() { while (!Thread.interrupted()) { try { selector.select(); //阻塞等待事件 Iterator<SelectionKey> iterator = this.selector.keys().iterator(); // 事件列表 , key -> channel ,每一个KEY对应了一个channel while (iterator.hasNext()) { iterator.remove(); dispatch(iterator.next()); //分发事件 } } catch (IOException e) { e.printStackTrace(); } } } private void dispatch(SelectionKey key) { if (key.isAcceptable()) { register(key); //新链接创建,注册一个新的读写处理器 } else if (key.isReadable()) { this.executorService.submit(new ReadHandler(key)); //能够写,执行写事件 } else if (key.isWritable()) { this.executorService.submit(new WriteHandler(key)); //能够读。执行读事件 } } private void register(SelectionKey key) { ServerSocketChannel channel = (ServerSocketChannel) key.channel(); //经过key找到对应的channel try { SocketChannel socketChannel = channel.accept(); channel.configureBlocking(false); channel.register(this.selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } }
优化线程模型
上述模型还能够继续优化。由于上述模型只是增多个客户端链接的数量,可是在高并发的状况下,