一,回望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 static Map<SocketChannel,SocketChannel> proxyChannelMap = new HashMap<>(); //client socket 对应的代理到目的服务器的socket
private static Map<SocketChannel,ByteBuffer> backBufferToClient = new HashMap<SocketChannel,ByteBuffer>(); //要发给客户端的消息
private static Map<SocketChannel,ByteBuffer> forwardBufferToServer = new HashMap<>(); //要发给目的服务器的消息
public static void main(String[] s) {
buildServerSocketChannel();
// ServerSocketChannel ssc = new ServerSocket(11111);
}
public static ServerSocketChannel buildServerSocketChannel() {
ServerSocketChannel ssc = null;
try {
ssc= ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().setReuseAddress(true);
ssc.bind(new InetSocketAddress(11111));
System.out.println("server channel built");
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
System.out.println("==========================");
if(selector.select()>0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
try {
if(selectionKey.isAcceptable()) {
afterAccept(selectionKey);
}
if(selectionKey.isConnectable()) {
afterConnect(selectionKey);
}
if(selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
String side = (String) selectionKey.attachment();
if(side.equals("0")) { //此channel是客户端与代理服务器的channel
ByteBuffer buffer = null;
SocketChannel proxyChannel = proxyChannelMap.get(channel);
if(proxyChannel!=null) {
if(forwardBufferToServer!=null && forwardBufferToServer.get(proxyChannel) !=null) {
buffer = forwardBufferToServer.get(proxyChannel);
}else {
buffer = ByteBuffer.allocate(1024*2);
}
try {
channel.read(buffer); //debug一下,看看在channel准备好的情况下,能不能一次性把所有内容读到buffer
buffer.flip();
if(buffer.limit()>0) {
forwardBufferToServer.put(proxyChannel, buffer);
selectionKey.interestOps(0);
proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
}
} catch (IOException e) {
proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
selectionKey.cancel();
channel.close();
proxyChannelMap.get(channel).close();
}
}else { //第一次连接
buffer = ByteBuffer.allocate(1024*2);
channel.read(buffer);
selectionKey.interestOps(0);
buffer.flip();
String headerStr = new String(buffer.array(),buffer.position(),buffer.limit(),"US-ASCII");
String[] lineStrs = headerStr.split("\\n");
String host="";
int port = 80;
int type=0; //默认是http方式
String hostTemp="";
for(int i=0 ; i<lineStrs.length ; i++) { //解析请求头
System.out.println(lineStrs[i]);
if(i==0) {
type = (lineStrs[i].split(" ")[0].equalsIgnoreCase("CONNECT") ? 1 : 0);
}else {
String[] hostLine = lineStrs[i].split(": ");
if(hostLine[0].equalsIgnoreCase("host")) {
hostTemp = hostLine[1];
}
}
}
if(hostTemp.split(":").length>1) {
host = hostTemp.split(":")[0];
port = Integer.valueOf(hostTemp.split(":")[1].split("\\r")[0]);
}else {
host = hostTemp.split(":")[0].split("\\r")[0];
}
try {
proxyChannel = SocketChannel.open(new InetSocketAddress(host, port));
} catch (Exception e) {
continue;
}
proxyChannel.configureBlocking(false);
proxyChannel.register(selector, SelectionKey.OP_CONNECT, "1");//将代理到目的服务器的连接注册到selector
proxyChannelMap.put(channel, proxyChannel);
proxyChannelMap.put(proxyChannel,channel);
if(type==1) { //https直接返回告诉客户端连接已建立
ByteBuffer writeBuffer = ByteBuffer.wrap("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes());
backBufferToClient.put(channel, writeBuffer);
selectionKey.interestOps(SelectionKey.OP_WRITE);
}else {
forwardBufferToServer.put(proxyChannel, buffer);
proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
}
}
}else { //此channel是代理服务器与目标服务器的channel
ByteBuffer buffer = null;
SocketChannel proxyChannel = proxyChannelMap.get(channel); //获取代理服务器到client的连接通道
if(proxyChannel==null) {
System.out.println("----------------channel broken!!");
}else {
if(backBufferToClient!=null && backBufferToClient.get(proxyChannel) !=null) {
buffer = backBufferToClient.get(proxyChannel);
}else {
buffer = ByteBuffer.allocate(1024*2);
}
try {
channel.read(buffer);
buffer.flip();
if(buffer.limit()>0) {
selectionKey.interestOps(0);
backBufferToClient.put(proxyChannel, buffer);
proxyChannel.keyFor(selector).interestOps(SelectionKey.OP_WRITE);
}
} catch (IOException e) {
if(proxyChannelMap.get(selectionKey.channel())!=null) {
proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
proxyChannelMap.get(channel).close();
}
selectionKey.cancel();
channel.close();
System.out.println("++++++++++++++++++++++++++++++close a client channel");
}
}
}
}
if(selectionKey.isWritable()) {
System.out.println("writeable++++++++++++++"+selectionKey.channel());
SocketChannel channel = (SocketChannel) selectionKey.channel();
String side = (String) selectionKey.attachment();
try {
if(side.equals("0")) { //此channel是客户端与代理服务器的channel
int a=0;
if(backBufferToClient != null && backBufferToClient.get(channel)!=null
&&(backBufferToClient.get(channel).position()<backBufferToClient.get(channel).limit())
&& (a=channel.write(backBufferToClient.get(channel)))>0) {
if(!backBufferToClient.get(channel).hasRemaining()) {
backBufferToClient.get(channel).clear();
if(proxyChannelMap.get(channel) !=null) {
proxyChannelMap.get(channel).keyFor(selector).interestOps(SelectionKey.OP_READ);
}
selectionKey.interestOps(SelectionKey.OP_READ);
}else {
if(proxyChannelMap.get(channel) !=null) {
proxyChannelMap.get(channel).keyFor(selector).interestOps(0);
}
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
}
}else {
int a=0;
if(forwardBufferToServer != null && forwardBufferToServer.get(channel) !=null
&&(forwardBufferToServer.get(channel).position()<forwardBufferToServer.get(channel).limit())
&& (a=channel.write(forwardBufferToServer.get(channel))) >0) {
if(!forwardBufferToServer.get(channel).hasRemaining()) {
forwardBufferToServer.get(channel).clear();
if(proxyChannelMap.get(channel) !=null) {
proxyChannelMap.get(channel).keyFor(selector).interestOps(SelectionKey.OP_READ);
}
selectionKey.interestOps(SelectionKey.OP_READ);
}else {
if(proxyChannelMap.get(channel) !=null) {
proxyChannelMap.get(channel).keyFor(selector).interestOps(0);
}
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
}
}
} catch (IOException e) {
if(proxyChannelMap.get(selectionKey.channel())!=null) {
proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
proxyChannelMap.get(channel).close();
}
selectionKey.cancel();
channel.close();
}
}
} catch (CancelledKeyException e) {
System.out.println("cancle channel:"+selectionKey.channel());
System.out.println("cancle proxy channel:"+proxyChannelMap.get(selectionKey.channel()));
if(proxyChannelMap.get(selectionKey.channel())!=null){
proxyChannelMap.get(selectionKey.channel()).keyFor(selector).cancel();
proxyChannelMap.remove(proxyChannelMap.get(selectionKey.channel()));
}
selectionKey.cancel();
System.out.println("cancle the selectionkey+++++++++++++++++++++++++++");
proxyChannelMap.remove(selectionKey.channel());
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return ssc;
}
public static void afterAccept(SelectionKey selectionKey) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
Selector selector = selectionKey.selector();
socketChannel.register(selector, SelectionKey.OP_READ,"0");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void afterConnect(SelectionKey selectKey) {
try {
SocketChannel socketChannel = (SocketChannel) selectKey.channel();
Selector selector = selectKey.selector();
socketChannel.keyFor(selector).interestOps(SelectionKey.OP_READ);
} catch (Exception e) {
System.out.println("建立到目的服务器socket后出错:"+e);
}
}
}
必须承认,上面的程序我认为没有必要每次在读结束后执行selectionKey.interestOps(0);这句话,这句话作用是暂时让selector不对刚读完的这个通道的任何事件感兴趣,也就是希望当把刚读出来的buffer内容全部写到对应的到目的服务器的通道后在触发selector再次对这个通道的读事件感兴趣,设计这句话的初衷是因为我担心因为下一步要读和要写必须有一个确定的步骤(因为nio中的buffer对于读和写用的是同一个position和limit,多说一句netty中就把buffer中的读和写的position和limit就区分开了,所以就不需要有这样的担心),但是后来想一想,我这个是单线程的啊,一次操作必定是写或者读啊,所以我原来的担心完全是没必要的,这里之所以把代码留下来,也正好加深一下印象。这不会有问题,而且向我们上面所说的channel大部分时候都是writable的,所以等将刚读出到buffer写完到通道才再次触发原来通道可读是效率不大受影响的。但确实在理论上,这个地方确实是不应该设置的。当你要试图去掉每次读后的这行代码时,记得在每次写完后,执行buffer.compact(),这个方法什么作用,你自己查询吧。
五,个人总结
nio真的就那么完美无缺吗?我看不一定,经过个人实践写出的单线程nio代理服务器,首先一个体验——很容易出错,难调试分析错误比较难。其次,其实说实话这个单线程版本nio实现的代理服务器展示出来的效果并没有bio的效果好。分析起来也很正常,虽然nio在我们开发http代理服务器时在访问资源时会很节省资源,但是人家bio是多线程啊,当你实现这个代理服务器只是为你自己用时根本不在乎多那么几个线程时,肯定是多线程处理更快啊,而nio中对所有channel的io操作都是在一个线程中完成的,肯定会出现页面刷出比较慢,尤其是页面元素(图片较多)时会发现刷出慢。但是,nio省资源那是没得说的,在实现聊天那样小流量功能时,这当然非常棒啊,而且我们可以搞多个selector,去监听不同的ServerSocket从而去另开一个线程去监听连接,把客户端的连接负载均衡到这些selector上,这就可以利用你的服务器多核的优势了,而且很大程度的节省内存资源cpu线程切换耗损。