因为有个不会存在大量连接的小的Web服务器需求,不至于用上重量级服务器,于是自己动手写一个服务器。
同时也提供了一个简单的Web框架。能够简单的使用了。
大体的需求包括
会省略一些暂时影响察看的代码。还不够完善,供记录问题和解决办法之用,可能会修改许多地方。
让我们开始吧~
Project的地址 : Github
点这里是这部分的完整代码,可以对照察看
大家都知道HTTP协议使用的是TCP服务。 而要用TCP通信都得从ServerSocket开始。ServerSocket监听指定IP地址指定端口之后,另一端便可以通过连接这个ServerSocket来建立一对一的Socket进行收发数据。
我们先从命令行参数里获得要监听的ip地址和端口号,当然没有的话使用默认的。
1 public static void main(String[] args) { 2 ... 3 InetAddress ip = null; 4 int port; 5 if (args.length == 2 && args[1].matches(".+://d+")) { 6 ... 7 ip = InetAddress.getByName(address[0]); 8 ... 9 } else { 10 ... 11 ip = InetAddress.getLocalHost(); 12 ... 13 port = 8080; 14 System.out.println("未指定地址和端口,使用默认ip和端口..." + ip.getHostAddress() + ":" + port); 15 } 16 17 Server server = new Server(ip, port); 18 server.start(); 19 }
输入是 start 123.45.67.89:8080
或者直接一个 start
InetAddress.getByName(address[0])
通过一个IP地址的字符串构造一个InetAddress对象。
InetAddress.getLocalHost()
获取localhost的InetAddress对象。
接下来看看Server类。
首先,这个服务器要体谅个人电脑的配置,不宜创建太多线程。考虑使用NIO来进行IO处理,一个线程处理IO。所以我们需要一个Selector来选择已经就绪的管道,同时用一个线程池来处理任务。(可以用 Runtime.getRuntime().availableProcessors()
获取可用的处理器核数。)
Server启动时首先进行ServerSocket的绑定以及其他的初始化工作。
1 ServerSocketChannel serverChannel; 2 registerServices(); 3 serverChannel = ServerSocketChannel.open(); 4 serverChannel.bind(new InetSocketAddress(this.ip, this.port)); 5 serverChannel.configureBlocking(false); 6 selector = Selector.open(); 7 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
registerServices()
暂时先忽略,是用来注册用户写的服务的。
由于是NIO,在这里是用的ServerSocketChannel,绑定到ip和端口,设置好非阻塞,注册ACCEPT事件。不设置非阻塞状态是不能使用Selectior的。
然后开始循环监听和处理事件
1 public void start() { 2 init(); 3 while (true) { 4 ... 5 selector.select(); 6 ... 7 Set<SelectionKey> readyKeys = selector.selectedKeys(); 8 Iterator<SelectionKey> iterator = readyKeys.iterator(); 9 while (iterator.hasNext()) { 10 SelectionKey key = iterator.next(); 11 iterator.remove(); 12 if (key.isAcceptable()) { 13 ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel(); 14 ...//处理接受事件 15 } else if (key.isReadable()) { 16 SocketChannel client = (SocketChannel) key.channel(); 17 ...//处理读事件 18 } else if (key.isWritable()) { 19 SocketChannel client = (SocketChannel) key.channel(); 20 ...//处理写事件 21 } 22 ... 23 } 24 } 25 }
在我看来SelectionKey指的就是一个事件,它关联一个channel并且可以携带一个对象。slector.select()
会阻塞直到有注册的事件来临。 获取一个SelectionKey之后需要使用iterator.next()
将它从selectedKeys中去除,不然下次selector.select()
仍然会获取到这个key。
下面来分析每个事件。
Accept事件其实很简单,就是可以来了一个Socket可以建立连接了。 那么就像下面这样,accept创建一个连接后,在SocketChannel监听Read事件,等到有数据可以读的时候就可以进行读取。
1 if (key.isAcceptable()) { 2 ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel(); 3 SocketChannel client = serverSocket.accept(); 4 client.configureBlocking(false); 5 client.register(selector, SelectionKey.OP_READ); 6 }
这个事件就可以接收到HTTP请求了。读取到数据之后提交给 Controller
进行异步的HTTP请求解析,根据FilePath转发给服务处理类。处理完后会给通道注册WRITE的监听。 client.register(selector, SelectionKey.OP_WRITE)
。
并让key携带 Response
对象(将在后续章节写出)
1 if (key.isReadable()) { 2 SocketChannel client = (SocketChannel) key.channel(); 3 ByteBuffer buffer = ByteBuffer.allocate(4096); 4 client.read(buffer); 5 executor.execute(new Controller(buffer, client, selector)); 6 }
这个事件将Response写入SocketChannel。
1 if (key.isWritable()) { 2 SocketChannel client = (SocketChannel) key.channel(); 3 Response response = (Response) key.attachment(); 4 if (response == null) { 5 continue; 6 } 7 ByteBuffer byteBuffer = response.getByteBuffer(); 8 if (byteBuffer.hasRemaining()) { 9 client.write(byteBuffer); 10 } 11 if (!byteBuffer.hasRemaining()) { 12 key.cancel(); 13 client.close(); 14 } 15 }
如果发现什么问题或者有什么建议请指教。谢谢~
附录区:
[1] 当消息主体出现在消息中时,一条消息的传输长度(transfer-length)是消息主体(messagebody)
的长度;也就是说在实体主体被应用了传输编码(transfer-coding)后。当消息中出现
消息主体时,消息主体的传输长度(transfer-length)由下面(以优先权的顺序)决定: