一,回望BIO
上篇博文用了java 阻塞模型socket实现了http及https代理,也简单的说了下其主要缺点是比较耗费资源或者更好的说法是资源利用率不高,为什么呢?一旦客户端和代理服务器每建立一个连接(基本上每请求一个url就会建立一个新的连接)而我们实现的代理服务器为了去监听客户端发给服务器端的消息并转发就建立一个线程去专门的监听相应的流并处理转发这些数据到目标服务器;同样,对于每一个目标服务器到我们实现的代理服务器的连接,上篇博文也是都新建一个线程去监听维护流发来到代理服务器的消息并转发到客户端。这样的设计很简单很直观——客户端和目标服务器每建立一个连接直接就新建一个线程去监听就行了,你不用去管http和https的中间沟通的细节,只需要知道https建立的握手环节就行。但是针对http和https都是一问一答式的交互,显然,我们可以把监听客户端连接的线程和监听服务器连接的线程合并到一个线程中,这样就又减少了线程的数量,减少了线程上下文切换的消耗。不过这样还是不得不用一个线程去维持客户端和目标服务器到我们的代理服务器的连接,即使他们中间不传数据或者说数据已经传完了(我们可以对socket设置so_timeout来让socket 过期,并在线程中抛出我们将捕获的异常来结束线程来回收一些线程资源,但是不可以设置过短,不然有些网站可能网速波动后,就会导致代理服务器断开连接了)。
二,NIO特点分析
那么nio到底好在哪呢?简单来说,nio可以用一个线程来处理代理服务器与客户端以及目的服务器的所有连接。它为什么能做到,主要是因为nio中的selector,channel以及buffer。你要做的就是想办法把连接都附着在一个channel上然后注册到一个selector,让这个selector去管理这些channel,buffer则是内存缓冲的东西负责接收channel发来的信息或者承载你将要发送到channel的信息,我自己的实践中主要用了ByteBuffer,其他几个使用方式基本相同,之所以使用nio包下的这几个buffer是因为封装的操作很适合nio开发,因为nio不想bio每次读写内容都读写至结束,nio则需要记录这些读写过程点,而nio包下的几种buffer就提供这些很方便的api。这么说可能有点抽象,读者可以自己去动手实践便可体会。
三,NIO实现http(s)代理服务器分析
把个人在实现http代理服务器过程中认为比较重要踩坑也较深的地方着重提一下,再给出实现代码,读者可以在看完我的这段话后自己试着实现,然后再看我的代码。
1,理解http和https请求的请求响应过程,都是一问一答模式。2,https有一个握手过程是明文传递消息的,通过这个可以建立最初连接,之后都是在这个建立的通道上代理传递客户端传到服务器的消息的。3,要有清晰地认识,nio在传递消息的过程中一定要对buffer中存入的消息有明确的记录(这个记录意思是,读知道读到的第一个字节(字符)在哪,能读到的最后一个字节(字符)在哪;写:第一个应该写在的位置应该在哪,最后一个能写的位置在哪。)4,有一个意识,我们这里针对buffer的使用都是单线程的,所以不用担心会不会一边channel在读,另一边通道在写的情况的。建议的是时刻将channel的readable事件注册给selector,而其writable要在满足某种情况下你手动设定(比如你有内容读到buffer中时),不要在一开始在生成一个channel时就把channel的writable事件注册给selector,因为正常情况下,channel都是writable的(原谅我这么中英结合,因为我觉得翻译成中真的有点别),这就会导致你的cpu会被大量占用。5,打日志。日志要特别重要,特别体现你的思路,不然会很乱,不好分析(debug就别想了)6,异常捕获,因为我们这里使用单线程实现代理服务器的所以,一旦某个地方出错可能就会导致程序停掉,你肯定不希望某一个url访问错误就导致你的代理服务器停掉吧。
好了,有了上面的的一些经验,下面给出自己的实现。
四,NIO的https实现
package server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.swing.text.Position;
public class ServerMain {
private st