《Spring MVC源码分析与实战》笔记(二)
Socket 与NioSocket
Socket分为ServerSocket和Socket两大类,ServerSocket用于服务端,可以通过accept()方法监听请求,监听到请求后返回Socket,Socket用于具体的数据传输,客户端可以直接使用Socket向服务端发起请求
accept()是阻塞方法,每次服务端监听到一个请求后,会新开一个线程负责与当前Socket通信,假如客户端网络有延迟,服务端对应客户端Socket的线程就会一直等待,对于服务器而言这是一种非常消耗资源的事情,于是从JDK1.4开始,就有了新的IO方式:NioSocket
举个通俗的例子来说,用去餐馆吃饭来打比方,用顾客比喻客户端,传统的Socket相当于每个顾客配有一个专属厨师(比喻ServerSocket新线程),在点菜(比喻客户端准备数据)的时候,顾客考虑多久,这个厨师就等待多久,直到顾客想好要吃什么,厨师才开始做菜(比喻客户端准备数据);而NIO模式下,还是用顾客比喻客户端,但这个时候整个餐厅只需要一个厨师外加一个服务员就够了,顾客想好要吃什么,会通知服务员(也就是事件驱动),然后服务员再将这些请求收集起来一并交给厨师处理,这样一来,本身需要多线程解决的事情现在单线程也解决了
NioSocket中有三个重要概念:
- Buffer:服务端与客户端之间交换的数据
- Channel:分为ServerSocketChannel和SocketChannel,在上文的例子中,对应厨师和顾客
- Selector:在上文的例子中,对应服务员,Selector管理Channel,并注册相应的事件,它采取事件驱动的方式,当注册的事件就绪后,调用相应的线程处理该事件,而不必像传统Socket那样用线程去维持一个长连接,从而减少了线程的开销(注意:Selector与Channel没有谁属于谁的关系,他们类似于数据库中“多对多”的关系,一个顾客可以喊不同的服务员给自己加菜,一个服务员也可以服务不同的顾客)
NioSocket实现简单HTTP协议的例子:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
public class HttpServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080)); // 创建channel 并监听8080端口
ssc.configureBlocking(false); // 设置为非阻塞模式,只有这样才能注册选择器
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT); // 为channel注册选择器
// 创建处理器
while (true) {
// 等待请求,每次等待三秒,超过三秒后程序继续向下执行,如果传入参数0或不传参数将一直阻塞
if (selector.select(3000) == 0) {
continue;
}
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 启动新线程处理SelectionKey
new Thread(new HttpHandler(key)).run();
// 处理完后,从待处理的集合中移除当前key
keyIterator.remove();
}
}
}
private static class HttpHandler implements Runnable {
private int bufferSize = 1024;
private String localCharset = "UTF-8";
private SelectionKey key;
public HttpHandler(SelectionKey key) {
this.key = key;
}
public void handleAccept() throws IOException {
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
public void handleRead() throws IOException {
// 获取channel
SocketChannel sc = (SocketChannel)key.channel();
// 获取buffer并重置
ByteBuffer buffer = (ByteBuffer)key.attachment();
buffer.clear();
// 没有读到内容则关闭
if (sc.read(buffer) == -1) {
sc.close();
} else {
// 接收请求数据
buffer.flip();
String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
// 控制台打印请求报文头
String[] requestMessage = receivedString.split("\r\n");
for (String s : requestMessage) {
System.out.println(s);
// 遇到空行说明报文头已经打印完
if (s.isEmpty()) {
break;
}
}
// 控制台打印首行信息
String[] firstLine = requestMessage[0].split(" ");
System.out.println();
System.out.println("Method:\t" + firstLine[0]);
System.out.println("url:\t" + firstLine[1]);
System.out.println("Http Version:\t" + firstLine[2]);
System.out.println();
// 返回客户端
StringBuilder sendString = new StringBuilder();
sendString.append("HTTP/1.1 200 OK\r\n"); //响应报文首行,200表示处理成功
sendString.append("Content-Type:text/html;charset=" + localCharset + "\r\n");
sendString.append("\r\n"); // 报文头结束后加一个空行
sendString.append("<html><head><title>显示报文</title></head><body>");
sendString.append("接收到的请求报文是:<br/>");
for (String s : requestMessage) {
sendString.append(s + "<br/>");
}
sendString.append("</body></html>");
buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
sc.write(buffer);
sc.close();
}
}
@Override
public void run() {
try {
// 接收到连接请求
if (key.isAcceptable()) {
handleAccept();
}
// 读数据
if (key.isReadable()) {
handleRead();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}