学习socket

通信之王socket yyds,只要有通信,就需要用到socket,它是对tcp/udp协议的封装,位于传输层与应用层之间(http在应用层) Tomcat服务器/servlet容器也是基于socket,启动tomcat可以理解为创建serversocket 监听某个端口,有连接进来时,创建一个socket,获取输入流,然后解析数据(是否符合http规范?请求是什么?等等,调用对应的servlet处理业务,然后返回数据)

Socket通信一个比较关键的问题:服务端对IO的处理模型

最开始学习Socket的时候(在学校) 使用的模型是阻塞多线程,就是主线程创建ServerSocket,然后循环while(true)调用accept方法来监听Socket的接入,一旦有新的Socket接入之后,便开启一个线程,进行业务处理。

while (true) {
    /**
     *  无限循环 监听客户端连接,如果没有客户端接入,将阻塞在accept()操作上
     *  Socket socket = new Socket(DEFAULT_SERVER_IP, DEFAULT_PORT);
     *  客户端执行上述代码,即接入之后,就会执行accept() 继续往下走,不是等客户端发送消息
     *  如果客户端通过socket连续发送消息,那accept()只会执行一次
     *  serverSocket.accept() 监听的是Socket的接入,不是消息,消息是在新的处理线程中操作的
     *  通过获取到的Socket进行消息的发送/返回
     *  Socket是长连接,同一个Socket可以进行多次的发送/返回
     */
    Socket socket = serverSocket.accept();
    // 开启新线程,处理对应的业务(可以使用对应的线程池)
    System.out.println("收到客户端连接,开启线程处理...");
    new Thread(new ServerHandler(socket)).start();
}

这种处理模型存在如下问题:
(1)每个Socket要绑定一个单独的线程,服务器无法处理高并发的情况,线程耗尽
(2)线程大多数时间处于等待状态,造成极大的资源浪费,因为不知道客户端什么时候有数据传送过来,所以对应的线程一直在while(true) 死循环遍历

为什么一个Socket需要一个线程来处理?最根本的原因:IO是阻塞的!
阻塞IO是指服务器在读写数据时是阻塞的,读取客户端数据时要等待客户端发送数据并且把操作系统内核复制到用户进程中,这时才解除阻塞状态。写数据回客户端时要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。
每个线程执行到读取或者写入操作时都将进入阻塞状态,直到读取到客户端的数据或者数据成功写入客户端后才解除阻塞状态。

InputStream的read()方法是一个阻塞方法,直到有数据可用
那么如下情况,会导致资源消耗严重,性能下降
1. 对方一直没有数据传输过来,即没有数据可用,则一直阻塞,线程一直被占用!!!(这是最主要的情况,客户端只是连接了,还没准备好数据,服务器端的业务线程就要一直等待/死循环 while(true))
2. 一般我们读取数据,会通过byte[]循环读取,直到流结束,read()方法返回-1
那么如果网络传输慢,每次返回几个字节,read()每次都是返回3,5,10,一直没有-1
就要一直慢慢处理,直到结束,性能差!

为了提高通信效率,必须使用非阻塞IO模型!!!
阻塞IO 即:执行到read()方法时线程就阻塞,一直等到有数据+数据读取完成,才能往下执行(如果客户端一直没准备好数据,等了半天,数据准备好了,传送的过程由于网络等原因又慢,那么性能就极低)
解决办法是:不需要应用端的进程死等,等有数据(数据准备好的)的时候告诉他就可以!!!(回调思想 yiben-tech)

在优化之前,我们需要先了解一下,非阻塞情况下套接字事件的检测机制,一般有如下3种检测方式:
(1)应用程序遍历套接字 (伪NIO)
服务端保持一个Socket列表,用一个线程对Socket列表轮询,尝试读取或写入。读取到一点就处理一点(比如存入一个buffer),等一个Socket读取完成,再进行业务处理。
这个最多算是解决了多线程的问题,实际的读写还是阻塞的(还是要死等客户端写入数据),而且轮询的话,会出现连接空闲时也会占用较多cpu资源,不适合实际使用
(2)内核遍历套接字的事件检测(遍历交个内核,效率也不高,因为轮询就是一种低效的模式)
服务器端有多个客户端连接,应用层向内核请求读写事件列表,内核遍历所有Socket,生成readList/writeList……
(3)内核基于回调的事件检测
通过遍历的方式检测Socket是否可读可写是一种效率比较低的方式,不管是在应用层中遍历还是在内核中遍历。所以需要另外一种机制来优化,那就是回调函数。
内核中的Socket都对应一个回调函数,当客户端往Socket发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件。
由操作系统内核维护客户端的所有连接并通过回调函数不断更新事件列表,而应用层线程只要遍历这些事件列表即可知道可读取或可写入的连接,进而对这些连接进行读写操作,极大提高效率。

对于Java来说,非阻塞I/O的实现完全是基于操作系统内核的非阻塞I/O,它将操作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心操作系统。JDK会帮我们选择非阻塞I/O的实现方式,例如对于Linux系统,在支持epoll的情况下JDK会优先选择用epoll实现Java的非阻塞I/O。这种非阻塞方式的事件检测机制就是效率最高的“内核基于回调的事件检测”。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注